在今天的文章中,你将学习如何使用各种技术和库在 .NET 中映射对象。我们将探讨 2024 年在 .NET 中映射对象的最佳方式。
什么是对象映射,为什么在 ASP.NET Core 应用程序中需要对象映射?
对象映射是对象从一种类型到另一种类型的转换,通常是在应用程序的不同层之间。
映射对于分离关注点至关重要
以下是使用对象映射的几个原因:
在我的网站 antondevtips.com 上**,我已经有关于 .NET 的博客。**
订阅,因为更多即将到来。
在许多应用程序中,通常有 2 个级别的模型:领域和公共契约。这些公共契约模型通常称为 DTO(数据传输对象)。
DTO 是客户端使用的模型,通常它们比其域对应物更小。此外,DTO 模型可能包含来自少数域模型的属性。
必须将领域内部模型与公共模型区分开来。客户应该收到他们需要的确切数据,而不是更多。出于安全考虑,他们不应使用包含私有和内部数据的模型。
对公共合约使用单独模型的另一个原因:您可以在保持公共合约不变的同时更改域模型,这样您就不会使用 API 破坏客户端。
我们弄清楚了什么是对象映射,现在让我们探索一些最常用的映射技术。
对象映射有 2 种主要方法:手动和使用映射库自动。
手动方法涉及手动编写执行映射的代码,例如:
public static BookDto MapToBookDto(this Book entity)
{
return new BookDto
{
Title = entity.Title,
Year = entity.Year,
Isbn = entity.Isbn,
Price = entity.Price
};
}
var bookDto = book.MapToBookDto();
在将域实体的属性映射到时,我们需要手动设置 DTO 模型的所有属性。
这通常很烦人,这就是创建映射库的原因。这些库可自动执行映射过程并减少样板代码。从理论上讲,这些库应该将错误的风险降到最低,但现实恰恰相反。
为了说明为什么使用映射库不是 2024 年的最佳选择,首先我们需要看看这些库以及它们如何执行对象映射。
AutoMapper 是 .NET 中用于对象到对象映射的最流行的库之一。
若要将 AutoMapper 添加到项目,需要运行以下命令来安装 NuGet 包:
dotnet add package AutoMapper
让我们创建一个从实体到的映射:
public class Book
{
public Guid Id { get; set; }
public string Title { get; set; }
public int Year { get; set; }
public string Isbn { get; set; }
public decimal Price { get; set; }
public Author Author { get; set; }
}
public record BookDto
{
public string Isbn { get; set; }
public string Title { get; init; }
public int Year { get; init; }
public decimal Price { get; set; }
public string Author { get; set; }
}
首先,您需要配置 AutoMapper 如何映射这些对象。您需要创建一个从基类继承的映射类。
在此映射配置文件中,只需指定模型之间不同的字段。其他属性会自动映射。
public class BookProfile : Profile
{
public BookProfile()
{
CreateMap<Book, BookDto>()
.ForMember(dest => dest.Author, opt => opt.MapFrom(src => src.Author.Name));
CreateMap<BookDto, Book>()
.ForPath(dest => dest.Author.Name, opt => opt.MapFrom(src => src.Author));
}
}
BookDto与实体非常相似,但没有属性,并且名称为映射到子实体的字符串。
接下来,您需要在 DI 容器中注册 AutoMapper 及其配置文件。
builder.Services.AddAutoMapper(typeof(Program));
在该方法中,需要指定包含映射配置文件的程序集类型。
最后,要使用映射,需要使用接口并调用方法:
public class SomeService
{
private readonly IMapper _mapper;
public SomeService(IMapper mapper)
{
_mapper = mapper;
}
public BookDto ToBookDto(Book entity)
{
return _mapper.Map<BookDto>(entity);
}
}
Mapster 是 .NET 中另一个强大的对象映射库,以其高性能和灵活性而闻名。与 AutoMapper 相比,此库更新且更快。
若要安装 Mapster,请使用 NuGet 包管理器:
dotnet add package Mapster.DependencyInjection
Mapster 设置与 AutoMapper 非常相似。首先,通过从接口继承来创建配置文件:
public class BookProfile : IRegister
{
public void Register(TypeAdapterConfig config)
{
config
.NewConfig<Book, BookDto>()
.TwoWays()
.Map(dest => dest.Author, src => src.Author.Name);
}
}
然后在 DI 中注册 Mapster 并添加所有映射配置文件:
builder.Services.AddMapster();
TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly());
要使用 Mapster,请注入相同的接口,但来自另一个命名空间:
public class SomeService
{
private readonly IMapper _mapper;
public SomeService(IMapper mapper)
{
_mapper = mapper;
}
public BookDto ToBookDto(Book entity)
{
return _mapper.Map<BookDto>(entity);
}
}
Mapster还支持静态扩展方法,可用于任何对象。此方法执行不需要任何映射配置的自动映射:
var bookDto = bookEntity.Adapt<BookDto>();
您可以将此方法用于两个模型略有不同的简单映射。
因此,映射库似乎是一个不错的选择,但为什么这不是进行对象映射的最佳方法呢?让我们来了解一下吧!
映射库有很多优点。
尽管有这些优点,但映射库也有几个潜在的缺点:
那么,映射对象的最佳选择是什么?
这听起来可能令人震惊,但这是手动映射。但是我有一个有趣的补充。一起来看看吧!
首先,让我们探索博客文章应用程序的实体:
public class BlogPost
{
public required Guid Id { get; init; }
public required string Title { get; init; }
public required string Content { get; set; }
public required DateTime PublishedUtc { get; set; }
public required Guid PublisherId { get; set; }
public required Publisher Publisher { get; set; }
public required List\<BlogHistoryRecord> BlogHistoryRecords { get; init; } = [];
}
public class Publisher
{
public required Guid Id { get; init; }
public required string Email { get; init; }
public required string Name { get; init; }
public required string Role { get; init; }
public required List<BlogPost> BlogPosts { get; init; } = [];
}
public class BlogHistoryRecord
{
public required Guid Id { get; init; }
public required Guid BlogPostId { get; init; }
public required BlogPost BlogPost { get; init; }
public required DateTime Date { get; init; }
public required double Rating { get; init; }
}
我们有一个实体,一个代表系统中写博客的用户的实体。以及一个实体,用于保存每篇博客文章的所有用户评分的信息。
现在,让我们探讨从 webapi 返回的公共协定 (DTO) 模型:
public record BlogPostDto
{
public required string Url { get; init; }
public required string Title { get; init; }
public required string Content { get; init; }
public required DateOnly PublishedDate { get; init; }
public required PublisherDto Publisher { get; init; }
public required double Rating { get; init; }
}
public record PublisherDto
{
public required string Name { get; init; }
public required int TotalPosts { get; init; }
public required double Rating { get; init; }
}
这些模型具有有趣的功能:
现在,让我们为这些对象创建一个映射。首先,您需要创建一个静态类,该类是映射扩展方法的位置。
接下来,你需要定义映射方法,我喜欢用以下方式制作它们:
public static class BlogPostMappingExtensions
{
public static BlogPostDto MapToBlogPostDto(this BlogPost entity)
{
// ...
}
}
var blogPostDto = blogPost.MapToBlogPostDto();
此代码非常简单,因为您可以在阅读代码或调试时导航到该方法,并准确查看映射中发生的情况。使用映射库时,需要在整个解决方案中搜索映射配置文件或扩展,以了解映射是如何完成的。MapToBlogPostDto
让我们来探索一下完整的映射实现:
public static class BlogPostMappingExtensions
{
public static BlogPostDto MapToBlogPostDto(this BlogPost entity)
{
return new BlogPostDto
{
Url = entity.Id.ToString(),
Title = entity.Title,
Content = entity.Content,
PublishedDate = DateOnly.FromDateTime(entity.PublishedUtc),
Publisher = entity.Publisher.MapToPublisherDto(),
Rating = CalculateRating(entity.BlogHistoryRecords)
};
}
public static PublisherDto MapToPublisherDto(this Publisher entity)
{
var blogPostRatings = entity.BlogPosts
.SelectMany(x => x.BlogHistoryRecords)
.Select(x => x.Rating)
.ToList();
var averageRating = Math.Round(blogPostRatings.Average(), 2);
return new PublisherDto
{
Name = entity.Name,
TotalPosts = entity.BlogPosts.Count,
Rating = averageRating
};
}
private static double CalculateRating(List<BlogHistoryRecord> historyRecords)
{
return Math.Round(historyRecords.Average(record => record.Rating), 2);
}
}
让我们看看在 asp.net 核心最小 API 中返回 DTO 模型时的映射:
app.MapGet("/api/blogs", async (ApplicationDbContext dbContext) =>
{
var blogPosts = await dbContext.BlogPosts
.Include(b => b.Publisher)
.Include(b => b.BlogHistoryRecords)
.ToListAsync();
var blogPostDtos = blogPosts
.Select(x => x.MapToBlogPostDto())
.ToList();
return Results.Ok(blogPostDtos);
});
app.MapGet("/api/publishers", async (ApplicationDbContext dbContext) =>
{
var publishers = await dbContext.Publishers
.Include(b => b.BlogPosts)
.ThenInclude(b => b.BlogHistoryRecords)
.ToListAsync();
var publisherDtos = publishers
.Select(x => x.MapToPublisherDto())
.ToList();
return Results.Ok(publisherDtos);
});
如您所注意到,实体和 DTO 的所有属性都标记为必需。我发现这个秘密添加是对象映射中的游戏规则改变者。
每当您创建映射时,您都不会忘记映射属性。
让我们在实践中看看这一点。我们将修改实体并添加两个新属性:
public class BlogPost
{
// ...
public required string Description { get; set; }
public required string Category { get; set; }
}
假设我们忘记更新映射,让我们编译我们的应用程序:
我们的应用程序无法编译,我们会收到易于修复的编译错误列表。
我认为使用所需属性进行手动映射是在 2024 年进行对象映射的最佳方式。
让我们回顾一下为什么这种方法比使用映射库更好: