如何使用AutoMapper将可空属性映射到DTO?

Uri*_*izu 8 c# linq entity-framework automapper

我正在开发Azure移动服务,在我的模型中,一些关系是可选的,使得表示它的属性可以为空.

例如,我的模型类中的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; }
}
Run Code Online (Sandbox Code Playgroud)

我的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; }

}
Run Code Online (Sandbox Code Playgroud)

在我的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)));
Run Code Online (Sandbox Code Playgroud)

作为参考,MySqlFuncs类具有此函数来处理从string到int和int到string的转换:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

虽然我能够插入元素,但由于以下错误,我无法获取它们:

缺少从Nullable`1到String的映射.使用Mapper.CreateMap <Nullable`1,String>创建

从这里我得到的是我在AutoMapper定义中对此错误负责的行是:

            .ForMember(messageDTO => messageDTO.ParentId, map => 
            map.MapFrom(message => message.ParentId))
Run Code Online (Sandbox Code Playgroud)

AutoMapper需要知道如何将可居住的int从ParentId属性映射到DTO.我试图使用MySqlFuncs类中的函数:

            .ForMember(messageDTO => messageDTO.ParentId, map => 
            map.MapFrom(message => MySqlFuncs.LTRIM(MySqlFuncs.StringConvert(message.ParentId))))
Run Code Online (Sandbox Code Playgroud)

但是这给出了错误:

无法从'int?'转换 'int'

如果我们认为配置必须以某种方式完成LINQ可以正确读取转换它,我如何定义映射,以便任何可空属性都被映射到字符串中作为空字符串""或只是值null到我的DTO?

编辑1

似乎要解决这个问题,我必须使用ValueResolver,我编写如下:

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

并将映射更改为:

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

但这给了我错误

你调用的对象是空的

而StackTrace就是这样的:

在AutoMapper.QueryableExtensions.Extensions.ResolveExpression(属性映射属性映射,类型currentType,表达instanceParameter)
在AutoMapper.QueryableExtensions.Extensions.CreateMemberBindings(IMappingEngine mappingEngine,TypePair typePair,的TypeMap类型映射,表达instanceParameter,IDictionary`2 typePairCount)
在AutoMapper.QueryableExtensions.Extensions .CreateMapExpression(IMappingEngine mappingEngine,TypePair typePair,Expression instanceParameter,IDictionary`2 typePairCount),
位于AutoMapper.QueryableExtensions.Extensions.Extensions的AutoMapper.QueryableExtensions.Extensions.CreateMapExpression(IMappingEngine mappingEngine,TypePair typePair,IDictionary`2 typePairCount)
.<> c__DisplayClass1`2. b__0(TypePair tp)
在System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKEY的键,Func`2 valueFactory)
在AutoMapper.Internal.DictionaryFactoryOverride.ConcurrentDictionaryImpl`2.GetOrAdd(TKEY的键,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( )

知道为什么我得到一个空引用?

注意

在调试错误时,在我的TableController类中抛出了该GetAllMessageDTO方法:

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

调试时,如果发生此错误,则不会访问映射的任何行,因为在我初步确定服务时,映射就完成了.

NWa*_*ard 17

我想你可以简单地解决这个问题.

请考虑以下示例:

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; }
}
Run Code Online (Sandbox Code Playgroud)

以下映射语句将根据需要解决它们:

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));
Run Code Online (Sandbox Code Playgroud)

在这种情况下不需要ValueResolver,因为您的行为非常简单 - 如果没有值则为空字符串,如果存在则为值.您可以替换StringConvert()方法,而不是调用.ToString().这里重要的是在Nullable包装器上使用.HasValue属性,并在存在时访问.Value属性.这避免了需要从int转换的复杂性?到int.

For converting your persisted string value back into an enum, I encourage you to explore this question: How should I convert a string to an enum in C#? You should be able to use the same mapping logic.

Here is a .NET Fiddle with more detail: https://dotnetfiddle.net/Eq0lof