在本文中,我们将学习如何在 ASP.NET Core 应用程序中使用 Mapster。
首先,我们将了解 Mapster 是什么以及如何将其安装到 .NET Core 应用程序中。 然后,我们将在使用 Mapster 时尝试不同的数据映射选项。 之后,我们将了解 Mapster 的功能,用于在 Web API 项目中生成代码和扁平化对象。
开始吧。
Mapster是啥?
顾名思义,Mapster 是一个将一种对象类型映射到另一种对象类型的库。 它是一个基于约定的映射器,易于配置和使用。 编写代码将一个对象映射到另一个对象可能非常重复且无聊。 正因为如此,Mapster 使我们免于编写容易出错的样板代码。
在大多数情况下,我们不应该尝试重新发明轮子,而应该使用可用的选项。
让我们看看 Mapster 是否是一个不错的选择。
如何获取Mapster?
第一步是安装 Mapster NuGet 包:
Install-Package Mapster
就是这么简单。 现在我们已经可以在我们的项目中使用 Mapster 了。
准备环境
首先,我们创建两个类来表示实体,并创建一个用于 DTO 对象的类,我们将把实体中的值映射到该对象:
public class Person
{
public string? Title { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public DateTime? DateOfBirth { get; set; }
public Address? Address { get; set; }
}
在这里,我们创建将在整个示例中使用的 Person 类。 另外,我们里面有 Address 类,所以我们也添加这个类:
public class Address
{
public string? Street { get; set; }
public string? City { get; set; }
public string? PostCode { get; set; }
public string? Country { get; set; }
}
现在,我们已经有了实体。 是时候创建我们的第一个 DTO 类了:
public class PersonDto
{
public string? Title { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public DateTime? DateOfBirth { get; set; }
}
PersonDto 类具有原始 Person 实体的所有属性,但我们将在本文后面使用的 Address 除外。 需要注意的是,DTO 类中的所有属性都与实体属性命名相同,因此我们无需额外配置即可映射它们。
实体基础数据
为了准备我们的示例,我们将创建一个简单的类 DemoData 来创建一个简单的 Person 实体和一个 Person 集合。 在现实项目中,我们将从数据库中获取这些数据。
让我们添加第一个创建Person的方法:
public static Person CreatePerson()
{
return new Person()
{
Title = "Mr.",
FirstName = "Peter",
LastName = "Pan",
DateOfBirth = new DateTime(2000, 1, 1),
Address = new Address()
{
Country = "Neverland",
PostCode = "123N",
Street = "Funny Street 2",
City = "Neverwood"
},
};
}
这里我们创建一个名为 CreatePerson() 的新方法,它返回一个添加了地址的 Person 对象。
类似地,我们添加 CreatePeople() 方法,该方法的工作方式与第一个方法相同,但返回 Person 对象的集合。
使用 Mapster 进行基本映射
现在是时候了解一下 Mapster 在无需配置的情况下动态映射对象和集合的基本用法了。
首先,让我们创建一个名为 MappingFunctions 的新静态类,我们将用它来实现所有不同的映射功能:
public static class MappingFunctions
{
private static readonly Person _person = DemoData.CreatePerson();
private static readonly ICollection _people = DemoData.CreatePeople();
}
这里我们添加两个静态只读字段并初始化单个实体和 Person 实体的集合。
我们有两种不同的方式使用通用的 Adapt() 方法将单个对象映射到另一个对象:
映射到新对象
映射到现有对象
映射到新对象
首先,让我们在 MappingFunctions 类中创建一个新的静态方法:
public static PersonDto MapPersonToNewDto()
{
var personDto = _person.Adapt();
return personDto;
}
在 MapPersonToNewDto() 方法中,我们初始化一个新变量 personDto 作为将 _person 对象中的所有值映射到的目的地。
其次,让我们在项目中创建一个新控制器,并使用从函数中检索数据的方法:
[Route("api/people")]
[ApiController]
public class PeopleController: ControllerBase
{
[HttpGet("new-person")]
public IActionResult GetNewPerson()
{
var person = MappingFunctions.MapPersonToNewDto();
return Ok(person);
}
}
在这里,我们实现 PeopleController 类,以便我们可以使用我们的 API 操作。 我们添加一个名为 GetNewPerson() 的新操作,该操作使用 MappingFunctions 类中的 MapPersonToNewDto() 方法。 将结果存储在 person 变量中后,我们将返回带有 OK 状态代码的数据。
让我们用 Postman 来测试一下:
完美,我们的第一个映射函数可以工作了。
映射到现有对象
当我们想要将数据映射到现有对象时,我们可以使用另一种映射方式:
public static PersonDto MapPersonToExistingDto()
{
var personDto = new PersonDto();
_person.Adapt(personDto);
return personDto;
}
与前面的示例不同,在本例中,我们首先创建一个新的 PersonDto 对象。 之后,我们将数据从 _person 对象映射到现有的 personDto 变量。
我们使用与之前相同的 Adapt() 方法,该方法当前基于属性名称映射数据,无需自定义逻辑。
让我们在控制器中创建一个新操作:
[HttpGet("existing-person")]
public IActionResult GetExistingPerson()
{
var person = MappingFunctions.MapPersonToExistingDto();
return Ok(person);
}
在这里,我们添加 GetExistingPerson() 操作以及该操作上指定的新 URI。
同样,让我们用 Postman 来测试一下:
奇迹般有效。
映射集合
Mapster 库还提供了从 .NET 中的 Queryable 进行映射的扩展。 这样,我们可以加快从数据库获取的数据映射到可查询数据类型的过程。
让我们在 MappingFunctions 类中创建另一个方法:
public static IQueryable MapPersonQueryableToDtoQueryable()
{
var peopleQueryable = _people.AsQueryable();
var personDtos = peopleQueryable.ProjectToType();
return personDtos;
}
在
MapPersonQueryableToDtoQueryable() 方法中,我们使用 AsQueryable() 方法将 _people 对象中的数据存储到 peopleQueryable 变量中。 在下一步中,我们使用 Mapster 中的 ProjectToType() 通用方法将数据映射到 personDtos 变量。
再次,让我们在控制器中创建一个新端点来测试功能:
[HttpGet]
public IActionResult GetPeopleQueryable()
{
var people = MappingFunctions.MapPersonQueryableToDtoQueryable();
return Ok(people);
}
最后,让我们从 Postman 客户端调用 API 来获取结果:
我们可以看到所有具有正确数据的 DTO 对象。
使用 Mapster 自定义映射
现在,我们将通过自定义映射来实现功能,以了解 Mapster 库的潜力。
让我们解决映射数据时可能遇到的一些挑战,好吗?
我们需要添加自定义 Mapster 配置来实现自定义映射逻辑。 为此,我们将使用 TypeAdapterConfig 类。
首先,我们添加一个 MapsterConfig 静态类,其中包含用于 Mapster 配置的扩展方法:
public static class MapsterConfig
{
public static void RegisterMapsterConfiguration(this IServiceCollection services)
{
}
}
然后,我们可以在 Program 类中的服务中使用
RegisterMapsterConfiguration() 扩展方法:
builder.Services.RegisterMapsterConfiguration();
太好了,现在我们已经准备好使用 Mapster 实现自定义映射了。
映射到不同的成员
如果我们的 DTO 类中有不同名称的属性怎么办? 嗯,Mapster 有一个解决方案。
让我们添加另一个 DTO 类,我们将使用它来显示 Person 实体的信息:
public class PersonShortInfoDto
{
public string? FullName { get; set; }
}
在我们的 PersonShortInfoDto 类中,我们只有一个属性,我们将使用它来存储 Person 实体的全名,称为 FullName。
之后,我们向
RegisterMapsterConfiguration() 扩展方法添加一个新配置:
public static void RegisterMapsterConfiguration(this IServiceCollection services)
{
TypeAdapterConfig
.NewConfig()
.Map(dest => dest.FullName, src => $"{src.Title} {src.FirstName} {src.LastName}");
}
我们使用 TypeAdapterConfig 泛型类,其中 Person 类作为源,PersonShortInfoDto 类作为目标。
然后,我们使用接受两个 Func 委托的 Map() 方法。 在这里,我们使用字符串插值将 Title、FirstName 和 LastName 从 src 对象映射到 dest 对象的 FullName 字段。
最后,我们需要调用
RegisterMapsterConfiguration() 方法中的 Scan() 方法:
TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly());
Scan() 方法扫描程序集并将注册添加到 TypeAdapterConfig。
现在,我们可以在 MappingFunctions 类中实现一个新方法,稍后我们将在控制器中使用该方法:
public static PersonShortInfoDto CustomMapPersonToShortInfoDto()
{
var personShortInfoDto = _person.Adapt();
return personShortInfoDto;
}
在这里,我们使用与 Adapt() 方法相同的功能,但来自 MapsterConfig 类的自定义映射将定义我们如何映射数据。
让我们添加一个控制器Action:
[HttpGet("short-person")]
public IActionResult GetShortPerson()
{
var person = MappingFunctions.CustomMapPersonToShortInfoDto();
return Ok(person);
}
检查结果:
这是我们新的 DTO 的全名。
惊人的。
基于条件的映射
Mapster 中的 Map() 方法可以接受第三个参数,我们可以使用该参数根据源对象设置条件。 如果不满足条件,则将为目标对象分配 null 或默认值。
让我们向 PersonShortInfoDto 类添加另一个属性:
public int? Age { get; set; }
接下来,让我们修改 Mapster 配置以添加条件映射:
TypeAdapterConfig
.NewConfig()
.Map(dest => dest.FullName, src => $"{src.Title} {src.FirstName} {src.LastName}")
.Map(dest => dest.Age,
src => DateTime.Now.Year - src.DateOfBirth.Value.Year,
srcCond => srcCond.DateOfBirth.HasValue);
这里我们向 Map() 函数添加第三个参数,它是一个返回布尔值的 Func 委托。 我们通过从今天的日期年份中减去出生年份来为 Age 属性分配一个值,但前提是 DateOfBirth 属性具有值。
现在,我们可以再次从控制器调用 GetShortPerson() 操作来查看结果:
我们开始吧。 我们可以看到我们这个人的年龄。
映射嵌套成员
自定义映射的另一个示例是嵌套成员的映射。
让我们向 PersonShortInfoDto 类添加一个新属性:
public string? FullAddress { get; set; }
现在,我们可以修改配置以填充目标类的 FullAddress 属性:
TypeAdapterConfig
.NewConfig()
.Map(dest => dest.FullName, src => $"{src.Title} {src.FirstName} {src.LastName}")
.Map(dest => dest.Age,
src => DateTime.Now.Year - src.DateOfBirth.Value.Year,
srcCond => srcCond.DateOfBirth.HasValue)
.Map(dest => dest.FullAddress, src => $"{src.Address.Street} {src.Address.PostCode} - {src.Address.City}");
在这里,我们通过实体类上的 Address 属性访问嵌套属性。 根据 Address 值,我们通过字符串插值构造 FullAddress。
值得注意的是,Mapster 默认应用空传播。 从上面的示例中,如果 src 对象上的 Address 属性为 null,则会将 null 值分配给 FullAddress,而不是抛出 NullPointerException。
让我们调用控制器操作并检查 Postman 的结果:
忽略自定义成员
默认情况下,Mapster 将按名称映射属性,无需配置。 我们不需要在所有用例中都这样做,因此我们可以使用 Ignore() 方法来忽略特定成员:
TypeAdapterConfig
.NewConfig()
.Ignore(dest => dest.Title);
在本例中,我们忽略目标对象(即 PersonDto)上的 Title 属性。
完成后,让我们调用 GetNewPerson() 操作并再次检查结果:
我们可以看到与之前调用的差异,因为 Title 值现在为 null。
使用 Mapster,我们可以使用 IgnoreNonMapped() 方法来忽略配置中未显式设置的所有成员。 除此之外,我们还可以使用 IgnoreIf() 方法以及基于源或目标对象的条件。 当满足条件时,Mapster将跳过该属性。
使用 Mapster 忽略成员的另一种方法是使用类本身的 AdaptIgnore 属性。
为了实现这一点,我们可以在 Title 属性上方添加注释:
[AdaptIgnore]
public string? Title { get; set; }
这与上一个示例中基于规则的 Ignore() 方法的工作方式相同。
双向映射
Mapster 中的双向映射是一种以两种方式配置映射的简单方法:从源到目标以及从目标到源对象。
我们可以通过在配置中调用 TwoWays() 方法来实现此目的:
TypeAdapterConfig
.NewConfig()
.Ignore(dest => dest.Title)
.TwoWays();
之后,让我们创建一个新方法来基于 DTO 返回实体:
public static Person MapPersonDtoToPersonEntity()
{
var personDto = MapPersonToNewDto();
var person = personDto.Adapt();
return person;
}
并向我们的控制器添加一个新操作:
[HttpGet("entity")]
public IActionResult GetPersonEntity()
{
var person = MappingFunctions.MapPersonDtoToPersonEntity();
return Ok(person);
}
最后,我们可以调用我们的操作来确认双向映射正在工作:
我们看到 DTO 中的所有属性都已成功映射,但标题被忽略。
映射之前和之后
Mapster 有更多的自定义映射选项,有必要提及之前和之后的映射功能。 我们可以使用 BeforeMapping() 方法在映射操作之前执行操作。
要检查此功能,让我们向 PersonDto 类添加一个新方法:
public void SayHello()
{
Console.WriteLine("Hello...");
}
这里我们实现了输出一些文本的 SayHello() 方法。
现在,让我们转到 MapsterConfig 类并为 PersonDto 添加新配置:
TypeAdapterConfig.ForType()
.BeforeMapping((src, result) => result.SayHello());
我们使用 Mapster 中的 BeforeMapping() 方法,然后从结果变量中调用 SayHello() 方法。
最后,让我们再次调用 GetNewPerson() 操作并检查控制台以查看输出:
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5002
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: D:\GITHUB\CodeMaze\CodeMazeGuides\dotnet-client-libraries\HowToUseMapster\HowToUseMapster\
Hello...
我们可以在控制台中看到输出。
同样,现在让我们向 PersonDto 类添加另一个方法:
public void SayGoodbye()
{
Console.WriteLine("Goodbye...");
}
我们将使用 SayGoodbye() 方法来演示 AfterMapping() 方法在 Mapster 中的工作原理。
也就是说,让我们修改 MapsterConfig 类:
TypeAdapterConfig.ForType()
.BeforeMapping((src, result) => result.SayHello())
.AfterMapping((src, result) => result.SayGoodbye());
然后在调用相同的操作后检查控制台:
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5002
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: D:\GITHUB\CodeMaze\CodeMazeGuides\dotnet-client-libraries\HowToUseMapster\HowToUseMapster\
Hello...
Goodbye...
我们可以看到这两种方法的输出。
使用 Mapster 生成代码
使用映射库时我们经常提到的问题之一是更改类中的属性名称时可能发生的潜在回归。 为了避免这种情况,最好能看到 Mapster 用于映射成员的实际代码。
嗯,Mapster 有一个解决方案。 我们可以使用代码生成选项来生成映射函数的源代码。
首先,让我们创建一个新的配置类并实现 Mapster 中的 ICodeGenerationRegister 接口:
public class CodeGenerationConfig : ICodeGenerationRegister
{
public void Register(Mapster.CodeGenerationConfig config)
{
config.AdaptTo("[name]Model")
.ForType()
.ForType();
config.GenerateMapper("[name]Mapper")
.ForType()
.ForType();
}
}
这里我们根据第一个 AdaptTo() 方法自动创建两个带有“Model”前缀的类。 通过这种方法,我们还可以避免在项目中编写 DTO 类。
然后我们使用GenerateMapper()方法动态创建两个具有映射逻辑的类。 默认情况下,Mapster 将所有新类存储在“Models”文件夹中。
现在,我们将以下代码添加到 csproj 文件中以在构建时生成类:
最后,让我们在控制台中运行 dotnet build 命令来查看项目结构中的结果:
太棒了,我们有四个从 Mapster 自动生成的新类。 让我们检查 PersonMapper 类以查看实际的映射代码:
public static PersonModel AdaptToModel(this Person p1)
{
return p1 == null ? null : new PersonModel()
{
Title = p1.Title,
FirstName = p1.FirstName,
LastName = p1.LastName,
DateOfBirth = p1.DateOfBirth,
Address = p1.Address == null ? null : new AddressModel()
{
Street = p1.Address.Street,
City = p1.Address.City,
PostCode = p1.Address.PostCode,
Country = p1.Address.Country
}
};
}
public static PersonModel AdaptTo(this Person p2, PersonModel p3)
{
if (p2 == null)
{
return null;
}
PersonModel result = p3 ?? new PersonModel();
result.Title = p2.Title;
result.FirstName = p2.FirstName;
result.LastName = p2.LastName;
result.DateOfBirth = p2.DateOfBirth;
result.Address = funcMain1(p2.Address, result.Address);
return result;
}
我们可以看到现在可以使用的映射函数 AdaptToModel() 和 AdaptTo() 的逻辑。 有了这个,我们就可以确保编译器将报告我们在修改实体和模型的属性时可能出现的任何潜在错误。
使用 Mapster 代码生成的另一种方法是通过数据注释。 我们可以使用 AdaptTo 属性来实现这一点:
[AdaptTo("[name]Model"), GenerateMapper]
public class Person
使用 Mapster 展平和取消展平复杂对象
默认情况下,Mapster 会为我们执行展平操作。 假设我们有一个 Person 实体,它具有 Address 属性,其中包含 Street 属性。 现在我们可以向 PersonDto 类添加一个名为 AddressStreet 的新属性。 此时,Mapster 将执行映射,无需任何其他配置。
让我们将 AddressStreet 属性添加到我们的 PersonDto 类中:
public string? AddressStreet { get; set; }
现在,让我们再次调用 GetNewPerson() 操作(无需额外配置)并检查结果:
我们可以看到地址街道是根据展平自动填充的。
但是,我们需要显式配置展开过程。 因此,如果我们想从 AddressStreet 映射到 Address.Street,我们可以从 TypeAdapterConfig 类调用 Unflattening 方法并将第一个参数设置为 true。
结论
在本文中,我们学习了如何使用 Mapster 对 ASP.NET Core 项目中的数据执行基本映射操作。 我们已经了解了自动代码生成如何与 Mapster 一起工作,以及如何通过几个步骤展平或取消展平我们的对象。
主要结论是,就 .NET 生态系统中的映射而言,Mapster 是一个非常有用且简单的库,因此请检查一下。