当属性具有相同代码时保持 AutoMapper ProjectTo() 干燥

Keeping AutoMapper ProjectTo() DRY When Properties Have the Same Code

我想使用 AutoMapper 的可查询扩展 (.ProjectTo),但如果不复制数据库对象的许多属性中的代码并将其复制到投影中,我不知道该怎么做.下面是一个既覆盖 ToString 又具有自定义 属性 的示例:

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; }
}

现在假设我有上述两个数据库模型和视图模型,我想在查询中将 Claim 投影到 ClaimViewModel。我知道如何做到这一点的唯一方法是这样的:

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}"));

显然,这重复了名称 属性 和 ClaimType.ToString 方法的代码。是否有解决此问题并保持代码 DRY 的既定模式?我们的模型中有很多自定义属性和 ToString 覆盖。我发现我可以为姓名 属性 执行以下操作...首先,在声明 class 中:

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

然后在映射中执行此操作:

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

这对于非常简单的情况似乎没问题,但它不适用于具有外键引用的情况,例如 ClaimType.ToString 方法。在这种情况下,我可以将表达式放在 Claim 中并在 Claim 映射中引用它,但是当我需要将 ClaimType 投影到 ClaimTypeViewModel 时,我必须复制代码。或者,如果有另一个数据库模型引用了 ClaimType 模型,我也会遇到同样的问题。

如果对答案很重要,ORM 是 EF Core @ 2.1.3。

编辑: 我在写这个问题时没有意识到,但是上面的映射在没有 ForMember 配置的情况下也可以工作,但我在 SQL Profiler 中注意到查询将拉回 Claim 和 ClaimType 模型中的每一列,而不仅仅是所需的列。这很好,但我真的需要 ProjectTo 的性能奖励,只提取实际需要的列。

@LucianBargaoanu 提供了一个伟大的、有效的解决方案的正确关键。执行实际工作的 AutoMapper.EF6 provides some simple wrappers to do this; what they wrap is actually the DelegateDecompiler 包。最终解决方案如下:

首先,通过将 NuGet 包添加到您的项目来引用 DelegateDecompiler。

其次,将 ComputedAttribute 添加到您希望 EFCore 能够正确处理变成 SQL 的模型上的任何计算属性或函数,例如:

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

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

最后,任何你想调用ProjectTo的地方,也附加Decompile()(或DecompileAsync())。例如

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

我验证了上面的代码工作得很好,即使使用标记为 Computed 的重写 ToString 方法也是如此。我检查了 SQL Server Profiler 以确保查询只拉回必要的列,谢天谢地,它是。

作为参考,AutoMapper.EF6 库实际上只有一个 single source file,它可以很容易地复制并粘贴到您自己的源代码中,目标是 EF Core 而不是 EF 6。