Can I use AutoMapper to map destination inheritance without source inheritance?

automapper c# entity-framework entity-framework-core inheritance

Question

I have searched high and low and cannot figure out if there's a way to use AutoMapper (5.2) to map the following EF Core (1.1) scenario. The source classes do not use inheritance as Table-per-Type inheritance is not yet supported and I'm working against an existing database.

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

I would like to use a base class for the change properties in my data transfer classes:

DTOs

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

I wrote an extension method to get LastChangeBy and LastChangeTime values using reflection, which may not be ideal, but I cannot figure out how to get the nested properties for the farmer name. Here's the extension method:

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

Is there any way I can map the nested properties LastChangeFarmer.FirstName and LastChangeFarmer.LastName in the extension method, rather than having to write it out for every single DTO inheriting from FarmerChangeDtoBase?

1
2
12/23/2016 3:58:03 PM

Accepted Answer

Rather than trying to map destination inheritance without source inheritance, let solve the origin of the issue - the lack of source inheritance.

EF (Core) inheritance is a concept of modeling an entity inheritance around a single polymorphic abstract entity set and mapping it to a single table (TPH) or multiple tables (TPT or TPC). However EF (Core) does not prohibit using POCO class inheritance without using EF inheritance - there is absolutely no problem of using base classes containing a common entity properties like in your case.

For instance, the sample model produces the following tables for Chicken and Cow entities:

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

If we extract a base class with the common properties and let the Chicken and Cow inherit from it:

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

the resulting migration (hence the mapping) is exactly the same as the previous one without using inheritance.

Once you do that, the mapping method is simple due to the appropriate generic type constraints:

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

P.S. To answer the original question, in case by some reason you can't use source inheritance, the following custom extension method builds dynamically the required expression from a string containing a property path:

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

It can be used this way:

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"));
}
1
12/24/2016 7:41:37 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow