How to map a nullable property to a DTO using AutoMapper?

24,122

Solution 1

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. Instead of calling .ToString(), you can substitute your StringConvert() method. 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. This avoids the complication of needing to convert from 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.

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

Solution 2

You can use NullSubstitute to provide alternative value to your destination property if the source property value is null

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);
Share:
24,122
Uriel Arvizu
Author by

Uriel Arvizu

Software developer with experience in different languages. I prefer stuff like C++ or Java. I like an open source approach, although I still prefer Windows since videogames are more available there. I don't get along with MacOX for several reasons. I'm interested in growing in mobile and game development, barely starting in the later. I aim to contribute to the community in a greater way in the future, I still feel I lack some experience but one never stops growing.

Updated on July 05, 2022

Comments

  • Uriel Arvizu
    Uriel Arvizu almost 2 years

    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.

    For example, my Message entity in my model class is like this:

    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:

    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:

            /*
             * 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:

    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. Create using Mapper.CreateMap<Nullable`1, String>

    From this I get that the line resonsible for this error in my AutoMapper definition is:

                .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. I tried to use the functions from MySqlFuncs class:

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

    But that gives shows the error:

    cannot convert from 'int?' to '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?

    EDIT 1

    It seems for this to be resolved I have to use a ValueResolver, which I coded as follows:

    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:

    at 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)
    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 key, Func`2 valueFactory)
    at AutoMapper.Internal.DictionaryFactoryOverride.ConcurrentDictionaryImpl`2.GetOrAdd(TKey key, Func`2 valueFactory)
    at AutoMapper.QueryableExtensions.Extensions.CreateMapExpression[TSource,TDestination](IMappingEngine mappingEngine)
    at AutoMapper.QueryableExtensions.ProjectionExpression`1.ToTResult
    at Microsoft.WindowsAzure.Mobile.Service.MappedEntityDomainManager`2.Query() at 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:

    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.