如何使用 Automapper 将 ProjectTo 与 Map 结合起来?
How to combine ProjectTo with Map using Automapper?
简而言之,这里是数据库实体:
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; }
}
这些由 EF Core 6 正确映射到 TPH。
当读回值时,我们得到 ClientAddressFact
和 ClientAddressRegistered
对应于 Client.Addresses
.
中的 AddressType
现在我需要将它们转换为我的 DTO:
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; }
}
显然使用 ProjectTo
是行不通的,因为无法从 LINQ 构造正确的 SELECT
语句并创建相应的实体类型。所以想法是首先 ProjectTo
地址列表到这样的东西:
public record ClientAddressCommon : ClientAddress
{
public string SpecificValue_Fact { get; init; }
public string SpecificValue_Registered { get; init; }
}
然后 Map
这些来纠正实体类型所以最后我可以得到我正确的 Client
DTO 并在 [=27] 中填充正确的 ClientAddressFact
和 ClientAddressRegistered
=].
但问题是如何使用单个 ProjectTo
调用并且仅使用配置文件来做到这一点?问题是投影代码与使用它的多个配置文件项目是分开的。
这是个人资料之一:
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())
;
使用 var projected = _mapper.ProjectTo<Models.Client>(filtered).Single()
给我正确填写的 Client
但只有 ClientAddressCommon
地址。那么我如何在第二步使用 Map
的全部功能转换它们?
UPDATE_01:
根据 Lucian Bargaoanu 的 我做了一些调整:
var projected = _mapper.ProjectTo<Models.Client>(filtered).Single();
var mapped = _mapper.Map<Models.Client>(projected);
但不确定如何进行。这是更新后的个人资料:
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
;
基本上,我现在将 Client
映射到自身,只是为了转换投影剩下的部分。在这种情况下,我需要根据 AddressType
“aftermap”ClientAddressCommon
到 ClientAddressFact
或 ClientAddressRegistered
。但看起来没有使用映射。我现在缺少什么?
这就是我的想法。
ClientAddress
现在看起来像这样:
public record ClientAddress
{
public ClientAddressType AddressType { get; init; } // <-- used to differentiate between address types
public string Address { get; init; }
public virtual string SpecificValue_Fact { get; init; } // <-- specific for ClientAddressFact
public virtual string SpecificValue_Registered { get; init; } // <-- specific for ClientAddressRegistered
}
public record ClientAddressFact : ClientAddress
{
}
public record ClientAddressRegistered : ClientAddress
{
}
public enum ClientAddressType
{
Fact,
Registered,
}
配置文件如下所示:
CreateProjection<Database.Entities.Data.Client, Models.Client>() // <-- project from DB to DTO for the main entity
;
CreateProjection<Database.Entities.Data.ClientAddress, Models.ClientAddress>() // <-- project from TPH entity type to a type which holds all the common properties for all address types
.ForMember(dst => dst.AddressType, opts => opts.MapFrom(src => ConvertAddressType(src.AddressType)))
;
CreateMap<Models.Client, Models.Client>() // <-- this is needed so AM knows that we need to map a type to itself
;
CreateMap<Models.ClientAddress, Models.ClientAddress>() // <-- changed destination type to itself, since it is the only one available at that moment after projection
.ConvertUsing<ClientAddressTypeConverter>()
;
CreateMap<Models.ClientAddress, Models.ClientAddressFact>()
;
CreateMap<Models.ClientAddress, Models.ClientAddressRegistered>()
;
一个enum
转换助手:
public 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("Неизвестный address type", nameof(addressType))
};
最后是这样转换的:
private class ClientAddressTypeConverter : ITypeConverter<Models.ClientAddress, Models.ClientAddress>
{
public Models.ClientAddress Convert(Models.ClientAddress source, Models.ClientAddress destination, ResolutionContext context) =>
source.AddressType switch
{
Models.ClientAddressType.Fact => context.Mapper.Map<Models.ClientAddressFact>(source),
Models.ClientAddressType.Registered => context.Mapper.Map<Models.ClientAddressRegistered>(source),
_ => throw new ArgumentException("Unknown address type")
};
}
是的,投影后我还需要再次re-map:
var projected = _mapper.ProjectTo<Models.Client>(filtered).Single();
var mapped = _mapper.Map<Models.Client>(projected); // map from itself to itself to convert ClientAddress to corresponding sub-types
这一切似乎都有效,但我不完全确定这是否是正确的做事方式。
简而言之,这里是数据库实体:
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; }
}
这些由 EF Core 6 正确映射到 TPH。
当读回值时,我们得到 ClientAddressFact
和 ClientAddressRegistered
对应于 Client.Addresses
.
AddressType
现在我需要将它们转换为我的 DTO:
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; }
}
显然使用 ProjectTo
是行不通的,因为无法从 LINQ 构造正确的 SELECT
语句并创建相应的实体类型。所以想法是首先 ProjectTo
地址列表到这样的东西:
public record ClientAddressCommon : ClientAddress
{
public string SpecificValue_Fact { get; init; }
public string SpecificValue_Registered { get; init; }
}
然后 Map
这些来纠正实体类型所以最后我可以得到我正确的 Client
DTO 并在 [=27] 中填充正确的 ClientAddressFact
和 ClientAddressRegistered
=].
但问题是如何使用单个 ProjectTo
调用并且仅使用配置文件来做到这一点?问题是投影代码与使用它的多个配置文件项目是分开的。
这是个人资料之一:
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())
;
使用 var projected = _mapper.ProjectTo<Models.Client>(filtered).Single()
给我正确填写的 Client
但只有 ClientAddressCommon
地址。那么我如何在第二步使用 Map
的全部功能转换它们?
UPDATE_01:
根据 Lucian Bargaoanu 的
var projected = _mapper.ProjectTo<Models.Client>(filtered).Single();
var mapped = _mapper.Map<Models.Client>(projected);
但不确定如何进行。这是更新后的个人资料:
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
;
基本上,我现在将 Client
映射到自身,只是为了转换投影剩下的部分。在这种情况下,我需要根据 AddressType
“aftermap”ClientAddressCommon
到 ClientAddressFact
或 ClientAddressRegistered
。但看起来没有使用映射。我现在缺少什么?
这就是我的想法。
ClientAddress
现在看起来像这样:
public record ClientAddress
{
public ClientAddressType AddressType { get; init; } // <-- used to differentiate between address types
public string Address { get; init; }
public virtual string SpecificValue_Fact { get; init; } // <-- specific for ClientAddressFact
public virtual string SpecificValue_Registered { get; init; } // <-- specific for ClientAddressRegistered
}
public record ClientAddressFact : ClientAddress
{
}
public record ClientAddressRegistered : ClientAddress
{
}
public enum ClientAddressType
{
Fact,
Registered,
}
配置文件如下所示:
CreateProjection<Database.Entities.Data.Client, Models.Client>() // <-- project from DB to DTO for the main entity
;
CreateProjection<Database.Entities.Data.ClientAddress, Models.ClientAddress>() // <-- project from TPH entity type to a type which holds all the common properties for all address types
.ForMember(dst => dst.AddressType, opts => opts.MapFrom(src => ConvertAddressType(src.AddressType)))
;
CreateMap<Models.Client, Models.Client>() // <-- this is needed so AM knows that we need to map a type to itself
;
CreateMap<Models.ClientAddress, Models.ClientAddress>() // <-- changed destination type to itself, since it is the only one available at that moment after projection
.ConvertUsing<ClientAddressTypeConverter>()
;
CreateMap<Models.ClientAddress, Models.ClientAddressFact>()
;
CreateMap<Models.ClientAddress, Models.ClientAddressRegistered>()
;
一个enum
转换助手:
public 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("Неизвестный address type", nameof(addressType))
};
最后是这样转换的:
private class ClientAddressTypeConverter : ITypeConverter<Models.ClientAddress, Models.ClientAddress>
{
public Models.ClientAddress Convert(Models.ClientAddress source, Models.ClientAddress destination, ResolutionContext context) =>
source.AddressType switch
{
Models.ClientAddressType.Fact => context.Mapper.Map<Models.ClientAddressFact>(source),
Models.ClientAddressType.Registered => context.Mapper.Map<Models.ClientAddressRegistered>(source),
_ => throw new ArgumentException("Unknown address type")
};
}
是的,投影后我还需要再次re-map:
var projected = _mapper.ProjectTo<Models.Client>(filtered).Single();
var mapped = _mapper.Map<Models.Client>(projected); // map from itself to itself to convert ClientAddress to corresponding sub-types
这一切似乎都有效,但我不完全确定这是否是正确的做事方式。