Skip to content

AutoMapper 对象映射实践 ​

AutoMapper 是一个对象-对象映射器,核心作用是解决手工编写大量 dto.Name = entity.Name 这类样板代码的问题。主要用于将复杂的领域模型(Domain Model)展平为简单的传输数据模型(DTO)。

一、 为什么使用 AutoMapper ​

在三层架构或 DDD 架构中,数据模型(Entity)和数据传输对象(DTO)通常是分离的:

  • 解耦:不直接向外暴露数据库实体结构。
  • 安全性:防止外部直接修改敏感字段。
  • 性能:按需返回字段,减少序列化体积。

如果不使用工具,手工映射会产生大量枯燥且易错的赋值代码。AutoMapper 通过约定优于配置的原则,基于属性名称自动匹配,极大减少了手工赋值的工作量。

二、 基础使用流程 ​

1. 定义源与目标类 ​

csharp
public class Foo
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class FooDto
{
    public int ID { get; set; }
    public string Name { get; set; }
}

2. 配置与注册 ​

在使用 Map 方法之前,必须告诉 AutoMapper 哪些类可以映射。每个 AppDomain 只能进行一次配置,通常放在应用程序启动时(如 Startup.cs 或 Global.asax)。

csharp
var config = new MapperConfiguration(cfg => cfg.CreateMap<Foo, FooDto>());
var mapper = config.CreateMapper();

推荐使用 Profile 来组织映射,而不是全写在一起:

csharp
public class EmployeeProfile : Profile
{
    public EmployeeProfile()
    {
        CreateMap<Employee, EmployeeDto>();
    }
}

// 在启动时注册
var config = new MapperConfiguration(cfg =>
{
    cfg.AddProfile<EmployeeProfile>();
    // 或者直接扫描当前程序集
    // cfg.AddMaps(System.AppDomain.CurrentDomain.GetAssemblies());
});

3. 执行映射 ​

csharp
Foo foo = new Foo { ID = 1, Name = "Tom" };
FooDto dto = mapper.Map<FooDto>(foo);

三、 常见高级场景 ​

1. 扁平化映射 (Flattening) ​

对象-对象映射的常见用法是将复杂的嵌套对象模型展平为更简单的模型。 如果目标属性的名称与源类型的 类名+属性名 对应,AutoMapper 会自动进行拆解和匹配。

csharp
public class Employee
{
    public string Name { get; set; }
    public Department Department { get; set; }
}

public class Department
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class EmployeeDto
{
    public string Name { get; set; }
    public int DepartmentID { get; set; }   // 自动映射到 Employee.Department.ID
    public string DepartmentName { get; set; } // 自动映射到 Employee.Department.Name
}

2. 自定义映射规则 ​

当源类型与目标类型名称不一致,或需要对数据做简单转换时,可以通过 ForMember 自定义规则。

csharp
var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Employee, EmployeeDto>()
        // 属性名不一致的映射
        .ForMember(dest => dest.EmployeeID, opt => opt.MapFrom(src => src.ID))
        // 简单的数据转换
        .ForMember(dest => dest.JoinYear, opt => opt.MapFrom(src => src.JoinTime.Year));
});

3. 数组和列表映射 ​

集合的映射非常简单,只需配置元素类型的映射关系,无需单独为 List<T> 配置。

csharp
cfg.CreateMap<Source, Destination>();
IMapper mapper = config.CreateMapper();

var sources = new[]
{
    new Source { Value = 5 },
    new Source { Value = 6 }
};

// 支持多种集合类型
IEnumerable<Destination> ienum = mapper.Map<Source[], IEnumerable<Destination>>(sources);
List<Destination> list = mapper.Map<Source[], List<Destination>>(sources);

⚠️ 空集合处理:当源集合为 null 时,AutoMapper 默认会将目标映射为空集合(Empty Collection),而不是 null。这符合框架设计规范(IEnumerable 永远不应该为 null)。

4. 方法到属性的映射 ​

如果源对象包含无参方法(如 GetFullName()),它可以直接映射到目标对象的同名属性(如 FullName),无需任何额外配置。

csharp
public class Employee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public string GetFullName()
    {
        return $"{FirstName} {LastName}";
    }
}

public class EmployeeDto
{
    // 会自动调用 GetFullName() 并赋值
    public string FullName { get; set; }
}

5. 命名约定冲突处理 ​

如果源和目标采用了不同的命名规范(如蛇形命名法与帕斯卡命名法),可以全局指定命名约定,而不需要一个个手动映射。

csharp
var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Foo, FooDto>();
    cfg.SourceMemberNamingConvention = new PascalCaseNamingConvention();
    cfg.DestinationMemberNamingConvention = new LowerUnderscoreNamingConvention();
});

四、 常见踩坑与注意事项 ​

  1. 不要滥用 AutoMapper

    • 映射逻辑应该是简单的。如果一个 ForMember 里面写了大量的业务判断和计算,说明这个逻辑属于业务层,而不是映射层。
    • 遇到这种情况,应当在业务代码里显式处理,或者提炼到实体的方法中,保持 AutoMapper 职责单一。
  2. 私有属性与构造函数映射

    • 默认情况下 AutoMapper 仅映射 public 成员。
    • 如果目标类没有公共的 set 方法(如通过构造函数初始化的只读 DTO),AutoMapper 也能自动匹配构造函数进行传参。
  3. 嵌套映射

    • 扁平化是将内部对象的属性提取出来;如果需要保留嵌套结构(如 EmployeeDto 内包含 DepartmentDto),只需确保内部的类(Department 到 DepartmentDto)也配置了 CreateMap 即可,AutoMapper 会自动递归处理。

方舟 · Ark