我正在开发一个基于 Jason Taylor 的 Clean Architecture 模板的 .Net 8.0 项目。在此项目中,始终使用 AutoMapper 在域实体和数据传输对象 (DTO) 之间进行映射。在应用程序的单元测试项目中,定义了一些测试来验证定义的映射。我最近介绍了一些 DTO,它们利用泛型来设置查找对象上
Id
属性的数据类型。引入泛型(及其关联的映射)后,我的 AutoMapper 测试开始失败,但仅限于泛型并且仅在使用 AddMaps
重载的情况下。如果使用 MapperConfiguration(config => c.CreateMap(src,dest)
重载显式添加地图,则地图将按预期工作。
相信这是一个 AutoMapper 错误,我在 AutoMapper 存储库中打开了一个问题,但我被引导到这里。
我创建了一个 .NET Fiddle 在浏览器中演示了该问题,完整的重现代码如下。是否存在配置疏忽、功能限制,或者这是一个 AutoMapper 错误?
public class MyClass
{
public int Id { get; set; }
public string Name { get; set; } = null!;
}
public record MyClassDto
{
public int Id { get; init; }
public string Name { get; init; } = null!;
private class Mapping : Profile
{
public Mapping() => CreateMap<MyClass, MyClassDto>();
}
}
public record MyGenericClassDto<T>
{
public T Id { get; init; } = default!;
public string Name { get; init; } = null!;
private class Mapping : Profile
{
public Mapping() => CreateMap<MyClass, MyGenericClassDto<int>>();
}
}
public class MappingTests
{
[Test]
// Will succeed
[TestCase(typeof(MyClass), typeof(MyClassDto))]
// Will fail
[TestCase(typeof(MyClass), typeof(MyGenericClassDto<int>))]
public void ShouldSupportMappingFromSourceToDestination_AddMaps(
Type source,
Type destination)
{
var instance = GetInstanceOf(source);
var configuration = new MapperConfiguration(config =>
config.AddMaps(Assembly.GetAssembly(typeof(MyClass))));
var mapper = configuration.CreateMapper();
mapper.Map(instance, source, destination);
}
[Test]
// Will succeed
[TestCase(typeof(MyClass), typeof(MyClassDto))]
// Will succeed
[TestCase(typeof(MyClass), typeof(MyGenericClassDto<int>))]
public void ShouldSupportMappingFromSourceToDestination_CreateMap(
Type source,
Type destination)
{
var instance = GetInstanceOf(source);
var config = new MapperConfiguration(cfg =>
cfg.CreateMap(source, destination));
var mapper = new Mapper(config);
mapper.Map(instance, source, destination);
}
private static object GetInstanceOf(Type type) =>
type.GetConstructor(Type.EmptyTypes) != null
? Activator.CreateInstance(type)! :
// Type without parameterless constructor
RuntimeHelpers.GetUninitializedObject(type);
}
在对此进行了更多的尝试之后,我开始调查 AutoMapper 源代码,以努力追踪这种行为差异,并且我已经确定了受影响的代码。我复制了下面的方法,删除了不相关的部分。
private void AddMapsCore(IEnumerable<Assembly> assembliesToScan)
{
var autoMapAttributeProfile = new Profile(nameof(AutoMapAttribute));
foreach (var type in assembliesToScan.Where(a => !a.IsDynamic && a != typeof(Profile).Assembly).SelectMany(a => a.GetTypes()))
{
if (typeof(Profile).IsAssignableFrom(type) && !type.IsAbstract && !type.ContainsGenericParameters)
{
AddProfile(type);
}
foreach (var autoMapAttribute in type.GetCustomAttributes<AutoMapAttribute>())
{
// Scans assemblies with AutoMap attribute. Irrelevant to the question.
}
}
AddProfile(autoMapAttributeProfile);
}
您会注意到,在 foreach 循环内部,AutoMapper 显式忽略任何泛型(或嵌套在泛型中)的 Profile 类。如果您关注
AddProfile(type)
电话,您就会明白原因:
public void AddProfile(Type profileType) => AddProfile((Profile)Activator.CreateInstance(profileType));
该方法只是实例化实现 Profile 的类的新实例。对于泛型类,有必要向正在实例化的类提供类型参数。考虑到这一点,我们可以创建一些新方法,并且可以更新
AddMapsCore
方法以将它们用于泛型类。
public void AddGenericProfile(Type profileType) => AddProfile((Profile)GetInstanceOfGeneric(profileType));
private static object GetInstanceOfGeneric(Type genericType)
{
var typeArgs = GetGenericArguments(genericType);
var constructedType = genericType.MakeGenericType(typeArgs);
return Activator.CreateInstance(constructedType);
}
private static Type[] GetGenericArguments(
Type genericType) =>
genericType.GetGenericArguments()
.Select(_ => typeof(object))
.ToArray();
private void AddMapsCore(IEnumerable<Assembly> assembliesToScan)
{
var autoMapAttributeProfile = new Profile(nameof(AutoMapAttribute));
foreach (var type in assembliesToScan.Where(a => !a.IsDynamic && a != typeof(Profile).Assembly).SelectMany(a => a.GetTypes()))
{
if (typeof(Profile).IsAssignableFrom(type) && !type.IsAbstract)
{
if (type.ContainsGenericParameters)
{
AddGenericProfile(type);
}
else
{
AddProfile(type);
}
}
foreach (var autoMapAttribute in type.GetCustomAttributes<AutoMapAttribute>())
{
// Scans assemblies with AutoMap attribute. Irrelevant to the question.
}
}
AddProfile(autoMapAttributeProfile);
}
通过这些更新,我们在 OP 中编写的测试将通过。为了验证这一点,我分叉并更新了 AutoMapper 并进行了更改,并引入了新的测试来涵盖这些场景。所有现有测试和新测试均已通过。 更改的拉取请求在这里,但不幸的是它被维护者关闭,没有太多/任何反馈。
如果通过 PR 解决 AutoMapper 使用不一致问题的努力不成功,可以通过将嵌套 Profile 移动到其最初定义的通用类之外的自己的类来解决此问题。OP 中的用例可以通过将代码更新为以下内容即可解决:
public record MyGenericClassDto<T>
{
public T Id { get; init; } = default!;
public string Name { get; init; } = null!;
}
internal class GenericMapping : Profile
{
public GenericMapping() => CreateMap<MyClass, MyGenericClassDto<int>>();
}
这种用法与OP中的用法并不完全同义,因为它泄漏了原始包含类之外的映射类的详细信息,而非通用版本可以完全封装。更重要的是,它在实现细节方面造成了不一致。源生成器、分析器和其他旨在帮助抽象出其中一些细节的工具需要意识到这种差异。