我可以使用AutoMapper映射目标继承而无需继承源吗?

时间:2016-12-23 15:58:03

标签: c# entity-framework inheritance automapper entity-framework-core

我搜索过高和低,无法弄清楚是否有办法使用AutoMapper(5.2)来映射以下EF Core(1.1)场景。源类不使用继承,因为尚不支持Table-per-Type继承,而且我正在对现有数据库进行操作。

EF Core POCOS:

public class Farmer
{
    [Key]
    public int FarmerId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    //changed entities
    public virtual ICollection<Chicken> ChangedChickens { get; set; }
    public virtual ICollection<Cow> ChangedCows { get; set; }
}

public class Chicken
{
    [Key]
    public int ChickenId { get; set; }
    public bool IsRooster { get; set; }
    //common change props
    public int LastChangeByFarmerId { get; set; }
    public DateTime LastChangeTimestamp { get; set; }
    [ForeignKey("LastChangeBy")]
    public virtual Farmer LastChangeFarmer { get; set; }
}

public class Cow
{
    [Key]
    public int CowId { get; set; }
    public string Name { get; set; }
    //common change props
    public int LastChangeByFarmerId { get; set; }
    public DateTime LastChangeTimestamp { get; set; }
    [ForeignKey("LastChangeBy")]
    public virtual Farmer LastChangeFarmer { get; set; }        
}

我想在数据传输类中使用基类来更改属性:

DTO的

public abstract class FarmerChangeDtoBase
{
    public int LastChangeBy { get; set; }
    public DateTime LastChangeTime { get; set; }
    public string ChangingFarmerFirstName { get; set; }
    public string ChangingFarmerLastName { get; set; }
    public string ChangingFarmerFullName => $"{ChangingFarmerFirstName} {ChangingFarmerLastName}";
}

public class ChickenDto : FarmerChangeDtoBase
{
    public int ChickenId { get; set; }
    public bool IsRooster { get; set; }
}

public class CowDto : FarmerChangeDtoBase
{
    public int CowId { get; set; }
    public string Name { get; set; }
}

我编写了一个扩展方法,使用反射获取LastChangeByLastChangeTime值,这可能不太理想,但我无法弄清楚如何获取农夫名称的嵌套属性。这是扩展方法:

public static IMappingExpression<TSource, TDest> MapChangeFarmer<TSource, TDest>(
    this IMappingExpression<TSource, TDest> mappingExpression)
    where TDest : FarmerChangeDtoBase
{
    return mappingExpression.ForMember(d => d.LastChangeBy, 
            opt => opt.MapFrom(s => 
                (int) s.GetType().GetProperty("LastChangeByFarmerId").GetValue(s)))
         .ForMember(d => d.LastChangeTime, 
            opt => opt.MapFrom(s => 
                (DateTime) s.GetType().GetProperty("LastChangeTimestamp").GetValue(s)));
    //what/how can I map the name properties???
}

有没有办法可以在扩展方法中映射嵌套属性LastChangeFarmer.FirstNameLastChangeFarmer.LastName,而不必为从FarmerChangeDtoBase继承的每个DTO写出来?

1 个答案:

答案 0 :(得分:1)

不要尝试在没有源继承的情况下映射目标继承,而是解决问题的根源 - 缺少源继承。

EF(Core)继承是围绕单个多态抽象实体集建模实体继承并将其映射到单个表(TPH)或多个表(TPT或TPC)的概念。但是,EF(Core)并不禁止在不使用EF继承的情况下使用POCO类继承 - 使用包含公共实体属性的基类绝对没有问题。

例如,样本模型为ChickenCow实体生成以下表格:

migrationBuilder.CreateTable(
    name: "Chicken",
    columns: table => new
    {
        ChickenId = table.Column<int>(nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        IsRooster = table.Column<bool>(nullable: false),
        LastChangeByFarmerId = table.Column<int>(nullable: false),
        LastChangeTimestamp = table.Column<DateTime>(nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Chicken", x => x.ChickenId);
        table.ForeignKey(
            name: "FK_Chicken_Farmer_LastChangeByFarmerId",
            column: x => x.LastChangeByFarmerId,
            principalTable: "Farmer",
            principalColumn: "FarmerId",
            onDelete: ReferentialAction.Cascade);
    });

migrationBuilder.CreateTable(
    name: "Cow",
    columns: table => new
    {
        CowId = table.Column<int>(nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        LastChangeByFarmerId = table.Column<int>(nullable: false),
        LastChangeTimestamp = table.Column<DateTime>(nullable: false),
        Name = table.Column<string>(nullable: true)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Cow", x => x.CowId);
        table.ForeignKey(
            name: "FK_Cow_Farmer_LastChangeByFarmerId",
            column: x => x.LastChangeByFarmerId,
            principalTable: "Farmer",
            principalColumn: "FarmerId",
            onDelete: ReferentialAction.Cascade);
    });

如果我们提取具有公共属性的基类并让ChickenCow继承它:

public abstract class FarmerChangeBase
{
    public int LastChangeByFarmerId { get; set; }
    public DateTime LastChangeTimestamp { get; set; }
    [ForeignKey("LastChangeByFarmerId")]
    public virtual Farmer LastChangeFarmer { get; set; }
}

public class Chicken : FarmerChangeBase
{
    [Key]
    public int ChickenId { get; set; }
    public bool IsRooster { get; set; }
}

public class Cow : FarmerChangeBase
{
    [Key]
    public int CowId { get; set; }
    public string Name { get; set; }
}

生成的迁移(因此映射)与前一个迁移完全相同而不使用继承。

一旦这样做,由于适当的泛型类型约束,映射方法很简单:

public static IMappingExpression<TSource, TDest> MapChangeFarmer<TSource, TDest>(
    this IMappingExpression<TSource, TDest> mappingExpression)
    where TSource : FarmerChangeBase
    where TDest : FarmerChangeDtoBase
{
    return mappingExpression
        .ForMember(d => d.LastChangeBy, opt => opt.MapFrom(s => s.LastChangeByFarmerId))
        .ForMember(d => d.ChangingFarmerFirstName, opt => opt.MapFrom(s => s.LastChangeFarmer.FirstName))
        .ForMember(d => d.ChangingFarmerLastName, opt => opt.MapFrom(s => s.LastChangeFarmer.LastName))
        .ForMember(d => d.LastChangeTime, opt => opt.MapFrom(s => s.LastChangeTimestamp));
}

PS 要回答原始问题,如果由于某种原因您无法使用源继承,以下自定义扩展方法会从包含属性路径的字符串动态构建所需的表达式:

public static void MapFromPath<TSource, TDestination, TMember>(this IMemberConfigurationExpression<TSource, TDestination, TMember> opt, string memberPath)
{
    var source = Expression.Parameter(typeof(TSource), "s");
    var member = memberPath.Split('.').Aggregate(
        (Expression)source, Expression.PropertyOrField);
    var selector = Expression.Lambda<Func<TSource, TMember>>(member, source);
    opt.MapFrom(selector);
}

可以这样使用:

public static IMappingExpression<TSource, TDest> MapChangeFarmer<TSource, TDest>(
    this IMappingExpression<TSource, TDest> mappingExpression)
    where TDest : FarmerChangeDtoBase
{
    return mappingExpression
        .ForMember(d => d.LastChangeBy, opt => opt.MapFromPath("LastChangeByFarmerId"))
        .ForMember(d => d.ChangingFarmerFirstName, opt => opt.MapFromPath("LastChangeFarmer.FirstName"))
        .ForMember(d => d.ChangingFarmerLastName, opt => opt.MapFromPath("LastChangeFarmer.LastName"))
        .ForMember(d => d.LastChangeTime, opt => opt.MapFromPath("LastChangeTimestamp"));
}