13

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.

Uriel Arvizu
  • 1,876
  • 6
  • 37
  • 97
  • FYI, although you happen to be creating an Azure Mobile Service, it does not appear that your question (or the eventual answers) have anything to do with Azure. I recommend you remove that tag, and edit your question text to remove the reference to Azure. – John Saunders Jan 20 '15 at 19:22
  • The reason you're getting the `cannot convert from 'int?' to 'int'` error is that your function `StringConvert` takes an `int` and not an `int?`. Have you tried creating an overload of that method that takes an `int?` or possibly using a conditional (`message.ParentId.HasValue ? : MySqlFuncs.LTRIM(MySqlFuncs.StringConvert(message.ParentId.Value)) : null)` – Andrew Whitaker Jan 20 '15 at 19:32
  • I did that, but the issue is that when mapping the null from the Model class which is a int? to the DTO string property, AutoMapper needs a way to resolve this. I'm checking now ValueResolver implementations but I'm getting the error `Object reference not set to an instance of an object`. Check my updated question. – Uriel Arvizu Jan 20 '15 at 19:35
  • A simple way to do this would be `.ConstructUsing`: `Mapper.CreateMap() .ConstructUsing(i => i.HasValue ? i.Value.ToString() : null);`. Does that work for you? – Andrew Whitaker Jan 20 '15 at 19:41
  • I had to implement it differently because your code doesn't apply to those values to this: `cfg.CreateMap().ConstructUsing(i => i.SourceValue != null ? i.SourceValue.ToString() : ""); ` but this throws the error `Type 'System.String' does not have a default constructor`, from what I've read that means the code is replacing the default mapping provided by AutoMapper, so it shouldln't be done. – Uriel Arvizu Jan 20 '15 at 19:58
  • Debug and see which object is null, probably `message` in `message.ParentId`. – Gert Arnold Jan 20 '15 at 21:18
  • I´m placing a breakpoint in that line, but it is never reached, the exception is thrown on my TableController for Message on the `GetAllMessageDTO` method on the line `return Query();` – Uriel Arvizu Jan 20 '15 at 22:11

2 Answers2

19

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

d219
  • 2,707
  • 5
  • 31
  • 36
NWard
  • 2,016
  • 2
  • 20
  • 24
  • Wow that worked, but now I found another problem in my code, see I have a Nullable property in my Message class? That's an Enum, while I can send the string value for a value in my Enum and store the int value in the database, AutoMapper doesn't know how to convert that into the string representation, throwing the message `LINQ to Entities does not recognize the method 'System.String GetName(System.Type, System.Object)' method, and this method cannot be translated into a store expression.`, am I missing a mapping? I don't know if I should open another question for this issue. – Uriel Arvizu Jan 20 '15 at 23:23
  • To clarify - you want to *send* an enum value (or null) to your DTO, and *retrieve* the int-value of the enum? Or do you want to *retrieve* the stringified value? – NWard Jan 20 '15 at 23:26
  • If I have an enum with a value `Alert = 1`, then when the property Type has a value of 1, I want to send the string `Alert` on the DTO. That's what I need. – Uriel Arvizu Jan 20 '15 at 23:36
  • That's easy enough to accomplish. You then want to resolve the string `Alert` into the corresponding enum value when you go from DTO to your other class? – NWard Jan 20 '15 at 23:47
  • I'm looking to be able to resolve the string into the enum value when sending a post request, and to resolve the enum value into a string and return it in get request. – Uriel Arvizu Jan 20 '15 at 23:53
  • That's it, that did it, now I'm able to send even nullables and retrieve a nice and clean response, and with the enums the strings are resolved as int and viceversa with no problem. – Uriel Arvizu Jan 21 '15 at 00:21
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);
Niraj Trivedi
  • 2,370
  • 22
  • 24