Object Mapping - Mapping.Mapster

Object Mapping - Mapping.Mapster

In projects, we often encounter object mapping, such as mapping between Model and Dto, or deep copying of objects, all of which need to be implemented by ourselves.

Last updated 7/6/2022 8:18 PM
磊_磊
9 min read
Category
.NET
Tags
.NET C#

Foreword

In projects, we often encounter object mapping, such as mapping between Model and Dto, or deep copying of objects, all of which need to be implemented by ourselves. At this point, the project will have a lot of code for initializing objects, which is quite tedious to write. Is there any way to reduce our workload so we can spend time on business functions?

Currently, object mapping frameworks in .NET with powerful features and excellent performance already exist. The most commonly used ones are:

When it comes to object mapping frameworks, most people think of AutoMapper. Many may not have even heard of Mapster, but there is no denying that Mapster is indeed a great object mapping framework. However, due to the lack of Chinese documentation, its popularity in China is not very high. Today, we will introduce what functionality Mapster provides, how to use it in projects, and what Masa's Mapster has done.

Introduction to Mapster

Mapster is an object mapping framework that is simple to use and powerful. It has been open source since 2014, now over 8 years. As of now, it has 2.6k stars on GitHub and maintains a release frequency of about 3 times per year. Its functionality is similar to AutoMapper, providing object-to-object mapping and supporting IQueryable-to-object mapping. Compared with AutoMapper, it performs better in terms of speed and memory usage, achieving a 4x performance improvement while using only 1/3 of the memory. Let's see how to use Mapster.

Preparation

Create a new console project Assignment.Mapster and install Mapster.

dotnet add package Mapster --version 7.3.0

Mapping to a new object

  1. Create a new class UserDto
public class UserDto
{
    public int Id { get; set; }

    public string Name { get; set; }

    public uint Gender { get; set; }

    public DateTime BirthDay { get; set; }
}
  1. Create an anonymous object as the source object to convert
var user = new
{
    Id = 1,
    Name = "Tom",
    Gender = 1,
    BirthDay = DateTime.Parse("2002-01-01")
};
  1. Map the source object user to the target object (UserDto)
var userDto = user.Adapt<UserDto>();
Console.WriteLine($"Mapping to new object, Name: {userDto.Name}");

Run the console program to verify the conversion:

Mapping to a new object

Data Types

In addition to object-to-object mapping, it also supports data type conversion, such as:

Primitive Types

  • Provides type mapping functionality, similar to Convert.ChangeType()
string res = "123";
decimal i = res.Adapt<decimal>(); // equal to (decimal)123;
Console.WriteLine($"Result: {i == int.Parse(res)}");

Run the console program:

Primitive type conversion

Enum Types

  • Maps enums to numeric types, and also supports string-to-enum and enum-to-string mapping, twice as fast as the default .NET implementation
var fileMode = "Create, Open".Adapt<FileMode>(); // equal to FileMode.Create | FileMode.Open
Console.WriteLine($"Result of enum type conversion: {fileMode == (FileMode.Create | FileMode.Open)}");

Run the console program to verify the conversion:

Enum type conversion

Queryable Extension

Mapster provides Queryable extensions for on-demand querying of DbContext, for example:

  1. Create a new class UserDbContext
using Assignment.Mapster.Domain;
using Microsoft.EntityFrameworkCore;

namespace Assignment.Mapster.Infrastructure;

public class UserDbContext : DbContext
{
    public DbSet<User> User { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var dataBaseName = Guid.NewGuid().ToString();
        optionsBuilder.UseInMemoryDatabase(dataBaseName); // Use in-memory database for easy testing
    }
}
  1. Create a new class User
public class User
{
    public int Id { get; set; }

    public string Name { get; set; }

    public uint Gender { get; set; }

    public DateTime BirthDay { get; set; }

    public DateTime CreationTime { get; set; }

    public User()
    {
        CreationTime = DateTime.Now;
    }
}
  1. Use the Queryable-based extension method ProjectToType
using (var dbContext = new UserDbContext())
{
    dbContext.Database.EnsureCreated();

    dbContext.User.Add(new User()
    {
        Id = 1,
        Name = "Tom",
        Gender = 1,
        BirthDay = DateTime.Parse("2002-01-01")
    });
    dbContext.SaveChanges();

    var userItemList = dbContext.User.ProjectToType<UserDto>().ToList();
}

Run the console program to verify the conversion:

Queryable Extension

In addition, Mapster also provides pre/post mapping processing, copy and merge, and nested mapping configuration support. See the documentation for details. Since Mapster is already so powerful, why not just use it directly? Why use Masa's Mapper?

What is Masa.Contrib.Data.Mapping.Mapster?

Masa.Contrib.Data.Mapping.Mapster is an object-to-object mapper based on Mapster. On top of the original Mapster, it adds automatic retrieval and use of the best constructor mapping, supports nested mapping, and reduces mapping workload.

Mapping Rules

  • When the target object has no constructor: use the default constructor, mapping to fields and properties.

  • When the target object has multiple constructors: retrieve the best constructor mapping.

Best constructor: Search constructors of the target object in descending order of parameter count. The parameter names must match (case-insensitive) and parameter types must match the source object's properties.

Preparation

Create a new console project Assignment.Masa.Mapster and install Masa.Contrib.Data.Mapping.Mapster and Microsoft.Extensions.DependencyInjection.

dotnet add package Masa.Contrib.Data.Mapping.Mapster --version 0.4.0-rc.4
dotnet add package Microsoft.Extensions.DependencyInjection --version 6.0.0
  1. Create a new class OrderItem
public class OrderItem
{
    public string Name { get; set; }

    public decimal Price { get; set; }

    public int Number { get; set; }

    public OrderItem(string name, decimal price) : this(name, price, 1)
    {

    }

    public OrderItem(string name, decimal price, int number)
    {
        Name = name;
        Price = price;
        Number = number;
    }
}
  1. Create a new class Order
public class Order
{
    public string Name { get; set; }

    public decimal TotalPrice { get; set; }

    public List<OrderItem> OrderItems { get; set; }

    public Order(string name)
    {
        Name = name;
    }

    public Order(string name, OrderItem orderItem) : this(name)
    {
        OrderItems = new List<OrderItem> { orderItem };
        TotalPrice = OrderItems.Sum(item => item.Price * item.Number);
    }
}
  1. Modify the Program class
using Assignment.Masa.Mapster.Domain.Aggregate;
using Masa.BuildingBlocks.Data.Mapping;
using Masa.Contrib.Data.Mapping.Mapster;
using Microsoft.Extensions.DependencyInjection;

Console.WriteLine("Hello Masa Mapster!");

IServiceCollection services = new ServiceCollection();
services.AddMapping();

var request = new
{
    Name = "Teach you to learn Dapr ……",
    OrderItem = new OrderItem("Teach you to learn Dapr hand by hand", 49.9m)
};
var serviceProvider = services.BuildServiceProvider();
var mapper = serviceProvider.GetRequiredService<IMapper>();
var order = mapper.Map<Order>(request);

Console.WriteLine($"{nameof(Order.TotalPrice)} is {order.TotalPrice}"); // Console outputs 49.9

Console.ReadKey();

If the conversion is successful, the value of TotalPrice should be 49.9. Let's run the console program to verify the conversion:

Mapping.Mapster

How It Works

We mentioned earlier that Masa.Contrib.Data.Mapping.Mapster can automatically retrieve and use the best constructor mapping to complete object-to-object mapping. So how does it achieve this? Does it have any performance impact?

The automatic retrieval and use of the best constructor mapping is achieved by using the constructor mapping functionality provided by Mapster. By specifying the constructor, object-to-object mapping is completed.

See the documentation

Summary

Currently, Masa.Contrib.Data.Mapping.Mapster has relatively limited functionality. Compared with Mapster, the current version only adds the ability to automatically retrieve and use the best constructor. This allows us to easily complete mapping for classes that have no default constructors but have multiple constructors, without needing to write an extra line of code.

However, I think the biggest advantage of Masa's Mapping is that the project depends on IMapper from BuildingBlocks, not Mapster. This decouples our project from the specific mapper implementation. If we are required to use AutoMapper in the project, we only need to implement the AutoMapper version of IMapper without changing too much business code—just replacing the referenced package. This is the charm of BuildingBlocks.

Source Code for This Chapter

Assignment04:https://github.com/zhenlei520/MasaFramework.Practice

Open Source Address

MASA.BuildingBlocks:https://github.com/masastack/MASA.BuildingBlocks

MASA.Contrib:https://github.com/masastack/MASA.Contrib

MASA.Utils:https://github.com/masastack/MASA.Utils

MASA.EShop:https://github.com/masalabs/MASA.EShop

MASA.Blazor:https://github.com/BlazorComponent/MASA.Blazor

If you are interested in our MASA Framework, whether it's code contributions, usage, or submitting issues, feel free to contact us.

Keep Exploring

Related Reading

More Articles