0

I have simple task: just map one class to another. For some fields I have complex logic, depends on 2 or more fields, so, I try to use ConvertUsing (https://docs.automapper.org/en/stable/Custom-type-converters.html)

I use AutoMapper 10.0.0

My code is:

Source class:

public class DeviceStatusHistory
{
    public DeviceStatusHistory()
    {
        DateChange = DateTime.UtcNow;
    }

    public int Id { get; set; }
    public int DeviceId { get; set; }
    public virtual Device Device { get; set; }

    public int? RequestId { get; set; }
    public virtual DeviceManagementRequest Request { get; set; }

    public DeviceStatus OldStatus { get; set; }
    public DeviceStatus NewStatus { get; set; }
    public string Notes { get; set; }
    public DateTime DateChange { get; set; }
}

DTO class:

public class DeviceChangeStatusDto
{
    public int DeviceId { get; set; }
    public string CarrierName { get; set; }
    public string DeviceName { get; set; }
    public string DeviceIMEI { get; set; }
    public string OldStatus { get; set; }
    public string NewStatus { get; set; }
    public string Reason { get; set; }
    public DateTime DateChange { get; set; }
}

and Automapper class:

public class AutoMapperEfDeviceManagement : AutoMapper.Profile
{
    public AutoMapperEfDeviceManagement()
    {
        CreateMap<DeviceStatusHistory, DeviceChangeStatusDto>().ConvertUsing<DeviceChangeStatusConverter>();
    }
}

where DeviceChangeStatusConverter is defined as:

public class DeviceChangeStatusConverter : ITypeConverter<DeviceStatusHistory, DeviceChangeStatusDto>
{
    public DeviceChangeStatusDto Convert(DeviceStatusHistory source, DeviceChangeStatusDto destination, ResolutionContext context)
    {
        destination = new DeviceChangeStatusDto
        {
            CarrierName = source.Device.CarrierId.HasValue ? source.Device.Carrier.Name : null,
            DeviceId = source.DeviceId,
            DateChange = source.DateChange,
            DeviceIMEI = source.Device.IMEI,
            DeviceName = source.Device.GetFriendlyDetailedName(),
            NewStatus = CommonHelper.SplitByWords(source.NewStatus.ToString())
        };

        // some complex logic here

        return destination;
    }
}

but when I try to map it:

var list = _context.DeviceStatusHistory
.Where(a => ((int)a.NewStatus < 100) && a.DateChange.Date == date.Date)
.ProjectTo<DeviceChangeStatusDto>(_mapperConfig)
.ToList();

where _mapperConfig is:

        _mapperConfig = new MapperConfiguration(cfg =>
        {
            cfg.AddProfile<AutoMapperEfDeviceManagement>();
        });

It maps looks like it was declared simple as :

CreateMap<DeviceStatusHistory, DeviceChangeStatusDto>();

so, only the same properties are mapped, converter is not called (debugger says the same). What is wrong?

ADDED:

approach like:

        CreateMap<DeviceStatusHistory, DeviceChangeStatusDto>()
            .ConvertUsing((source, destination) =>
            {
                destination.CarrierName = source.Device.CarrierId.HasValue ? source.Device.Carrier.Name : null;

does not work too

Oleg Sh
  • 8,496
  • 17
  • 89
  • 159

1 Answers1

4

TL;DR

It is not a bug, it's a known limitation. Basically AutoMapper can't convert some custom method into SQL (or translate it into the form which will allow your ORM to translate that into SQL) so it can't use your type converter in ProjectTo.

A little bit more details:

From the documentation:

The .ProjectTo<OrderLineDTO>() will tell AutoMapper’s mapping engine to emit a select clause to the IQueryable that will inform entity framework that it only needs to query the Name column of the Item table, same as if you manually projected your IQueryable to an OrderLineDTO with a Select clause.

And from Custom Type Conversion section:

Occasionally, you need to completely replace a type conversion from a source to a destination type. In normal runtime mapping, this is accomplished via the ConvertUsing method. To perform the analog in LINQ projection, use the ConvertUsing method: cfg.CreateProjection<Source, Dest>().ConvertUsing(src => new Dest { Value = 10 });
The expression-based ConvertUsing is slightly more limited than Func-based ConvertUsing overloads as only what is allowed in an Expression and the underlying LINQ provider will work.

And Supported mapping options:

Not all mapping options can be supported, as the expression generated must be interpreted by a LINQ provider. Only what is supported by LINQ providers is supported by AutoMapper:

  • MapFrom (Expression-based)
  • ConvertUsing (Expression-based)
  • Ignore
  • NullSubstitute
  • Value transformers
  • IncludeMembers

Not supported:

  • Condition
  • SetMappingOrder
  • UseDestinationValue
  • MapFrom (Func-based)
  • Before/AfterMap
  • Custom resolvers
  • Custom type converters
  • ForPath
  • Value converters
  • Any calculated property on your domain object
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • but it does not work even for simple things, like: ` .ConvertUsing((source, destination) => { destination.CarrierName = source.Device.CarrierId.HasValue ? source.Device.Carrier.Name : null;` – Oleg Sh Aug 05 '22 at 23:20
  • 2
    @OlegSh it does not matter if it is "simple thing" or not. What matters if it is `Func`-based or [`Expression`](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/)-based. The overload you are using accepts `Func<...>` (check the signature), therefore it can't be translated into SQL. – Guru Stron Aug 05 '22 at 23:24
  • This expression can be translated into SQL, it works in many other similar cases LinqToEntities – Oleg Sh Aug 05 '22 at 23:34
  • 1
    @OlegSh the one in the question is not rewritable as is due to the `// some complex logic here` and `GetFriendlyDetailedName()`, `CommonHelper.SplitByWords(...)` calls cause your ORM will not know how to translate that into SQL. As for the "simple one" - without full setup it's hard to say if it can be rewritten. – Guru Stron Aug 05 '22 at 23:34
  • 2
    @OlegSh yes such expression can be translated, but the overload used here - `.ConvertUsing((source, destination) => { destination.CarrierName = source.Device.CarrierId.HasValue ? source.Device.Carrier.Name : null;` is not an expression, it is a `Func` (check the signature). And `Func` are not translatable (at least without some dirty magic). – Guru Stron Aug 05 '22 at 23:35
  • Thank you. But why Automapper works so rough? Without throwing exception, just ignore part of code? – Oleg Sh Aug 05 '22 at 23:42
  • @OlegSh my pleasure! TBH - no idea. I would say that this is a question better addressed at their github. – Guru Stron Aug 05 '22 at 23:48