如何使用自动映射程序将可为空的属性映射到 DTO



我正在开发一个 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; }
}

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

在我的自动映射器配置中,我有这个:

        /*
         * 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;
    }
}

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

缺少从可为空的 1 到字符串的映射。创建使用 Mapper.CreateMap<Nullable'1, String>

由此我得到我的自动映射器定义中此错误的共鸣行是:

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

AutoMapper 需要知道如何将 ParentId 属性中的可为空的 int 映射到 DTO。我尝试使用MySqlFuncs类中的函数:

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

但这给出了错误:

无法从"int?"转换为"int"

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

编辑 1

似乎要解决这个问题,我必须使用一个价值解析器,我编码如下:

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 属性映射, 类型 当前类型, 表达式实例参数)
at AutoMapper.QueryableExtensions.Extensions.CreateMemberBindings(IMappingEngine mappingEngine, TypePair typePair, TypeMap typeMap, Expression instanceParameter, IDictionary'2 typePairCount)
at AutoMapper.QueryableExtensions.Extensions.CreateMapExpression(IMappingEngine mappingEngine, TypePair typePair, Expression instanceParameter, IDictionary'2 typePairCount)
at AutoMapper.QueryableExtensions.Extensions.CreateMapExpression(IMappingEngine mappingEngine, TypePair typePair, IDictionary'2 typePairCount)
at AutoMapper.QueryableExtensions.Extensions.<>c__DisplayClass1'2.b__0(TypePair TP)
at System.Collections.Concurrent.ConcurrentDictionary'2.GetOrAdd(TKey 键,Func'2 值工厂)
at AutoMapper.Internal.DictionaryFactoryOverride.ConcurrentDictionaryImpl'2.GetOrAdd(TKey 键,Func'2 值工厂)
at AutoMapper.QueryableExtensions.Extensions.CreateMapExpression[TSource,TDestination](IMappingEngine 映射引擎)
at AutoMapper.QueryableExtensions.ProjectionExpression'1.ToTResult
在 Microsoft.WindowsAzure.Mobile.Service.MappedEntityDomainManager'2.Query() at Microsoft.WindowsAzure.Mobile.Service.TableController'1.Query()

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

注意

调试时,错误会在 GetAllMessageDTO 方法中的 TableController 类中引发:

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

发生此错误时不会访问映射的任何行,因为据我所知,映射是在初始化服务时完成的。

我想

你可以简单地解决这个问题。

请考虑以下示例:

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,因为您的行为非常简单 - 如果没有值,则为空字符串,如果存在,则为值。您可以替换StringConvert()方法,而不是调用.ToString()。这里重要的是利用Nullable<T>包装器上的.HasValue属性,并在.Value属性存在时访问它。这避免了需要从 int?到国际。

为了将持久字符串值转换回枚举,我鼓励您探索这个问题: 在 C# 中将字符串转换为枚举 您应该能够使用相同的映射逻辑。

下面是一个 .NET 小提琴,其中包含更多详细信息:https://dotnetfiddle.net/Eq0lof

如果源媒体资源值为 null,则可以使用 NullSubstitute 为目标媒体资源提供替代值

var config = new MapperConfiguration(cfg => cfg.CreateMap<Person, PersonDto>()
    .ForMember(destination => destination.Name, opt => opt.NullSubstitute(string.Empty)));
var source = new Person { Name = null };
var mapper = config.CreateMapper();
var dest = mapper.Map<Person , PersonDto>(source);

最新更新