首页 > 解决方案 > 当属性具有相同代码时保持 AutoMapper ProjectTo() DRY

问题描述

我想使用 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}"));

显然,这复制了 Name 属性和 ClaimType.ToString 方法的代码。是否有任何既定的模式可以解决这个问题并保持代码干燥?我们的模型中有很多自定义属性和 ToString 覆盖。我发现我可以对 Name 属性执行以下操作...首先,在 Claim 类中:

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 的性能奖励,只需要拉动实际需要的列。

标签: c#automapperautomapper-7

解决方案


@LucianBargaoanu 为一个伟大的、有效的解决方案提供了正确的钥匙。AutoMapper.EF6提供了一些简单的包装器来做到这一点;他们包装的实际上是真正工作的DelegateDecompiler包。最终解决方案如下:

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

其次,将ComputedAttributeEFCore 添加到您希望 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();

我验证了上面的代码运行良好,即使使用ToString标记为Computed. 我检查了 SQL Server Profiler 以确保查询只拉回必要的列,谢天谢地,确实如此。

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


推荐阅读