在 ASP.NET Core 开发中,我们经常需要在不同层之间传递数据:比如从数据库模型(Entity)转换到 DTO,再从 DTO 转换为前端视图模型。这些转换代码大量重复、冗长、容易出错。为了解决这个问题,AutoMapper 诞生了。
本文主要介绍在WebApi(.net 8)环境中使用AutoMapper 14.0.0
官方文档地址:AutoMapper — AutoMapper documentation
一、什么是 AutoMapper?
AutoMapper 是一个 .NET 类库,它能自动在两个类型之间做映射,尤其是当这些类型拥有相似属性时。它减少了手动编写 obj.Prop = dto.Prop
的重复性劳动。
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
public string Address{ get; set; }
public string Nickname{ get; set; }
}
public class Dto_Student
{
public string Name { get; set; }
public int Age { get; set; }
}
可以直接通过 AutoMapper 在这两者之间做转换,而无需显式赋值。
二、基本用法:从零开始配置
1.安装 NuGet 包
dotnet add package AutoMapper --version 14.0.0
13.0开始无需引入AutoMapper.Extensions.Microsoft.DependencyInjection
2.配置映射关系
创建一个映射配置 Profile 类:
public class StudentProfile : Profile
{
public StudentProfile()
{
CreateMap<Student, Dto_Student>();
}
}
3.在 Program.cs中注册
using AutoMapper;
namespace AutoMapperProfileConfig
{
/// <summary>
/// 标记程序集哑类
/// </summary>
public class AutoMapperProfileConfigMarker : Profile
{
public AutoMapperProfileConfigMarker()
{
}
}
}
builder.Services.AddAutoMapper(typeof(AutoMapperProfileConfigMarker).Assembly);
建议专门建立一个类库,同时为每个表的映射创建一个映射类,然后创建一个专门标记程序集而不做其他事的一个哑类.
这样可以自动扫描该类库里面的所有的Profile类(还有几种其他方法,可自行查看)
4.依赖注入使用映射服务
[ApiController]
[Route("api/[controller]")]
public class StudentController : ControllerBase
{
private readonly YourDbContext _context;
private readonly IMapper _mapper;
public StudentController(YourDbContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
// GET: api/Student
[HttpGet]
public async Task<ActionResult<IEnumerable<Dto_Student>>> GetStudents()
{
// 从数据库获取 Student 实体列表
var students = await _context.Students.ToListAsync();
// 使用 AutoMapper 将实体列表映射为 DTO 列表
var studentDtos = _mapper.Map<List<Dto_Student>>(students);
// 返回给前端
return Ok(studentDtos);
}
}
从源数据映射到目标数据的过程中默认会忽略null,也不会抛异常.
5.命名约定
默认情况映射的属性名必须完全一致,但你可以配置实现特殊的映射关系.
AutoMapper 中命名约定的优先级关系如下:
优先级从高到低:
-
CreateMap().ForMember()
明确指定字段映射 -
在
Profile
中设置的命名约定 -
全局设置(MapperConfiguration 中配置)
-
默认行为(ExactMatchNamingConvention)
public class Source
{
public string user_id { get; set; }
}
public class Destination
{
public string UserId { get; set; }
}
情况 1:使用 .ForMember()
明确映射
CreateMap<Source, Destination>()
.ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.user_id));
优先级最高,无论有没有配置命名约定,它都生效
情况 2:Profile 中设置命名约定
public class MyProfile : Profile
{
public MyProfile()
{
SourceMemberNamingConvention = LowerUnderscoreNamingConvention.Instance;
DestinationMemberNamingConvention = PascalCaseNamingConvention.Instance;
CreateMap<Source, Destination>();
}
}
会生效,但仅限于此 Profile
中的 CreateMap
映射。
情况 3:全局配置命名约定
services.AddAutoMapper(cfg =>
{
cfg.SourceMemberNamingConvention = LowerUnderscoreNamingConvention.Instance;
cfg.DestinationMemberNamingConvention = PascalCaseNamingConvention.Instance;
}, typeof(SomeMarker));
会自动应用到所有未手动指定命名规则的 Profile 中的映射
情况 4:都不配置时,默认使用 ExactMatch
即只匹配名字完全一致(区分大小写)的属性名
设置方式 | 作用范围 | 优先级 | 备注 |
---|---|---|---|
.ForMember() | 某个字段映射 | 🥇 最高 | 覆盖一切 |
Profile 中命名约定 | 当前 Profile | 🥈 中高 | 优于全局配置 |
cfg.SourceMemberNamingConvention | 所有 Profile | 🥉 中等 | 默认使用 |
不设置 | 使用 ExactMatch | 最低 | 仅匹配完全相同 |
6.如何测试映射
#if DEBUG
using (var scope = app.Services.CreateScope())
{
var mapper = scope.ServiceProvider.GetRequiredService<IMapper>();
mapper.ConfigurationProvider.AssertConfigurationIsValid();
}
#endif
这可以在开发时提前测试你配置的映射是否正确
7.配置可见性
默认情况:AutoMapper自动映射public的属性和字段(public属性即便是private setters 也没问题),忽略internal和private,但是你可以覆盖默认的筛选器 ShouldMapField 或 ShouldMapProperty来修改
builder.Services.AddAutoMapper((cfg) =>
{
cfg.ShouldMapProperty= prop =>prop.GetMethod!=null&&(prop.GetMethod.IsPublic || prop.GetMethod.IsAssembly||prop.GetMethod.IsPrivate);
cfg.ShouldMapField =field=> field.IsPublic || field.IsAssembly || field.IsPrivate;
}, typeof(AutoMapperProfileConfigMarker).Assembly);
补充解释:
-
p.GetMethod != null
:排除掉没有 getter 的属性。 -
IsPublic
:表示public
getter。 -
IsAssembly
:表示internal
getter(注意:internal
在反射中叫IsAssembly
)。 -
ShouldMapField
:是否启用字段映射(多数情况下不需要,但你可以按需开启)。
AutoMapper 与属性访问权限的总结
(1) 属性的访问权限本质是 getter 和 setter 的访问修饰符
-
在 C# 中,属性本身没有独立的访问修饰符,它的访问权限由 getter 和 setter 的访问修饰符决定。
-
例如:
-
public string Name { get; set; }
—— getter 和 setter 都是 public。 -
public string Name { get; private set; }
—— getter 是 public,setter 是 private。 -
private string Age { get; set; }
—— getter 和 setter 都是 private。 -
不可能出现 private string Age { public get; set; }
-
也不可能出现 private string Age { internal
get; set; }
-
(2) 反射中通过 prop.GetMethod
(getter 方法)来判断属性的“可见性”
-
prop.GetMethod
表示属性的 getter 方法。 -
通过
prop.GetMethod.IsPublic
、IsPrivate
、IsAssembly
(internal)等判断访问级别。 -
这就相当于判断属性的访问权限,AutoMapper 使用它决定是否映射某个属性。
(3) AutoMapper 默认只映射公开(public)属性
-
默认情况下,AutoMapper 只映射 getter 是 public 的属性。
-
它支持映射 setter 是 private 的属性(setter private 不影响映射)。
-
不映射 getter 是 internal 或 private 的属性。
(4) 如何让 AutoMapper 识别 internal 或 private 属性
-
可以通过覆盖配置中的筛选器:
cfg.ShouldMapProperty = prop => prop.GetMethod != null && (prop.GetMethod.IsPublic || prop.GetMethod.IsAssembly || prop.GetMethod.IsPrivate);
-
这样,AutoMapper 也会映射 internal 和 private getter 的属性。
(5) 映射私有字段也可以
-
除了属性,还可以配置
ShouldMapField
来映射字段(包括私有字段)。 -
这对某些隐藏数据很有用,但要谨慎使用,避免破坏封装。
(6) 总结理解
-
属性的访问权限本质依赖 getter/setter。
-
反射中通过
prop.GetMethod
判断属性访问权限是合理且准确的。 -
AutoMapper 用它来决定是否映射某个属性,既高效又可靠。
8.投影(Projection)
Projection 就是把一个对象结构转换成另一个更适合使用场景的结构。
举个例子:
你从数据库读到了一个完整的实体类 CalendarEvent
,但你在前端表单上不需要完全一样的结构——你希望时间是拆分的、字段名称不同,那么就需要用 AutoMapper 的 Projection 功能来进行“转换 + 结构重组”。
示例拆解说明
原始模型(source)
public class CalendarEvent
{
public DateTime Date { get; set; }
public string Title { get; set; }
}
数据库中拿到的实体就是这个类。比如 Date = 2008-12-15 20:30:00
。
目标模型(destination)
public class CalendarEventForm
{
public DateTime EventDate { get; set; }
public int EventHour { get; set; }
public int EventMinute { get; set; }
public string Title { get; set; }
}
这个类是用来给前端展示或绑定表单的。你希望:
-
日期是单独的日期(不带时间);
-
小时和分钟是两个单独字段;
-
字段名称也不一样。
AutoMapper 如何配置?
var configuration = new MapperConfiguration(cfg =>
cfg.CreateMap<CalendarEvent, CalendarEventForm>()
.ForMember(dest => dest.EventDate, opt => opt.MapFrom(src => src.Date.Date)) // 只取日期部分
.ForMember(dest => dest.EventHour, opt => opt.MapFrom(src => src.Date.Hour)) // 取小时
.ForMember(dest => dest.EventMinute, opt => opt.MapFrom(src => src.Date.Minute)) // 取分钟
);
ForMember 是对每个不规则映射字段进行配置
MapFrom 接收一个 lambda 表达式,定义你想怎么从源对象提取值
MapFrom 背后的原理
opt => opt.MapFrom(src => src.Date.Hour)
这相当于告诉 AutoMapper:
“你在做
CalendarEvent
→CalendarEventForm
映射的时候,EventHour
这个字段别猜了,我来明确告诉你,它等于src.Date.Hour
。”
MapFrom
接受一个委托(lambda 表达式)作为参数,它会在运行时动态调用你提供的这个逻辑。
最终结果
CalendarEventForm form = mapper.Map<CalendarEvent, CalendarEventForm>(calendarEvent);
form.EventDate == new DateTime(2008, 12, 15); // ✅
form.EventHour == 20; // ✅
form.EventMinute == 30; // ✅
form.Title == "Company Holiday Party"; // ✅
总结
项目 | 含义 |
---|---|
Projection | 结构上的重新组织,目标模型不一定是源模型的“平铺结构” |
ForMember | 指定目标对象某个成员如何映射 |
MapFrom | 提供自定义逻辑来指定如何从源对象取值 |
9.嵌套映射(Nested Mappings)
如何让 AutoMapper 自动将一个类中的嵌套对象,也映射成目标类中对应的嵌套对象。
(1) 背景:嵌套对象和映射需求
假设我们有如下两个类结构:
public class OuterSource
{
public int Value { get; set; }
public InnerSource Inner { get; set; }
}
public class InnerSource
{
public int OtherValue { get; set; }
}
这叫做嵌套结构,OuterSource
有个属性 Inner
是 InnerSource
类型,它本身也是一个复杂类型(不是简单的 int、string 等)。
而目标类结构是:
public class OuterDest
{
public int Value { get; set; }
public InnerDest Inner { get; set; }
}
public class InnerDest
{
public int OtherValue { get; set; }
}
(2) 你可以怎么映射?
AutoMapper 有两种选择:
-
扁平化映射(Flattening):
你可以把OuterSource.Inner.OtherValue
映射成一个扁平属性:public class OuterDestFlat { public int Value { get; set; } public int InnerOtherValue { get; set; } }
AutoMapper 能够识别这种扁平路径并自动映射。
-
嵌套映射(Nested Mapping):
也可以保持原来的嵌套结构,OuterDest.Inner
依然是个对象。
(3) 如何配置嵌套映射?
AutoMapper 是通过你对内部类型也配置映射关系来支持嵌套的:
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<OuterSource, OuterDest>();
cfg.CreateMap<InnerSource, InnerDest>();
});
这里我们定义了两个映射:
-
外层
OuterSource → OuterDest
-
内层
InnerSource → InnerDest
AutoMapper 会自动知道:
-
OuterSource.Inner
是InnerSource
-
OuterDest.Inner
是InnerDest
-
已经配置好了这两个类型之间的映射关系
-
所以会自动递归调用
Map<InnerSource, InnerDest>
来完成嵌套对象的映射
(4) 使用方式
var source = new OuterSource
{
Value = 5,
Inner = new InnerSource { OtherValue = 15 }
};
var mapper = config.CreateMapper();
var dest = mapper.Map<OuterSource, OuterDest>(source);
这里只调用了一次 Map<OuterSource, OuterDest>
,并不需要你去手动映射内部 InnerSource
,AutoMapper 会自动识别并使用我们配置的类型映射去转换嵌套对象。
(5) 注意
✅ 配置顺序无关
你不必担心cfg.CreateMap<InnerSource, InnerDest>()
写在OuterSource → OuterDest
前还是后,AutoMapper 会在需要的时候查找并使用映射。
✅ 嵌套自动映射
你只要调用顶层的Map<OuterSource, OuterDest>
,内部的嵌套对象如果有映射关系,AutoMapper 会递归处理。
✅ 灵活生成目标结构
AutoMapper 不仅支持字段名相同的直接映射,还支持扁平化、嵌套、字段转换、组合映射等高级映射需求。
当然,以下是关于 AutoMapper 构造函数映射(Constructor Mapping)的精炼总结:
10. AutoMapper 构造函数映射
-
默认情况:
-
如果目标类型没有定义构造函数,AutoMapper 使用自动生成的无参构造函数,通过属性赋值完成映射。
-
-
一旦定义了构造函数:
-
默认无参构造函数就不再存在。
-
AutoMapper 会尝试使用你定义的构造函数,通过参数名匹配源属性名来自动绑定。
-
-
参数名不匹配时:
-
AutoMapper 无法自动绑定,需要使用
.ForCtorParam()
显式指定映射关系。
cfg.CreateMap<Source, Dest>() .ForCtorParam("paramName", opt => opt.MapFrom(src => src.Property));
-
-
可选配置:
-
禁用构造函数映射(改用属性赋值):
cfg.DisableConstructorMapping();
-
指定哪些构造函数可被使用(如仅 public):
cfg.ShouldUseConstructor = ctor => ctor.IsPublic;
-