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