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

automapper c# entity-framework linq

Question

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.

1
9
1/20/2015 10:54:15 PM

Accepted Answer

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

17
5/23/2017 11:45:57 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow