2024 年在 .Net 中映射对象的最佳方式

作者:微信公众号:【架构师老卢】
6-25 8:9
33

概述:在今天的文章中,你将学习如何使用各种技术和库在 .NET 中映射对象。我们将探讨 2024 年在 .NET 中映射对象的最佳方式。什么是对象映射什么是对象映射,为什么在 ASP.NET Core 应用程序中需要对象映射?对象映射是对象从一种类型到另一种类型的转换,通常是在应用程序的不同层之间。映射对于分离关注点至关重要以下是使用对象映射的几个原因:关注点分离: 保持业务逻辑和数据传输逻辑的区别。性能: 减小通过网络传输的对象的大小,仅发送客户端所需的必要数据。安全性: 对客户端隐藏私有域数据。例如,真实的实体 ID、个人数据和内部域属性。可维护性: 简化数据结构的更新和更改,因为域模型与客户端

在今天的文章中,你将学习如何使用各种技术和库在 .NET 中映射对象。我们将探讨 2024 年在 .NET 中映射对象的最佳方式。

什么是对象映射

什么是对象映射,为什么在 ASP.NET Core 应用程序中需要对象映射

对象映射是对象从一种类型到另一种类型的转换,通常是在应用程序的不同层之间。

映射对于分离关注点至关重要

以下是使用对象映射的几个原因:

  • 关注点分离: 保持业务逻辑和数据传输逻辑的区别。
  • 性能: 减小通过网络传输的对象的大小,仅发送客户端所需的必要数据。
  • 安全性: 对客户端隐藏私有域数据。例如,真实的实体 ID、个人数据和内部域属性。
  • 可维护性: 简化数据结构的更新和更改,因为域模型与客户端使用的公共契约不同。

在我的网站 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 库进行映射

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 库进行制图

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>();

您可以将此方法用于两个模型略有不同的简单映射。

因此,映射库似乎是一个不错的选择,但为什么这不是进行对象映射的最佳方法呢?让我们来了解一下吧!

为什么使用映射库不是灵丹妙药

映射库有很多优点。

尽管有这些优点,但映射库也有几个潜在的缺点:

  1. 性能开销 — 映射库通常使用反射在运行时检查和映射对象属性。这可能会带来性能开销,尤其是在高性能应用程序中或处理复杂或大量数据时。
  2. 复杂配置 — 虽然映射库旨在简化映射过程,但它们有时会导致复杂的配置,尤其是对于高级方案。配置自定义映射、值解析程序和类型转换器可能会变得繁琐且容易出错。
  3. 调试挑战 — 使用映射库时,调试映射问题可能具有挑战性。错误可能仅在运行时出现,从而更难跟踪和修复问题。
  4. 容易出错 — 如果您曾经在实际应用程序中使用过映射库,我确信您遇到了运行时错误,只是因为您在向映射对象添加新属性后忘记更新映射配置文件。

那么,映射对象的最佳选择是什么?

这听起来可能令人震惊,但这是手动映射。但是我有一个有趣的补充。一起来看看吧!

2024 年在 .NET 中进行映射的最佳方式

首先,让我们探索博客文章应用程序的实体:

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; }  
}

这些模型具有有趣的功能:

  • BlogPost具有代表平均博客评分的属性,并考虑了所有用户评论Rating
  • PublisherDtohas 和 属性。该属性代表所有发布商博客的平均博客评分,并考虑了所有用户评论TotalPostsRatingRating

现在,让我们为这些对象创建一个映射。首先,您需要创建一个静态类,该类是映射扩展方法的位置。

接下来,你需要定义映射方法,我喜欢用以下方式制作它们:

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 年进行对象映射的最佳方式。

让我们回顾一下为什么这种方法比使用映射库更好:

  • 按照这种方法,在阅读代码时,您可以准确地看到映射中发生了什么。您无需在整个应用程序中搜索映射类即可了解库如何执行映射魔术。
  • 你有代码安全。如果忘记更新映射方法,则会引发编译器错误。
  • 您可以完全控制映射过程,无需花时间学习如何在库中进行花哨的映射操作。
  • 这种方法的性能要高得多,因为在运行时不需要反射
  • 调试很简单。您是否曾经尝试过在调试映射库时单步执行映射配置文件中的断点?这真的很难或几乎不可能做到。忘记这个问题,进行无压力的调试。
阅读排行