1

I want to use AutoMapper's queryable extensions (.ProjectTo), but I can't figure out how to do it without copying the code in many of my properties on the database objects and duplicating it in the projection. Here's an example that both overrides ToString and has a custom property:

class Claim {
  public int Id { get; set; }
  public int TypeId { get; set; }
  public ClaimType Type { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string Name
    => $"{FirstName} {LastName}";
}

class ClaimType {
  public int Id { get; set; }
  public string Value { get; set; }
  public string Abbrev { get; set; }
  public override string ToString()
    => Value + (Abbrev != null ? $" ({Abbrev})" : "");
}

class ClaimViewModel {
  public int Id { get; set; }
  public string Type { get; set; }
  public string Name { get; set; }
}

Now let's say I have the above two database models and view model I want to project the Claim to a ClaimViewModel in a query. The only way I know how to do this is like so:

CreateMap<Claim, ClaimViewModel>()
  .ForMember(c => c.ClaimType, x => x.MapFrom(c => c.ClaimType.Value + (c.ClaimType.Abbrev != null ? $" ({c.ClaimType.Abbrev})" : "")))
  .ForMember(c => c.Name, x => x.MapFrom(c => $"{c.FirstName} {c.LastName}"));

Obviously this duplicates the code for the Name property and the ClaimType.ToString method. Are there any established patterns for solving this problem and keeping the code DRY? We have lots of custom properties and ToString overrides in our models. I have discovered that I can do the following for the Name property... first, in the Claim class:

public static readonly Expression<Func<Claim, string>> NameExpr = (c) => $"{c.FirstName} {c.LastName}";
public string Name => NameExpr(this);

And then in the mapping do this:

.ForMember(c => c.Name, x => x.MapFrom(Claim.NameExpr));

This seems fine for that very simple case, but it doesn't work for situations with foreign key references, like the ClaimType.ToString method. In that case I could put the expression in Claim and reference it in the Claim mapping, but the moment I needed to project a ClaimType to a ClaimTypeViewModel, I'd have to duplicate the code. Or if there was another database model that referenced the ClaimType model, I'd have the same problem.

If it matters to the answer, the ORM is EF Core @ 2.1.3.

EDIT: I didn't realized it when I wrote this question, but the above mapping will work without the ForMember configuration, but I noticed in SQL Profiler that the query will pull back every column in the Claim and ClaimType models rather than just the columns needed. That's nice, but I really need the performance bonus of ProjectTo only pulling the columns actually needed.

pbristow
  • 1,997
  • 4
  • 26
  • 46
  • See [this](https://github.com/AutoMapper/AutoMapper.EF6). But EF Core is different, has client evaluation, so I don't know how useful is that. Anyway, EF6 is much better at projections than EF Core. – Lucian Bargaoanu Oct 02 '18 at 04:49
  • @LucianBargaoanu Thanks. Yes, feature parity is a ways off, unfortunately. Though I heard that v4.8 will be .NET Frameworks' last major release, so hopefully that will push .NET Core to catch up. I can't just switch from .NET Core to .NET Fwk so I was thinking that link wasn't very helpful, but looking at the source code, it looks like the part of that I need is actually coming from https://github.com/hazzik/DelegateDecompiler. So thanks; I'll give that a try. – pbristow Oct 02 '18 at 11:13

1 Answers1

0

@LucianBargaoanu provided the right key to a great, working solution. The AutoMapper.EF6 provides some simple wrappers to do this; what they wrap is actually the DelegateDecompiler package that does the real work. The final solution is as follows:

First, reference the DelegateDecompiler by adding the NuGet package to your project(s).

Second, add the ComputedAttribute to any computed properties or functions on the models that you expect EFCore to be able to handle turning into SQL properly, e.g.:

class Claim {
  [Computed]
  public string Name
    => $"{FirstName} {LastName}";
}

class ClaimType {
  [Computed]
  public override string ToString()
    => Value + (Abbrev != null ? $" ({Abbrev})" : "");
}

Finally, any place that you want to call ProjectTo, also append Decompile() (or DecompileAsync()) to it. e.g.

var myClaimViewModels = Db.Claims.ProjectTo<ClaimViewModel>(Mapper.ConfigurationProvider).Decompile().ToList();

I verified the above code works nicely, even using the overridden ToString method marked as Computed. I checked SQL Server Profiler to make ensure the query is only pulling back the necessary columns, and thankfully, it is.

For reference, the AutoMapper.EF6 library really only has a single source file, and it could easily be copied & pasted into your own source, targeting EF Core instead of EF 6.

pbristow
  • 1,997
  • 4
  • 26
  • 46