To make it short here are database entities:
public class Client
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
public ICollection<ClientAddress> Addresses { get; set; }
}
public abstract class ClientAddress : ClientSubEntityBase
{
public int ClientId { get; set; }
[Required]
public virtual AddressType AddressType { get; protected set; }
[Required]
public string Address { get; set; }
}
public enum AddressType
{
Fact = 1,
Registered = 2,
}
public class ClientAddressFact : ClientAddress
{
public override AddressType AddressType { get; protected set; } = AddressType.Fact;
public string SpecificValue_Fact { get; set; }
}
public class ClientAddressRegistered : ClientAddress
{
public override AddressType AddressType { get; protected set; } = AddressType.Registered;
public string SpecificValue_Registered { get; set; }
}
These are mapped by EF Core 6 to TPH correctly.
When reading values back we get ClientAddressFact and ClientAddressRegistered correspondingly to AddressType inside Client.Addresses.
Now I need to convert these to my DTOs:
public record Client
{
public string Name { get; init; }
public IEnumerable<ClientAddress> Addresses { get; init; }
}
public abstract record ClientAddress
{
public ClientAddressType AddressType { get; init; }
public string Address { get; init; }
}
public enum ClientAddressType
{
Fact,
Registered,
}
public record ClientAddressFact : ClientAddress
{
public string SpecificValue_Fact { get; init; }
}
public record ClientAddressRegistered : ClientAddress
{
public string SpecificValue_Registered { get; init; }
}
Obviously using ProjectTo won't work since there is no way to construct a correct SELECT statement out of LINQ and create corresponding entity types. So the idea is to first ProjectTo address list to something like this:
public record ClientAddressCommon : ClientAddress
{
public string SpecificValue_Fact { get; init; }
public string SpecificValue_Registered { get; init; }
}
And then Map these to correct entity types so in the end I could get my correct Client DTO with correct ClientAddressFact and ClientAddressRegistered filled inside Addresses.
But the question is how do I do that using single ProjectTo call and only the profiles? The issue is that projection code is separate from multiple profiles projects which use it.
Here is one of profiles:
private static Models.ClientAddressType ConvertAddressType(Database.Entities.Enums.AddressType addressType) =>
addressType switch
{
Database.Entities.Enums.AddressType.Fact => Models.ClientAddressType.Fact,
Database.Entities.Enums.AddressType.Registered => Models.ClientAddressType.Registered,
_ => throw new ArgumentException("Unknown address type", nameof(addressType))
};
CreateProjection<Database.Entities.Data.Client, Models.Client>()
;
CreateProjection<Database.Entities.Data.ClientAddress, Models.ClientAddress>()
.ForMember(dst => dst.AddressType, opts => opts.MapFrom(src => ConvertAddressType(src.AddressType)))
.ConstructUsing(src => new Models.ClientAddressCommon())
;
Using var projected = _mapper.ProjectTo<Models.Client>(filtered).Single() gives me correctly filled Client but only with ClientAddressCommon addresses. So how do I convert them on a second step using full power of Map?
UPDATE_01:
According to Lucian Bargaoanu's comment I've made some adjustments:
var projected = _mapper.ProjectTo<Models.Client>(filtered).Single();
var mapped = _mapper.Map<Models.Client>(projected);
But not sure how to proceed. Here is an updated profile:
CreateMap<Models.Client, Models.Client>()
.AfterMap((src, dst) => Console.WriteLine("CLIENT: {0} -> {1}", src, dst)) // <-- this mapping seems to work
;
CreateMap<Models.ClientAddressCommon, Models.ClientAddress>()
.ConstructUsing(src => new Models.ClientAddressFact()) // simplified for testing
.AfterMap((src, dst) => Console.WriteLine("ADR: {0} -> {1}", src, dst)) // <-- this is not outputting
;
Basically I'm now mapping Client to itself just to convert what's left from projection. In this case I need to "aftermap" ClientAddressCommon to ClientAddressFact or ClientAddressRegistered based on AddressType. But looks like the mapping isn't used. What am I missing now?
So here is what I've came up with. The
ClientAddresslooks like this now:The profile looks like this:
An
enumconversion helper:And this is how conversion is made in the end:
And yes, after projection I still need to re-map again:
This all seems to work but I'm not entirely sure if it's the correct way of doing stuff.