使用EntityFramework/EntityFrameworkCore时如何在实体和DTO之间轻松映射

目录

介绍

背景

使用代码

兴趣点


介绍

对于任何3-tier-architecture,这是服务器端使用一些OR映射工具从数据库中检索数据并将它们发送到浏览器/客户端,然后浏览器/客户端对数据进行一些操作然后将其发送回服务器的标准方法更新回数据库。对于大多数现实世界的情况,从数据库中检索的数据对象并不是在服务器和浏览器/客户端之间进行序列化和发送的DTO,因此必须在它们之间提供某种映射,编写这样的映射代码可能非常繁琐,而且对于开发人员来说可能容易出错。

微软提供了C#中的ILGenerator类,专为用源代码编写源代码而设计。这是一个方便的功能,并提供了将一些繁琐的代码编写给计算机而不是人类开发人员的可能性。本文介绍了一个采用该技术的免费库,以节省.NET开发人员在使用Microsoft实体框架Microsoft实体框架核心时编写POCODTO之间的映射代码的工作量。

背景

正如介绍中提到的,手动编写POCODTO之间的映射代码不是首选方案。那么可以做些什么来避免这种情况呢?一种想法是直接序列化POCO并将它们发送到互联网上。这已经成为可能,因为POCO可以直接序列化为JSONXML格式,但是众所周知,这两种格式对于通过Internet传输的开销都非常低效。必须有更好的方法来做到这一点。

Google ProtoBuf是一个流行的工具,它解决了JSONXML的问题。它唯一的问题是它生成的源代码不应该手动更新,而且生成的类对某些属性(如byte[]ByteStringICollection<>RepeatedField)使用自定义类型,因此它们可能不会直接用作Microsoft实体的实体类框架/微软实体框架核心

另一种解决方案可能是protobufnet,使用这个库,当然可以使用Google ProtoBuf直接格式化POCO。但仍有一些细节需要考虑:

  1. 用户可能不想发送一个POCO中的所有属性,或者想在不同情况下发送同一个POCO的不同属性,因此直接序列化POCO可能无法满足需求。
  2. POCO的导航属性可能会在序列化过程中导致无限循环(这没有通过protobufnet验证,只是在这里提出了一个问题)。

那么应该提出一个新的方向解决方案,考虑到POCODTO之间的属性自动映射,AutoMapper在普通类之间映射属性的用例中运行良好,但是当涉及到数据库时,需要关注更多:

  1. 对于一对多关系,AutoMapper不直接处理导航实体的更新或删除,仍然需要一些额外的手动代码,包括添加新实体、更新现有实体和删除已删除实体。
  2. 作为一种解决方法,用户可以考虑将一个只有id和时间戳的空实体附加到数据库上下文,然后使用AutoMapper映射属性以节省更新数据库中现有数据记录的一些工作,但这仍然需要一些手动编码,效率低下在运行时,除了idtimestamp之外的所有属性都将被视为已更新,但实际情况可能并非如此。
  3. AutoMapper需要用户手动创建顶级实体和子实体之间的一对一和一对多关系的映射,如果要映射的此类实体很多,这很繁琐。

因此,这种特殊情况需要一些新的东西,这就是EFMapper出现的地方。

使用代码

让我们用伪代码中的一个非常简单的图书馆系统来演示库的使用:图书馆系统跟踪借书人的借书。POCO定义如下:

public sealed class Borrower
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<BorrowRecord>? BorrowRecords { get; set; }
}
public sealed class Book
{
    public int Id { get; set; }
    public string Name { get; set; }
    public BorrowRecord? BorrowRecord { get; set; }
}
public sealed class BorrowRecord
{
    public int Id { get; set; }
    public int BorrowerId { get; set; }
    public int BookId { get; set; }
    
    public Borrower? Borrower { get; set; }
    public Book? Book { get; set; }
}

BorrowRecord类上,显然BorrowerIdBorrower的外键,并且BookIdBook的外键,这里忽略了用于此类事情的数据库上下文设置。

请注意,如果属性具有完全相同的属性名称和完全相同的属性类型,EFMapper只在类之间映射公共实例属性(或者属性类型被定义为可转换的,就像在示例代码中,WithScalarConverter方法用于定义byte[]ByteString之间的转换) 详细说明一下,intBookIdint bookId 不匹配;没有任何自定义配置,int BookId不匹配int bookIdlong BookId。因此,为了匹配POCODTO类定义如下:  

public sealed class BorrowerDTO
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<BorrowRecordDTO>? BorrowRecords { get; set; }
}
public sealed class BookDTO
{
    public int Id { get; set; }
    public string Name { get; set; }
}
public sealed class BorrowRecordDTO
{
    public int Id { get; set; }
    public int BorrowerId { get; set; }
    public int BookId { get; set; }
}

假设我们在数据库中有以下书籍:

ID

姓名

1

Book 1

2

Book 2

3

Book 3

用户Id 1借用了book 1book 2,在查询数据库并向客户端发送DTO数据时,代码如下:

var borrowerInfo = await databaseContext.Set<Borrower>().AsNoTracking().Include
                   (b => b.BorrowRecords).FirstAsync(b => b.Id == 1);

在服务器中,在服务器启动时,我们需要构建一个映射器实例,并使映射器接口在服务器代码中的任何位置都可用:

var factory = new MapperBuilderFactory();
var mapperBuilder = factory.Make("SomeName", defaultConfiguration);
mapperBuilder.RegisterTwoWay<Borrower, BorrowerDTO>();
var mapper = mapperBuilder.Build();

现在我们可以忽略RegisterTwoWay, "SomeName"或者defaultCongfiguration的细节,相关代码的意思很简单,我们创建一个映射器构建器,做一些配置(比如注册BorrowerBorrowerDTO之间的映射),然后在我们的服务器中,我们可以开始使用映射器用两个语句映射BorrowerBorrowerDTO的实例:

var borrowerDTO = mapper.Map<Borrower, BorrowerDTO>(entity);

然后DTO实例已准备好发送到客户端/浏览器。到目前为止,该库的工作方式不过是AutoMapper的弱化版本,其优势将在本示例的后面部分展示。在客户端/浏览器端,用户希望实现借阅人还book 2和借book 3的业务,因此操作删除book 2的借阅记录,添加book 3的借阅记录。同时,用户注意到借款人的姓名输入错误,因此他/她决定在同一批次中进行修复:

borrowerDTO.Name = "Updated Name";
var book2BorrowingRecord = borrowerDTO.BorrowRecords.Single(r => r.BookId == 2);
borrowerDTO.BorrowRecords.Remove(book2BorrowingRecord);
borrowerDTO.BorrowRecords.Add(new BorrowRecordDTO { BookId = 3 });

现在读取DTO以将其发送回服务器进行处理,服务器端应该以这种方式简单地处理它:

var borrower = await mapper.MapAsync<BorrowerDTO, Borrower>
               (borrowerDTO, databaseContext, qb => qb.Include(qb => qb.BorrowRecords))
await databaseContext.SaveChangesAsync();

就是这样!更新标量属性,添加或删除实体将由MapAsync库的方法自动处理。只需保存更改,它将正常工作。

这个例子说明FMapper是一个专为微软实体框架/微软实体框架核心设计的映射器库,省去了一些真正繁琐的写映射代码的工作,加上处理导航属性的添加/更新/删除。

请注意,本文中使用的示例代码遵循.NET 6.0样式。.NET Framework 4.5的示例代码将略有不同,但总体上相似。

完整的演示代码请下载附件。

这些包已经在nuget中可用,.NET Framework 4.5.NET Standard 2.1的包是Oasis.EntityFramework.Mapper.NET 6.0的包是Oasis.EntityFrameworkCore.Mapper

兴趣点

目前,该库有一些限制,最重要的是该库需要存在id属性才能将POCO更新到数据库,另外,不支持多个属性组合为id

有关此库的更多信息,请访问Github

如果有任何疑问或建议,请在此处发表评论或在存储库下提交错误。

https://www.codeproject.com/Tips/5328579/How-to-Easily-Map-between-Entities-and-DTOs-when-u

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值