Click here to Skip to main content
15,884,388 members
Articles / Programming Languages / C#
Tip/Trick

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

Rate me:
Please Sign up or sign in to vote.
4.80/5 (11 votes)
22 Oct 2023MIT8 min read 79.7K   759   49   18
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 (Verified to be true for JsonSerializer.Serialize method).

Then to consider a tool which helps to automatically map properties between POCOs and DTOs, AutoMapper could work fine in use cases of mapping properties between normal classes. In use cases when database is involved, there is more to be concerned with:

  1. AutoMapper requires developers to manually specify all mappings. For database entities that can have some navigation properties in one-to-one and one-to-many relationships, this could be tedious.
  2. AutoMapper doesn't automatically handle certain database related mapping use cases, such as concurrency token for optimistic locking.
  3. AutoMapper isn't integrated with EntityFramework/EntityFrameworkCore, developers will have to do some manual add/attach work before or after mapping. For navigation properties of a list of entities, some manual coding to identify entities to be newly inserted, updated to existing or removed would be unavoidable.

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

Use Case: Insert into Database

To demonstrate the power of EFMapper, let's consider a very simple project-employee use case: a company has many projects and employees, each employee may be assigned to 1 project to work on, and each project may have 1 to many employee assigned. The company manages such information with a web or client/server application, operators remotely manipulate the data in their local PC and update the data to a server.

At first, the company has two projects, namely Project 1 and Project 2, and 5 employees, namely Employee 1 to 5. Employee 1 and 2 are assigned to Project 1, while Employee 3 and 4 are assigned to Project 2. Employee 5 is not assigned to any project. The following code will fill in the data to an empty database with relevant data tables already prepared:

C#
var project1 = new ProjectDTO { Name = "Project 1", 
                                Description = "Project 1 description." };
var project2 = new ProjectDTO { Name = "Project 2", 
                                Description = "Project 2 description." };
project1.Employees.Add(new EmployeeDTO { Name = "Employee 1", 
                                         Description = "Employee 1 description." });
project1.Employees.Add(new EmployeeDTO { Name = "Employee 2", 
                                         Description = "Employee 2 description." });
project2.Employees.Add(new EmployeeDTO { Name = "Employee 3", 
                                         Description = "Employee 3 description." });
project2.Employees.Add(new EmployeeDTO { Name = "Employee 4", 
                                         Description = "Employee 4 description." });
var employee5 = new EmployeeDTO { Name = "Employee 5", 
                                  Description = "Employee 5 description." };

var mapper = new MapperBuilderFactory()
    .Configure()
        .SetIdentityPropertyName("Name")
        .Finish()
    .MakeMapperBuilder()
    .Register<ProjectDTO, Project>()
    .Build()
    .MakeToDatabaseMapper();

await ExecuteWithNewDatabaseContext(async databaseContext =>
{
    mapper.DatabaseContext = databaseContext;
    _ = await mapper.MapAsync<ProjectDTO, Project>(project1, null);
    _ = await mapper.MapAsync<ProjectDTO, Project>(project2, null);
    _ = await mapper.MapAsync<EmployeeDTO, Employee>(employee5, null);
    _ = await databaseContext.SaveChangesAsync();
});

In the sample code, DTO classes are used to initialize the data first, then the DTO data is converted to POCO data by EFMapper, then inserted into database. It is actually easier to simply prepare the data directly using POCO classes, instead it's done this way as a simple proof that EFMapper can do data insertion (as data update is demonstrated in a later code piece).

The first 7 lines of the code above is preparing the data with DTO classes, nothing fancy to discuss about.

Then comes 8 lines of code preparing a mapper instance which maps ProjectDTO to Project, and EmployeeDTO to Employee. There are some points to highlight here:

  1. In this code example, identity properties (which map to primary keys of corresponding data tables) of both Employee and Project are a property of string type named "Name", so SetIdentityPropertyName configures the mapper to look for a property named "Name" to be the identity property by default for all POCOs.
  2. Note that when configuring the mapper, only mapping from ProjectDTO to Project is registered, that's because ProjectDTO has a navigation property named Employees of a class implementing ICollection<EmployeeDTO>, while Project has a navigation property of the same name of a type implementing ICollection<Employee>. EFMapper will find out about this and automatically register the mapping from EmployeeDTO to Employee. It's a mechanism of EFMapper to save developers some efforts listing all navigation properties of relevant classes.

Then the last 8 lines of code use the mapper to insert the new data into database easily. Note that ExecuteWithNewDatabaseContext is a helper function in the test code, the MapAsync method does the heavy lifting work in inserting into database.

So far, EFMapper seems to simply provide the feature that AutoMapper already provides. As a mapper library mapping properties from one type to another is like some must-have trivial feature. In the update use case, the sample code will demonstrate the key feature uniquely provided by EFMapper.

Use Case: Update into Database

Go on with the project-employee example. After the initial data is inserted into the database, the company wants to have a list of updates:

  1. Update Description of Project 1 to be "Almost done"
  2. Update Description of Employee 2 to be "Second Employee"
  3. Add a new project named "New Project", transfer Employee 2 to work on this project instead of his/her original one.
  4. Assign Employee 5 to the new project.
  5. Add a new employee name "New Employee", and assign him/her to the new project.
  6. Release Employee 4 from Project 2, make him/her not assigned to any project.

The code sample in this section simulates how a web or c/s application works.

First of all, projects and employees data should be retrieved from server and send to client.

C#
var mapper = new MapperBuilderFactory()
    .Configure()
        .SetIdentityPropertyName("Name")
        .Finish()
    .MakeMapperBuilder()
    .Register<Project, ProjectDTO>()
    .Build()
    .MakeToMemoryMapper();

var allData = new AllDataDTO();
await ExecuteWithNewDatabaseContext(async databaseContext =>
{
    foreach (var project in await databaseContext.Set<Project>().ToListAsync())
    {
        allData.Projects.Add(mapper.Map<Project, ProjectDTO>(project));
    }

    foreach (var employee in await databaseContext.Set<Employee>().ToListAsync())
    {
        allData.Employees.Add(mapper.Map<Employee, EmployeeDTO>(employee));
    }
});

return allData.ToByteArray();

Preparation of the mapper is simple, just this time, it's mapping from Project to ProjectDTO.

The sample code simply retrieves database data into POCOs, and maps them to be DTO data to be transferred.

Then when client/browser side receives the transferred data, it manipulates the data for the 6 updates mentioned.

C#
var allData = AllDataDTO.Parser.ParseFrom(content);
var p1 = allData.Projects.First(p => string.Equals("Project 1", p.Name));
var p2 = allData.Projects.First(p => string.Equals("Project 2", p.Name));
var e1 = allData.Employees.First(e => string.Equals("Employee 1", e.Name));
var e2 = allData.Employees.First(e => string.Equals("Employee 2", e.Name));
var e3 = allData.Employees.First(e => string.Equals("Employee 3", e.Name));
var e4 = allData.Employees.First(e => string.Equals("Employee 4", e.Name));
var e5 = allData.Employees.First(e => string.Equals("Employee 5", e.Name));
p1.Description = "Almost done";
p1.Employees.Add(e1);
p2.Employees.Add(e3);
var newProject = new ProjectDTO { Name = "New Project", 
                                  Description = "Project number 3" };
e2.Description = "Second Employee";
newProject.Employees.Add(e2);
newProject.Employees.Add(e5);
newProject.Employees.Add(new EmployeeDTO { Name = "New Employee", 
                                           Description = "Employee Number 6" });
allData.Projects.Add(newProject);

return allData.ToByteArray();

The code logic is very straight forward. After the DTO data reflects all the expected updates, the data is serialized and sent back to server to be processed.

C#
var factory = new MapperBuilderFactory()
    .Configure()
        .SetIdentityPropertyName("Name")
        .Finish()
    .MakeMapperBuilder()
    .Register<ProjectDTO, Project>()
    .Build();

var allData = AllDataDTO.Parser.ParseFrom(content);

await ExecuteWithNewDatabaseContext(async databaseContext =>
{
    var mapper = factory.MakeToDatabaseMapper(databaseContext);
    foreach (var project in allData.Projects)
    {
        _ = await mapper.MapAsync<ProjectDTO, Project>
                         (project, p => p.Include(p => p.Employees));
    }

    _ = await databaseContext.SaveChangesAsync();
});

There is nothing more to explain for mapper preparing. It's apparent that after receiving the data, with EFMapper, server side uses very simple code to apply all the changes to database. No trivial coding of finding entities, adding/attaching entities, removing some entities from a list, and add some to another is required. This is a feature which AutoMapper doesn't provide, and is the main purpose and core feature of EFMapper.

The sample code is one way to update the database as expected. There could be other ways like loading all assigned employees together with the projects and removing unassigned employees from the list instead of adding them to empty lists. It doesn't affect the way EFMapper works, developers can pick their favorite way to do this with EFMapper.

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 will be slightly different but overall similar.

The packages are already available in nuget, the package for .NET Framework 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.

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
  • 17th June, 2023: Updated code samples, to new version of library, fixed the bug that updating mapping with different concurrency token doesn't trigger database concurrency exception
  • 1st September, 2023: The library has been updated to version 0.7.1, some major updated has been done to downloadable samples and code sample part in the article
  • 3rd September, 2023: Updated downloadable samples for version 0.7.2
  • 1st October, 2023: Updated package version to 0.8.0, sample code updated a little
  • 8th October, 2023: Updated package version to 0.8.1
  • 22nd October, 2023: Updated package version to 0.8.2, minor feature addition and bug fixes

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

 
QuestionGood work and can be helpful Pin
Salam Y. ELIAS28-Jan-24 3:37
professionalSalam Y. ELIAS28-Jan-24 3:37 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA22-Oct-23 4:07
professionalȘtefan-Mihai MOGA22-Oct-23 4:07 
GeneralRe: My vote of 5 Pin
David_Cui22-Oct-23 13:51
David_Cui22-Oct-23 13:51 
QuestionHow would you handle migration? Pin
Niels Peter Gibe9-Oct-23 2:37
Niels Peter Gibe9-Oct-23 2:37 
AnswerRe: How would you handle migration? Pin
David_Cui10-Oct-23 20:12
David_Cui10-Oct-23 20:12 
GeneralRe: How would you handle migration? Pin
Niels Peter Gibe11-Oct-23 3:03
Niels Peter Gibe11-Oct-23 3:03 
GeneralRe: How would you handle migration? Pin
David_Cui11-Oct-23 13:31
David_Cui11-Oct-23 13:31 
QuestionUsing auto-mappers are slow Pin
Graeme_Grant13-Sep-23 15:26
mvaGraeme_Grant13-Sep-23 15:26 
AnswerRe: Using auto-mappers are slow Pin
David_Cui15-Sep-23 2:10
David_Cui15-Sep-23 2:10 
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 
AnswerRe: Why use ilweaving? Pin
Graeme_Grant13-Sep-23 15:26
mvaGraeme_Grant13-Sep-23 15:26 

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.