简体   繁体   English

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

[英]How to map a nullable property to a DTO using AutoMapper?

I'm developing an Azure Mobile Service, in my model some of the relationships are optional, making the properties that represent it to be nullable.我正在开发 Azure 移动服务,在我的模型中,一些关系是可选的,这使得表示它的属性可以为空。

For example, my Message entity in my model class is like this:例如,我的模型类中的 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; }
}

And my DTO for it looks like this:我的 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; }

}

In my AutoMapper configuration I have this:在我的 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)));

For reference, the MySqlFuncs class has this functions to handle the conversion from string to int and int to string:作为参考,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;
    }
}

While I'm able to insert elements, I'm not able to get them due to the following error:虽然我可以插入元素,但由于以下错误,我无法获取它们:

Missing map from Nullable`1 to String.缺少从 Nullable`1 到 String 的映射。 Create using Mapper.CreateMap<Nullable`1, String>使用 Mapper.CreateMap<Nullable`1, String> 创建

From this I get that the line resonsible for this error in my AutoMapper definition is:从这里我得到,在我的 AutoMapper 定义中对这个错误负责的行是:

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

AutoMapper needs to know how to map the nullable int from the ParentId property into the DTO. AutoMapper 需要知道如何将 ParentId 属性中的可为 null 的 int 映射到 DTO。 I tried to use the functions from MySqlFuncs class:我尝试使用 MySqlFuncs 类中的函数:

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

But that gives shows the error:但这给出了错误:

cannot convert from 'int?'无法从“int”转换? to 'int' 'int'

If we consider the configuration must be done in a way LINQ can read an convert it correctly, how can I define the mapping so any nullable properties get mapped into string as empty string "" or just the value null into my DTO?如果我们认为必须以 LINQ 可以正确读取和转换它的方式完成配置,那么我如何定义映射以便将任何可为空的属性映射到字符串中作为空字符串 "" 或仅将 null 值映射到我的 DTO 中?

EDIT 1编辑 1

It seems for this to be resolved I have to use a ValueResolver, which I coded as follows:似乎要解决这个问题,我必须使用 ValueResolver,我将其编码如下:

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

And changed the mapping to this:并将映射更改为:

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

But this is giving me the error但这给了我错误

Object reference not set to an instance of an object你调用的对象是空的

And the StackTrace is this: StackTrace 是这样的:

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

Any idea why I'm getting a null reference?知道为什么我得到一个空引用吗?

Note笔记

When debugging the error is thrown in my TableController class in the GetAllMessageDTO method:调试时,在GetAllMessageDTO方法中的GetAllMessageDTO类中抛出错误:

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

When debugging none of the lines of the mapping are accessed when this error happens, since the mapping is done when the service is initialized as far as I can see.在调试时,发生此错误时不会访问映射的任何行,因为就我所见,在服务初始化时映射已完成。

I think you may be able to solve this problem simply.我想你也许可以简单地解决这个问题。

Consider the following example:考虑以下示例:

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

The following mapping statement will resolve them as desired:以下映射语句将根据需要解析它们:

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));

There is no need for a ValueResolver in this case, since your behavior is very simple - empty string if there's no value, or the value if it exists.在这种情况下不需要ValueResolver ,因为您的行为非常简单 - 如果没有值,则为空字符串,如果存在则为值。 Instead of calling .ToString() , you can substitute your StringConvert() method.您可以替换StringConvert()方法,而不是调用.ToString() The important thing here is to make use of the .HasValue property on the Nullable<T> wrapper, and to access to .Value property when it exists.这里重要的是利用Nullable<T>包装器上的.HasValue属性,并在它存在时访问.Value属性。 This avoids the complication of needing to convert from int?这避免了需要从 int 转换的复杂性? to int.到国际。

For converting your persisted string value back into an enum, I encourage you to explore this question: Convert a string to an enum in C# You should be able to use the same mapping logic.为了将持久化的字符串值转换回枚举,我鼓励您探索这个问题: 在 C# 中将字符串转换为枚举您应该能够使用相同的映射逻辑。

Here is a .NET Fiddle with more detail: https://dotnetfiddle.net/Eq0lof这是一个更详细的 .NET Fiddle: https : //dotnetfiddle.net/Eq0lof

You can use NullSubstitute to provide alternative value to your destination property if the source property value is null如果源属性值为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);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM