对象到对象映射
将一个对象映射到另一个相似的对象是很常见的。由于通常两个类具有相同或相似的属性相互映射,这也显得繁琐且重复。想象下面一个典型的应用服务方法:
public class UserAppService : ApplicationService
{
private readonly IRepository<User, Guid> _userRepository;
public UserAppService(IRepository<User, Guid> userRepository)
{
_userRepository = userRepository;
}
public async Task CreateUser(CreateUserInput input)
{
//手动从CreateUserInput对象创建User对象
var user = new User
{
Name = input.Name,
Surname = input.Surname,
EmailAddress = input.EmailAddress,
Password = input.Password
};
await _userRepository.InsertAsync(user);
}
}
CreateUserInput 是一个简单的DTO类,而 User 是一个简单的实体。上面的代码根据给定的输入对象创建一个 User 实体。在实际应用程序中,User 实体会拥有更多属性,手动创建它将变得繁琐且容易出错。当你向 User 和 CreateUserInput 类添加新属性时,还必须修改映射代码。
我们可以使用一个库来自动处理这类映射。ABP为对象到对象映射提供了抽象,并有一个集成包来使用AutoMapper作为对象映射器。
IObjectMapper
IObjectMapper 接口(位于 Volo.Abp.ObjectMapping 包中)定义了一个简单的 Map 方法。前面介绍的示例代码可以重写如下:
public class UserAppService : ApplicationService
{
private readonly IRepository<User, Guid> _userRepository;
public UserAppService(IRepository<User, Guid> userRepository)
{
_userRepository = userRepository;
}
public async Task CreateUser(CreateUserInput input)
{
//使用CreateUserInput对象自动创建新的User对象
var user = ObjectMapper.Map<CreateUserInput, User>(input);
await _userRepository.InsertAsync(user);
}
}
在此示例中,
ObjectMapper定义在ApplicationService基类中。当你在其他地方需要时,可以直接注入IObjectMapper接口。
Map方法有两个泛型参数:第一个是源对象类型,第二个是目标对象类型。
如果你需要设置现有对象的属性,可以使用 Map 方法的第二个重载:
public class UserAppService : ApplicationService
{
private readonly IRepository<User, Guid> _userRepository;
public UserAppService(IRepository<User, Guid> userRepository)
{
_userRepository = userRepository;
}
public async Task UpdateUserAsync(Guid id, UpdateUserInput input)
{
var user = await _userRepository.GetAsync(id);
//使用UpdateUserInput自动设置user对象的属性
ObjectMapper.Map<UpdateUserInput, User>(input, user);
await _userRepository.UpdateAsync(user);
}
}
在使用之前,你应该已经定义了映射关系。请参阅AutoMapper/Mapperly集成部分以了解如何定义映射。
AutoMapper 集成
AutoMapper 是最流行的对象到对象映射库之一。Volo.Abp.AutoMapper 包为 IObjectMapper 定义了AutoMapper集成。
一旦你按照下面所述定义了映射,你就可以像前面解释的那样使用 IObjectMapper 接口。
定义映射
AutoMapper提供了多种定义类之间映射的方法。详情请参阅其官方文档。
定义对象映射的一种方法是创建一个 Profile 类。示例:
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<User, UserDto>();
}
}
然后你应该使用 AbpAutoMapperOptions 注册配置:
[DependsOn(typeof(AbpAutoMapperModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpAutoMapperOptions>(options =>
{
//添加MyModule类所在程序集中定义的所有映射
options.AddMaps<MyModule>();
});
}
}
AddMaps 会注册给定类(通常是你的模块类)所在程序集中定义的所有配置类。它也会注册属性映射。
配置验证
AddMaps 可以可选地接受一个 bool 参数来控制模块的配置验证:
options.AddMaps<MyModule>(validate: true);
虽然此选项默认是 false,但建议启用配置验证作为最佳实践。
可以使用 AddProfile 而不是 AddMaps 来控制每个配置类的配置验证:
options.AddProfile<MyProfile>(validate: true);
如果你有多个配置,并且只需要为其中几个启用验证,首先使用不带验证的
AddMaps,然后为每个你想验证的配置使用AddProfile。
映射对象扩展
对象扩展系统允许为现有类定义额外的属性。ABP提供了一个映射定义扩展来正确映射两个对象的额外属性。
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<User, UserDto>()
.MapExtraProperties();
}
}
如果两个类都是可扩展对象(实现了 IHasExtraProperties 接口),建议使用 MapExtraProperties() 方法。更多信息请参阅对象扩展文档。
其他有用的扩展方法
还有一些扩展方法可以简化你的映射代码。
忽略审计属性
将对象映射到另一个对象时,忽略审计属性是很常见的。
假设你需要将一个 ProductDto (DTO) 映射到一个 Product 实体,并且该实体继承自 AuditedEntity 类(它提供了 CreationTime、CreatorId、IHasModificationTime...等属性)。
你可能希望在从DTO映射时忽略这些基类属性。你可以使用 IgnoreAuditedObjectProperties() 方法来忽略所有审计属性(而不是手动一个一个地忽略它们):
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<ProductDto, Product>()
.IgnoreAuditedObjectProperties();
}
}
根据你的实体类型,还有更多扩展方法,如 IgnoreFullAuditedObjectProperties() 和 IgnoreCreationAuditedObjectProperties()。
有关审计属性的更多信息,请参阅实体文档中的"审计属性的基类和接口"部分。
忽略其他属性
在AutoMapper中,你通常编写这样的映射代码来忽略一个属性:
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<SimpleClass1, SimpleClass2>()
.ForMember(x => x.CreationTime, map => map.Ignore());
}
}
我们发现它不必要地冗长,因此创建了 Ignore() 扩展方法:
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<SimpleClass1, SimpleClass2>()
.Ignore(x => x.CreationTime);
}
}
Mapperly 集成
Mapperly 是一个用于生成对象映射的 .NET 源生成器。Volo.Abp.Mapperly 包为 IObjectMapper 定义了 Mapperly 集成。
一旦你按照下面所示定义了映射类,就可以像前面解释的那样使用 IObjectMapper 接口。
定义映射类
你可以使用 Mapper 属性定义一个映射器类。该类和方法必须是 partial 的,以允许 Mapperly 在构建过程中生成实现:
[Mapper]
public partial class UserToUserDtoMapper : MapperBase<User, UserDto>
{
public override partial UserDto Map(User source);
public override partial void Map(User source, UserDto destination);
}
如果你还想将 UserDto 映射回 User,你可以继承 TwoWayMapperBase<User, UserDto> 类:
[Mapper]
public partial class UserToUserDtoMapper : TwoWayMapperBase<User, UserDto>
{
public override partial UserDto Map(User source);
public override partial void Map(User source, UserDto destination);
public override partial User ReverseMap(UserDto destination);
public override partial void ReverseMap(UserDto destination, User source);
}
映射前后方法
基类提供了 BeforeMap 和 AfterMap 方法,可以重写它们以在映射前后执行操作:
[Mapper]
public partial class UserToUserDtoMapper : TwoWayMapperBase<User, UserDto>
{
public override partial UserDto Map(User source);
public override partial void Map(User source, UserDto destination);
public override partial void BeforeMap(User source)
{
//TODO: 在映射前执行操作
}
public override partial void AfterMap(User source, UserDto destination)
{
//TODO: 在映射后执行操作
}
public override partial User ReverseMap(UserDto destination);
public override partial void ReverseMap(UserDto destination, User source);
public override partial void BeforeReverseMap(UserDto destination)
{
//TODO: 在反向映射前执行操作
}
public override partial void AfterReverseMap(UserDto destination, User source)
{
//TODO: 在反向映射后执行操作
}
}
映射对象扩展
对象扩展系统允许为现有类定义额外的属性。ABP提供了一个映射定义扩展来正确映射两个对象的额外属性:
[Mapper]
[MapExtraProperties]
public partial class UserToUserDtoMapper : MapperBase<User, UserDto>
{
public override partial UserDto Map(User source);
public override partial void Map(User source, UserDto destination);
}
如果两个类都是可扩展对象(实现了 IHasExtraProperties 接口),建议使用 MapExtraPropertiesAttribute 属性。更多信息请参阅对象扩展文档。
属性设置器方法
Mapperly 要求源对象和目标对象的属性都具有 setter 方法。否则,该属性将被忽略。你可以使用 protected set 或 private set 来控制 setter 方法的可见性,但每个属性必须有一个 setter 方法。
深度克隆
默认情况下,为了提高性能,Mapperly 不会创建对象的深度副本。如果一个对象可以直接赋值给目标对象,它就会这样做(例如,如果源类型和目标类型都是 List<T>,列表及其条目将不会被克隆)。要创建深度副本,请将 MapperAttribute 上的 UseDeepCloning 属性设置为 true。
[Mapper(UseDeepCloning = true)]
public partial class UserToUserDtoMapper : MapperBase<User, UserDto>
{
public override partial UserDto Map(User source);
public override partial void Map(User source, UserDto destination);
}
列表和数组支持
ABP Mapperly 集成还支持映射列表和数组,如 IObjectMapper<TSource, TDestination> 接口 部分所述。
示例:
[Mapper]
public partial class UserToUserDtoMapper : MapperBase<User, UserDto>
{
public override partial UserDto Map(User source);
public override partial void Map(User source, UserDto destination);
}
var users = await _userRepository.GetListAsync(); // 返回 List<User>
var dtos = ObjectMapper.Map<List<User>, List<UserDto>>(users); // 创建 List<UserDto>
当映射集合属性时,如果源值为 null,Mapperly 会将目标值保持为 null。这与 AutoMapper 不同,后者会将目标字段映射到一个空集合。
嵌套映射
在处理嵌套对象映射时,有一个重要的限制需要注意。如果你有单独的嵌套类型映射器,如下面的示例所示,父映射器 (SourceTypeToDestinationTypeMapper) 将不会自动使用嵌套映射器 (SourceNestedTypeToDestinationNestedTypeMapper) 来处理嵌套属性的映射。这意味着,嵌套映射器上的像 MapperIgnoreTarget 这样的配置在父映射操作期间将被忽略。
public class SourceNestedType
{
public string Name { get; set; }
public string Ignored { get; set; }
}
public class SourceType
{
public string Name { get; set; }
public SourceNestedType Nested { get; set; }
}
public class DestinationNestedType
{
public string Name { get; set; }
public string Ignored { get; set; }
}
public class DestinationType
{
public string Name { get; set; }
public DestinationNestedType Nested { get; set; }
}
[Mapper]
public partial class SourceTypeToDestinationTypeMapper : MapperBase<SourceType, DestinationType>
{
public override partial DestinationType Map(SourceType source);
public override partial void Map(SourceType source, DestinationType destination);
}
[Mapper]
public partial class SourceNestedTypeToDestinationNestedTypeMapper : MapperBase<SourceNestedType, DestinationNestedType>
{
[MapperIgnoreTarget(nameof(SourceNestedType.Ignored))]
public override partial DestinationNestedType Map(SourceNestedType source);
[MapperIgnoreTarget(nameof(SourceNestedType.Ignored))]
public override partial void Map(SourceNestedType source, DestinationNestedType destination);
}
有几种方法可以解决这个嵌套映射问题。选择最适合你特定需求的方法:
解决方案 1:多接口实现
在单个映射器类中实现两个映射接口 (IAbpMapperlyMapper<SourceType, DestinationType> 和 IAbpMapperlyMapper<SourceNestedType, DestinationNestedType>)。这种方法将所有相关的映射逻辑整合到一个类中。
重要提示: 记得实现 ITransientDependency 以将映射器类注册到依赖注入容器。
[Mapper]
public partial class SourceTypeToDestinationTypeMapper : IAbpMapperlyMapper<SourceType, DestinationType>, IAbpMapperlyMapper<SourceNestedType, DestinationNestedType>, ITransientDependency
{
public partial DestinationType Map(SourceType source);
public partial void Map(SourceType source, DestinationType destination);
public void BeforeMap(SourceType source)
{
}
public void AfterMap(SourceType source, DestinationType destination)
{
}
[MapperIgnoreTarget(nameof(SourceNestedType.Ignored))]
public partial DestinationNestedType Map(SourceNestedType source);
[MapperIgnoreTarget(nameof(SourceNestedType.Ignored))]
public partial void Map(SourceNestedType source, DestinationNestedType destination);
public void BeforeMap(SourceNestedType source)
{
}
public void AfterMap(SourceNestedType source, DestinationNestedType destination)
{
}
}
解决方案 2:整合映射方法
将嵌套映射方法从 SourceNestedTypeToDestinationNestedTypeMapper 复制到父类 SourceTypeToDestinationTypeMapper 中。这确保了所有映射逻辑都包含在一个映射器中。
示例:
[Mapper]
public partial class SourceTypeToDestinationTypeMapper : MapperBase<SourceType, DestinationType>
{
public override partial DestinationType Map(SourceType source);
public override partial void Map(SourceType source, DestinationType destination);
[MapperIgnoreTarget(nameof(SourceNestedType.Ignored))]
public override partial DestinationNestedType Map(SourceNestedType source);
[MapperIgnoreTarget(nameof(SourceNestedType.Ignored))]
public override partial void Map(SourceNestedType source, DestinationNestedType destination);
}
[Mapper]
public partial class SourceNestedTypeToDestinationNestedTypeMapper : MapperBase<SourceNestedType, DestinationNestedType>
{
[MapperIgnoreTarget(nameof(SourceNestedType.Ignored))]
public override partial DestinationNestedType Map(SourceNestedType source);
[MapperIgnoreTarget(nameof(SourceNestedType.Ignored))]
public override partial void Map(SourceNestedType source, DestinationNestedType destination);
}
解决方案 3:依赖注入方法
将嵌套映射器作为依赖项注入到父映射器中,并在 AfterMap 方法中手动使用它来处理嵌套对象映射。
示例:
[Mapper]
public partial class SourceTypeToDestinationTypeMapper : MapperBase<SourceType, DestinationType>
{
private readonly SourceNestedTypeToDestinationNestedTypeMapper _sourceNestedTypeToDestinationNestedTypeMapper;
public SourceTypeToDestinationTypeMapper(SourceNestedTypeToDestinationNestedTypeMapper sourceNestedTypeToDestinationNestedTypeMapper)
{
_sourceNestedTypeToDestinationNestedTypeMapper = sourceNestedTypeToDestinationNestedTypeMapper;
}
public override partial DestinationType Map(SourceType source);
public override partial void Map(SourceType source, DestinationType destination);
public override void AfterMap(SourceType source, DestinationType destination)
{
if (source.Nested != null)
{
destination.Nested = _sourceNestedTypeToDestinationNestedTypeMapper.Map(source.Nested);
}
}
}
选择正确的解决方案
每种解决方案都有其自身的优势:
- 解决方案 1 将所有映射逻辑整合到一个地方,当映射紧密相关时效果很好。
- 解决方案 2 简单,但如果你在其他地方也需要嵌套映射器,可能会导致代码重复。
- 解决方案 3 保持了关注点分离和可重用性,但需要在
AfterMap方法中手动映射。
选择最符合你的应用程序架构和可维护性要求的方法。
更多 Mapperly 功能
Mapperly 的大多数功能,如 Ignore,都可以通过其属性进行配置。更多详情请参阅 Mapperly 文档。
高级主题
IObjectMapper 接口
假设你创建了一个可重用模块,它定义了 AutoMapper/Mapperly 配置,并在需要映射对象时使用 IObjectMapper。根据模块化的性质,你的模块然后可以在不同的应用程序中使用。
IObjectMapper 是一个抽象,可以被最终应用程序替换以使用另一个映射库。这里的问题是你的可重用模块被设计为使用 AutoMapper/Mapperly 库,因为它只为该库定义了映射。在这种情况下,即使最终应用程序使用了另一个默认的对象映射库,你也希望确保你的模块始终使用 AutoMapper/Mapperly。
IObjectMapper<TContext> 用于对对象映射器进行上下文化,因此你可以为不同的模块/上下文使用不同的库。
用法示例:
public class UserAppService : ApplicationService
{
private readonly IRepository<User, Guid> _userRepository;
private readonly IObjectMapper<MyModule> _objectMapper;
public UserAppService(
IRepository<User, Guid> userRepository,
IObjectMapper<MyModule> objectMapper) //注入模块特定的映射器
{
_userRepository = userRepository;
_objectMapper = objectMapper;
}
public async Task CreateUserAsync(CreateUserInput input)
{
//使用模块特定的映射器
var user = _objectMapper.Map<CreateUserInput, User>(input);
await _userRepository.InsertAsync(user);
}
}
UserAppService 注入了 IObjectMapper<MyModule>,即此模块的特定对象映射器。它的用法与 IObjectMapper 完全相同。
上面的示例代码没有使用 ApplicationService 中定义的 ObjectMapper 属性,而是注入了 IObjectMapper<MyModule>。但是,仍然可以使用基类属性,因为 ApplicationService 定义了一个可以在类构造函数中设置的 ObjectMapperContext 属性。因此,上面的示例可以重写如下:
public class UserAppService : ApplicationService
{
private readonly IRepository<User, Guid> _userRepository;
public UserAppService(IRepository<User, Guid> userRepository)
{
_userRepository = userRepository;
//设置对象映射器上下文
ObjectMapperContext = typeof(MyModule);
}
public async Task CreateUserAsync(CreateUserInput input)
{
var user = ObjectMapper.Map<CreateUserInput, User>(input);
await _userRepository.InsertAsync(user);
}
}
虽然使用上下文化的对象映射器与普通对象映射器相同,但你应该在模块的 ConfigureServices 方法中注册上下文化映射器:
使用 AutoMapper 时:
[DependsOn(typeof(AbpAutoMapperModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
//为 MyModule 使用 AutoMapper
context.Services.AddAutoMapperObjectMapper<MyModule>();
Configure<AbpAutoMapperOptions>(options =>
{
options.AddMaps<MyModule>(validate: true);
});
}
}
使用 Mapperly 时:
[DependsOn(typeof(AbpMapperlyModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
//为 MyModule 使用 Mapperly
context.Services.AddMapperlyObjectMapper<MyModule>();
}
}
IObjectMapper<MyModule> 对于可重用模块来说是一个基本特性,该模块可以在多个应用程序中使用,每个应用程序可能使用不同的库进行对象到对象映射。所有预构建的 ABP 模块都使用它。但是,对于最终应用程序,你可以忽略此接口,始终使用默认的 IObjectMapper 接口。
IObjectMapper<TSource, TDestination> 接口
ABP 允许你为特定类自定义映射代码。假设你想创建一个自定义类来将 User 映射到 UserDto。在这种情况下,你可以创建一个实现 IObjectMapper<User, UserDto> 的类:
public class MyCustomUserMapper : IObjectMapper<User, UserDto>, ITransientDependency
{
public UserDto Map(User source)
{
//TODO: 创建一个新的 UserDto
}
public UserDto Map(User source, UserDto destination)
{
//TODO: 设置现有 UserDto 的属性
return destination;
}
}
ABP 会自动发现并注册 MyCustomUserMapper,并且每当你使用 IObjectMapper 将 User 映射到 UserDto 时,它会自动使用。一个类可以为不同的对象对实现多个 IObjectMapper<TSource, TDestination>。
这种方法非常强大,因为
MyCustomUserMapper可以注入任何其他服务并在Map方法中使用。
一旦你实现了 IObjectMapper<User, UserDto>,ABP 就可以自动将 User 对象的集合转换为 UserDto 对象的集合。支持以下通用集合类型:
IEnumerable<T>ICollection<T>Collection<T>IList<T>List<T>T[](数组)
示例:
var users = await _userRepository.GetListAsync(); // 返回 List<User>
var dtos = ObjectMapper.Map<List<User>, List<UserDto>>(users); // 创建 List<UserDto>
抠丁客


