web-dev-qa-db-ja.com

AutoMapperを使用してnull許容プロパティをDTOにマップするにはどうすればよいですか?

私はAzureMobile Serviceを開発しています。私のモデルでは、一部の関係はオプションであり、それを表すプロパティをnull許容にします。

たとえば、モデルクラスのMessageエンティティは次のようになります。

public partial class Message
{
    public Message()
    {
        this.Messages = new HashSet<Message>();
    }

    public int Id { get; set; }
    public int CreatedById { get; set; }
    public int RecipientId { get; set; }
    public Nullable<int> ParentId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public int MessageTypeId { get; set; }
    public Nullable<MessageType> Type { get; set; }
    public Nullable<bool> Draft { get; set; }
    public Nullable<bool> Read { get; set; }
    public Nullable<bool> Replied { get; set; }
    public Nullable<bool> isDeleted { get; set; }

    [JsonIgnore]
    [ForeignKey("CreatedById")]
    public virtual User CreatedBy { get; set; }
    [JsonIgnore]
    [ForeignKey("RecipientId")]
    public virtual User Recipient { get; set; }
    [JsonIgnore]
    public virtual ICollection<Message> Messages { get; set; }
    [JsonIgnore]
    public virtual Message Parent { get; set; }
}

そして、それに対する私のDTOは次のようになります。

public class MessageDTO : EntityData 
{
    public string CreatedById { get; set; }
    public string RecipientId { get; set; }
    public string ParentId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public string Type { get; set; }
    public Nullable<bool> Draft { get; set; }
    public Nullable<bool> Read { get; set; }
    public Nullable<bool> Replied { get; set; }
    public Nullable<bool> isDeleted { get; set; }

}

AutoMapper構成には、次のものがあります。

        /*
         * Mapping for Message entity
         */
        cfg.CreateMap<Message, MessageDTO>()
            .ForMember(messageDTO => messageDTO.Id, map => 
            map.MapFrom(message => MySqlFuncs.LTRIM(MySqlFuncs.StringConvert(message.Id))))
            .ForMember(messageDTO => messageDTO.ParentId, map => 
            map.MapFrom(message => message.ParentId))
            .ForMember(messageDTO => messageDTO.CreatedById, map => 
            map.MapFrom(message => MySqlFuncs.LTRIM(MySqlFuncs.StringConvert(message.CreatedById))))
            .ForMember(messageDTO => messageDTO.RecipientId, map => 
            map.MapFrom(message => MySqlFuncs.LTRIM(MySqlFuncs.StringConvert(message.RecipientId))))
            .ForMember(messageDTO => messageDTO.Type, map => 
            map.MapFrom(message => Enum.GetName(typeof(MessageType), message.MessageTypeId)));

        cfg.CreateMap<MessageDTO, Message>()
            .ForMember(message => message.Id, map => 
            map.MapFrom(messageDTO => MySqlFuncs.IntParse(messageDTO.Id)))
            .ForMember(message => message.ParentId, map => 
            map.MapFrom(messageDTO => messageDTO.ParentId))
            .ForMember(message => message.CreatedById, map => 
            map.MapFrom(messageDTO => MySqlFuncs.IntParse(messageDTO.CreatedById)))
            .ForMember(message => message.RecipientId, map => 
            map.MapFrom(messageDTO => MySqlFuncs.IntParse(messageDTO.RecipientId)));

参考までに、MySqlFuncsクラスには、文字列からintおよびintから文字列への変換を処理するための次の関数があります。

class MySqlFuncs
{
    [DbFunction("SqlServer", "STR")]
    public static string StringConvert(int number)
    {
        return number.ToString();
    }
    [DbFunction("SqlServer", "LTRIM")]
    public static string LTRIM(string s)
    {
        return s == null ? null : s.TrimStart();
    }
    // Can only be used locally.
    public static int IntParse(string s)
    {
        int ret;
        int.TryParse(s, out ret);
        return ret;
    }
}

要素を挿入することはできますが、次のエラーのために要素を取得できません。

Nullable`1からStringへのマップがありません。 Mapper.CreateMap <Nullable`1、String>を使用して作成します

これから、AutoMapper定義のこのエラーの原因となる行は次のようになります。

            .ForMember(messageDTO => messageDTO.ParentId, map => 
            map.MapFrom(message => message.ParentId))

AutoMapperは、null許容整数をParentIdプロパティからDTOにマップする方法を知る必要があります。 MySqlFuncsクラスの関数を使用しようとしました。

            .ForMember(messageDTO => messageDTO.ParentId, map => 
            map.MapFrom(message => MySqlFuncs.LTRIM(MySqlFuncs.StringConvert(message.ParentId))))

しかし、それはエラーを示しています:

'int?'から変換できません'int'へ

LINQが正しく変換する方法で構成を行う必要があると考える場合、マッピングを定義して、null許容プロパティを空の文字列 ""として文字列にマッピングするか、値nullをDTOにマッピングするにはどうすればよいですか?

編集1

これが解決されるように思われるので、次のようにコーディングしたValueResolverを使用する必要があります。

public class NullableIntToStringResolver : ValueResolver<int?, string>
{
    protected override string ResolveCore(int? source)
    {
        return !source.HasValue ? "" : MySqlFuncs.LTRIM(MySqlFuncs.StringConvert(source));
    }
}

そして、マッピングを次のように変更しました。

cfg.CreateMap<Message, MessageDTO>()
     .ForMember(messageDTO => messageDTO.ParentId, 
     map => map.ResolveUsing(
        new NullableIntToStringResolver()).FromMember(message => message.ParentId))

しかし、これは私にエラーを与えています

オブジェクト参照がオブジェクトインスタンスに設定されていません

そして、StackTraceはこれです:

autoMapper.QueryableExtensions.Extensions.ResolveExpression(PropertyMap propertyMap、Type currentType、Expression instanceParameter)で
AutoMapper.QueryableExtensions.Extensions.CreateMemberBindings(IMappingEngine MappingEngine、TypePair typePair、TypeMap typeMap、Expression instanceParameter、IDictionary`2 typePairCount)
AutoMapper.QueryableExtensions.Extensions.CreateMapExpression(IMappingEngine MappingEngine、TypePair typePair、Expression instanceParameter、IDictionary`2 typePairCount)
AutoMapper.QueryableExtensions.Extensions.CreateMapExpression(IMappingEngine MappingEngine、TypePair typePair、IDictionary`2 typePairCount)
AutoMapper.QueryableExtensions.Extensions。<> c__DisplayClass1`2.b__0(TypePair tp)
at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key、Func`2 valueFactory)
AutoMapper.Internal.DictionaryFactoryOverride.ConcurrentDictionaryImpl`2.GetOrAdd(TKey key、Func`2 valueFactory)
AutoMapper.QueryableExtensions.Extensions.CreateMapExpression [TSource、TDestination](IMappingEngine MappingEngine)
AutoMapper.QueryableExtensions.ProjectionExpression`1.ToTResultで
Microsoft.WindowsAzure.Mobile.Service.MappedEntityDomainManager`2.Query()、Microsoft.WindowsAzure.Mobile.Service.TableController`1.Query()

なぜ私がnull参照を取得しているのか考えていますか?

注意

デバッグ時に、GetAllMessageDTOメソッドのTableControllerクラスでエラーがスローされます。

public class MessageController : TableController<MessageDTO>
{
    .
    .
    .
    // GET tables/Message
    public IQueryable<MessageDTO> GetAllMessageDTO()
    {
        return Query(); // Error is triggering here
    }
    .
    .
    .
}

デバッグ時には、このエラーが発生したときにマッピングのどの行にもアクセスされません。これは、サービスが初期化されたときにマッピングが行われるためです。

9
Uriel Arvizu

この問題は簡単に解決できると思います。

次の例を考えてみましょう。

public class A 
{
    public int? Foo { get; set; }
    public MyEnum? MyEnum { get; set; }
}

public class B 
{
    public string Bar { get; set; }
    public string MyEnumString { get; set; }
}

次のマッピングステートメントは、必要に応じてそれらを解決します。

Mapper.CreateMap<A, B>()
      .ForMember(dest => dest.Bar, opt => opt.MapFrom(src 
        => src.Foo.HasValue ? src.Foo.Value.ToString() : string.Empty))
      .ForMember(dest => dest.MyEnumString, opt => opt.MapFrom(src 
        => src.MyEnum.HasValue ? src.MyEnum.Value.ToString() : string.Empty));

この場合、動作は非常に単純であるため、ValueResolverは必要ありません。値がない場合は空の文字列、存在する場合は値です。 .ToString()を呼び出す代わりに、StringConvert()メソッドを置き換えることができます。ここで重要なことは、Nullableラッパーの.HasValueプロパティを利用し、.Valueプロパティが存在する場合はそれにアクセスすることです。これにより、intから変換する必要があるという複雑さが回避されますか? intに。

永続化された文字列値を列挙型に戻すには、次の質問を検討することをお勧めします。 C#で文字列を列挙型に変換するにはどうすればよいですか? 同じマッピングロジックを使用できるはずです。

これが.NET Fiddleの詳細です: https://dotnetfiddle.net/Eq0lof

16
NWard