Click here to Skip to main content
15,671,149 members
Articles / Programming Languages / C#
Tip/Trick
Posted 29 Mar 2022

Stats

43.5K views
393 downloads
26 bookmarked

How to Easily Map EntityFramework/EntityFrameworkCore Entities from/to DTOs

Rate me:
Please Sign up or sign in to vote.
4.95/5 (6 votes)
27 May 2023MIT7 min read
To introduce a free library to save some tedious work for writing mapping code between entities and DTOs
This article introduces a free, AutoMapper-like library that helps .NET developers to easily map properties between entity POCOs for entity framework core and DTOs.

Introduction

With any 3-tier-architecture, it's the standard approach that server side retrieves data from databases using some OR mapping tool and sends them to browser/client, then browser/client does some manipulation to the data and then sends it back to the server to be updated back into the database. For most real world cases, the data objects retrieved from database are not the DTOs that get serialized and sent between server and browser/client, hence some kind of mapping must be provided between them, and writing such mapping code can be quite tedious and perhaps error-prone for developers.

Microsoft provides ILGenerator class in C#, which is designed for writing source code with source code. This is a handy feature and provides the possibility to leave some tedious code writing to computers instead of human developers. This article introduces a free library that adopts this technology to save .NET developers efforts for writing mapping code between POCOs and DTOs, if they work with Microsoft Entity Framework or Microsoft Entity Framework Core.

Background

As mentioned in the introduction, manually writing mapping code between POCOs and DTOs is not a preferred option. So what can be done to avoid this? One idea is to directly serialize the POCOs and send them on the internet. This is already possible as POCOs can be directly serialized to JSON and XML formats, but both these formats are known to be incredibly inefficient for the overheads to be transported over the internet. There must be much better ways to do so.

Google ProtoBuf is a popular tool for its simplicity and efficiency that solves the problems of JSON and XML. Its only problem is that it generates the source code which is not supposed to be manually updated, plus the generated classes use custom types for certain properties (like ByteString for byte[], and RepeatedField for ICollection<>) so that they may not be directly used as entity classes for Microsoft entity framework/Microsoft entity framework core.

Another solution could be protobufnet, with this library, it's certainly possible to directly format the POCOs with Google ProtoBuf. But there are still some details to consider:

  1. Users may not want to send all properties in a POCO, or want to send different properties of the same POCO under different situations, so directly serializing the POCO may not fulfill the requirement.
  2. Navigation properties of POCOs may cause infinite loops during serialization process (This is not verified with protobufnet, just raising a concern here).

Then a new direction solution should be proposed, automatically mapping properties between POCOs and DTOs comes into consideration, AutoMapper works fine in use cases of mapping properties between normal classes, but when database is involved, there is more to be concerned with:

  1. For one-to-many relationships, AutoMapper doesn't directly handle update or removal of navigation entities, some extra manual code including adding in new entities, update existing entities and delete removed entities is still required.
  2. As a workaround, users may consider attaching an empty entity with only id and timestamp to database context, then use AutoMapper to map the properties to save some efforts in updating existing data records in database, but this still needs some manual coding, and is inefficient at run time as all properties except id and timestamp will be considered updated which may not really be the case.
  3. AutoMapper requires users to manually create mapping between top level entities and sub entities in one-to-one and one-to-many relationships, this is tedious if there are a lot of such entities to be mapped.

Hence, something new is needed for this special case, and that's where EFMapper comes into the picture.

Basics

Let's demonstrate usage of the library with a very simple library system in pseudo code: a library system tracks borrowed books by borrowers. POCOs are defined as below:

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

In BorrowRecord class, apparently BorrowerId is foreign key to Borrower, and BookId is foreign key to Book, database context setup for such things is ignored here.

Without custom property mapper defined, EFMapper only maps public instance properties between classes if the properties have exactly the same property names, and exactly the same property types (or the property types are defined to be convertable, like in the sample code, WithScalarConverter method is used to define conversion between byte[] and ByteString). To elaborate it a bit, int BookId doesn't match int bookId; without any custom configuration, int BookId doesn't match int? bookId or long BookId. So to match the POCOs, the DTO classes are defined as below:

C#
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 string BorrowerName { get; set; }
}
public sealed class BorrowRecordDTO
{
    public int Id { get; set; }
    public int BorrowerId { get; set; }
    public int BookId { get; set; }
}

Assume we have the following books in database:

Id Name
1 Book 1
2 Book 2
3 Book 3

The user of Id 1 has borrowed book 1 and book 2, when querying database and sending DTO data to client, the code is like:

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

In the server, upon server start up, we need to build a mapper instance and make the mapper interface available anywhere in the server code:

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

We can ignore the details of RegisterTwoWay, "SomeName" or defaultCongfiguration for now, meaning of the relevant code is simply, we create a mapper builder, do some configuration (like registering mapping between Borrower and BorrowerDTO), then in our server, we can start to use the mapper to map an instance of Borrower to BorrowerDTO with two statements:

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

Then the DTO instance is ready to be sent to client/browser. So far, the library works like nothing but a weakened version of AutoMapper, its advantage will be demonstrated in the later part of this example. At client/browser side, the user wants to implement the business that the borrower has returned book 2 and borrowed book 3, so he/she operates to remove borrowing record for book 2 and add in borrowing record for book 3. In the mean time, the user notices that the borrower's name is wrongly typed, so he/she decides to fix it in the same batch:

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

Now the DTO is read to be sent back to server to be processed, and the server side should simply process it this way:

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

That's it! Updating scalar properties, adding or removing entities will be automatically handled by MapAsync method of the library. Just save the changes, it will work correctly.

This example shows that EFMapper is a dedicated mapper library designed for Microsoft entity framework/Microsoft entity framework core, and saves some real tedious work for writing the mapping code, plus handling navigation property addition/update/removal.

Custom Property Mapping

Note that in the code example in the last section, BookDTO has a BorrowerName property, EFMapper provides a way to let user define a custom mapping to assign value of the borrower name from the entity. IMapperBuilderFactory.MakeCustomPropertyMapperBuilder method is implemented for such use cases. Refer to the following code:

C#
var bookCustomMapper = factory.MakeCustomPropertyMapperBuilder<Book, BookDTO>()
    .MapProperty(
        dto => dto.BorrowerName,
        book => book.BorrowRecord != null && book.BorrowRecord.Borrower != null
            ? book.BorrowRecord.Borrower.Name
            : string.Empty)
    .Build();
mapperBuilder.Register<Book, BookDTO>(bookCustomMapper).Build();

The above code specifies that when mapping from Book to BookDTO, BorrowerName of BookDTO will be assigned the value of Book.BorrowerRecord.Borrower.Name if it's not null. The rest fields will be mapped by name and type as usual.

If there are multiple properties mappings to be customized like that, all the developer needs to do is to call MapProperty method once for each of such properties, before calling ICustomPropertyMapperBuilder.Build method.

Using the Code

More complete usage examples and test cases can be found in the sample code, in the unit test code and the document of this library at GitHub.

Note that the sample code used in this article follows .NET 6.0 style. The sample code for .NET Framework 4.5 will be slightly different but overall similar.

The packages are already available in nuget, the package for .NET Framework 4.5 and .NET standard 2.1 is Oasis.EntityFramework.Mapper, and the package for .NET 6.0 is Oasis.EntityFrameworkCore.Mapper.

Should there be any inquiry or suggestion, please leave a comment here or submit a bug under the repository.

Points of Interest

Currently, the library has some limitations, the most significant one is that this library requires existence of id property if the POCO is to be updated to database, plus, multiple properties combined as id is not supported.

History

  • 29th March, 2022: Initial submission
  • 3rd April, 2022: Added .NET Framework/.NET standard implementation
  • 23rd April, 2022: IMapper interface enhanced
  • 25th May, 2023: New version updated, custom property mapper is supported
  • 27th May, 2023: Updated code samples to new version for library version update

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Singapore Singapore
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionWhy use ilweaving? Pin
lmoelleb23-Apr-22 18:23
lmoelleb23-Apr-22 18:23 
AnswerRe: Why use ilweaving? Pin
David_Cui26-Apr-22 3:48
David_Cui26-Apr-22 3:48 
GeneralRe: Why use ilweaving? Pin
lmoelleb26-Apr-22 4:03
lmoelleb26-Apr-22 4:03 
GeneralRe: Why use ilweaving? Pin
David_Cui26-Apr-22 21:33
David_Cui26-Apr-22 21:33 
GeneralRe: Why use ilweaving? Pin
lmoelleb27-Apr-22 21:51
lmoelleb27-Apr-22 21:51 
GeneralRe: Why use ilweaving? Pin
David_Cui27-Apr-22 21:58
David_Cui27-Apr-22 21:58 
GeneralRe: Why use ilweaving? Pin
lmoelleb27-Apr-22 22:04
lmoelleb27-Apr-22 22:04 
GeneralRe: Why use ilweaving? Pin
David_Cui27-Apr-22 23:01
David_Cui27-Apr-22 23:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.