Learn AutoMapper

内容纲要

Intro to AutoMapper

You can install it by searching in Nuget. The current version used by the author is 10.1.1, and the assembly of AutoMapper is about 280KB.

The main function of AutoMapper is to map the value of one object’s field to the corresponding field of another object. AutoMapper should be familiar to everyone, so I won’t go into details here.

Basic use of AutoMapper

If two types are as follows:

    public class TestA
    {
        public int A {get; set;}
        public string B {get; set;}
        // The remaining 99 fields are omitted

    }

    public class TestB
    {
        public int A {get; set;}
        public string B {get; set;}
        // The remaining 99 fields are omitted
    }

We can quickly copy the values ​​of all fields in TestA to TestB through AutoMapper.

Create a mapping configuration from TestA to TestB:

            MapperConfiguration configuration = new MapperConfiguration(cfg =>
            {
                // TestA -> TestB
                cfg.CreateMap<TestA, TestB>();
            });

Create the mapper:

            IMapper mapper = configuration.CreateMapper();

Use the .Map() method to copy the value of the field in TestA to TestB.

            TestA a = new TestA();

            TestB b = mapper.Map<TestB>(a);

Mapping configuration

Above we used cfg.CreateMap<TestA, TestB>(); to create a mapping from TestA to TestB. Without configuration, AutoMapper will map all fields by default.

Of course, we can define the mapping logic for each field in MapperConfiguration.

The constructor of MapperConfiguration is defined as follows:

public MapperConfiguration(Action<IMapperConfigurationExpression> configure);

The IMapperConfigurationExpression is a chained function that can define logic for each field in the mapping.

Modify the above model class to the following code:

    public class TestA
    {
        public int A {get; set;}
        public string B {get; set;}

        public string Id {get; set;}
    }

    public class TestB
    {
        public int A {get; set;}
        public string B {get; set;}
        public Guid Id {get; set;}
    }

Create the mapping expression as follows:

            MapperConfiguration configuration = new MapperConfiguration(cfg =>
            {
                // TestA -> TestB
                cfg.CreateMap<TestA, TestB>()
                // On the left is the field of TestB, and on the right is the logic for assigning values ​​to the field
                .ForMember(b => b.A, cf => cf.MapFrom(a => a.A))
                .ForMember(b => b.B, cf => cf.MapFrom(a => a.B))
                .ForMember(b => b.Id, cf => cf.MapFrom(a => Guid.Parse(a.Id)));
            });

The .ForMember() method is used to create the mapping logic of a field. There are two expressions ({expression}, {expression2}), where expression 1 represents the field mapped by TestB; expression 2 represents Where does the value of this field come from.

There are several commonly used mapping sources for expression 2:

  • .MapFrom() is obtained from TestA;
  • .AllowNull() set a null value;
  • .Condition() conditionally map;
  • .ConvertUsing() type conversion;

Here the author demonstrates how to use .ConvertUsing():

cfg.CreateMap<string, Guid>().ConvertUsing(typeof(GuidConverter));

This can convert string to Guid, where GuidConverter is the converter that comes with .NET, and we can also customize the converter.

Of course, even if no converter is defined, string can be converted to Guid by default because AutoMapper is smarter.

For other content, I won’t repeat it here, and you can consult the document if you are interested.

Mapping check

If there is no field in TestA, TestB, it will not be copied; if there is no field in TestA in TestB, this field will not be processed (initialized value).

By default, if the fields in TestA and TestB are not consistent, there may be some places that are easy to be ignored. Developers can use the checker to check.

Just call after defining MapperConfiguration and mapping relationship:

configuration.AssertConfigurationIsValid();

This check method should only be used under Debug.

When the mapping is not overwritten

You can add a D field to TestB, and then start the program, it will prompt:

AutoMapper.AutoMapperConfigurationException

Because of the D field in TestB, there is no corresponding mapping. In this way, when we are writing the mapping relationship, we can avoid missing values.

Performance

When you first used AutoMapper, you might be thinking about the principle of AutoMapper, reflection? How is the performance?

Here we write an example to test it with BenchmarkDotNet.

Define TestA:

    public class TestB
    {
        public int A {get; set;}
        public string B {get; set;}
        public int C {get; set;}
        public string D {get; set;}
        public int E {get; set;}
        public string F {get; set;}
        public int G {get; set;}
        public string H {get; set;}
    }

Define the attributes of TestB as above.

    [SimpleJob(runtimeMoniker: RuntimeMoniker.NetCoreApp31)]
    public class Test
    {
        private static readonly MapperConfiguration configuration = new MapperConfiguration(cfg =>
        {
            cfg.CreateMap<TestA, TestB>();
        });
        private static readonly IMapper mapper = configuration.CreateMapper();

        private readonly TestA a = new TestA
        {
            A = 1,
            B = "aaa",
            C = 1,
            D = "aaa",
            E = 1,
            F = "aaa",
            G = 1,
            H = "aaa",
        };
        [Benchmark]
        public TestB Get1()
        {
            return new TestB {A = a.A, B = a.B, C = a.C, D = a.D, E = a.E, F = a.F, G = a.G, H = a.H };
        }
        [Benchmark]
        public TestB Get2()
        {
            return mapper.Map<TestB>(a);
        }
        [Benchmark]
        public TestA Get3()
        {
            return mapper.Map<TestA>(a);
        }
    }

The test results are as follows:

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-3740QM CPU 2.70GHz (Ivy Bridge), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=5.0.200-preview.20601.7
  [Host]: .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT.NET Core 3.1: .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT

Job=.NET Core 3.1 Runtime=.NET Core 3.1

| Method | Mean | Error | StdDev |
|------- |----------:|---------:|---------:|
| Get1 | 16.01 ns | 0.321 ns | 0.284 ns |
| Get2 | 204.63 ns | 3.009 ns | 2.349 ns |
| Get3 | 182.53 ns | 2.215 ns | 2.072 ns |

    Outliers
  Test.Get1: .NET Core 3.1 -> 1 outlier was removed (25.93 ns)
  Test.Get2: .NET Core 3.1 -> 3 outliers were removed (259.39 ns..320.99 ns)

It can be seen that the performance difference is 10 times.

In situations such as increasing flexibility, some performance will be sacrificed, and there will not be too much performance problems when it is mainly not a large amount of calculation.

Profile configuration

In addition to MapperConfiguration, we can also define the mapping configuration by inheriting Profile to achieve smaller-grained control and modularization. It is this method of AutoMapper recommended in the ABP framework to cooperate with modularization.

Examples are as follows:

    public class MyProfile: Profile
    {
        public MyProfile()
        {
            // I won’t go into details here
            base.CreateMap<TestA, TestB>().ForMember(... ...);
        }
    }

If we use ABP, then each module can define a Profiles folder and define some Profile rules in it.

A kind of mapping defines a Profile class? This is a waste of space; a module defines a Profile class? This is too complicated. Different programs have their own architectures, so choose the profile granularity according to the project architecture.

Dependency Injection

AutoMapper dependency injection is very simple. We learned that Profile defines configuration mapping, so that we can use dependency injection framework to handle mapping easily.

We inject in StartUp of ASP.NET Core or IServiceCollection of ConsoleApp:

services.AddAutoMapper(assembly1, assembly2 /*, ...*/);

AutoMapper will automatically scan the types in the assembly (Assembly) and extract the types that inherit Profile.

If you want to control AutoMapper with a smaller granularity, you can use:

services.AddAutoMapper(type1, type2 /*, ...*/);

The life cycle of AutoMapper registered by .AddAutoMapper() is transient.

If you don’t like Profile, you can continue to use the previous MapperConfiguration, the sample code is as follows:

 MapperConfiguration configuration = new MapperConfiguration(cfg =>
        {
            cfg.CreateMap<TestA, TestB>();
        });

services.AddAutoMapper(configuration);

After that, we can use AutoMapper through dependency injection, in the form of IMapper:

public class HomeController {
private readonly IMapper _mapper;

public HomeController(IMapper mapper)
    {
        _mapper = mapper;
    }
}

IMapper has a .ProjectTo<>() method that can help deal with IQueryable queries.

List<TestA> a = new List<TestA>();
... ...
_ = mapper.ProjectTo<TestB>(a.AsQueryable()).ToArray();

or:

_ = a.AsQueryable().ProjectTo<TestB>(configuration).ToArray();

You can also configure EFCore to use:

            _ = _context.TestA.ProjectTo<TestB>(configuration).ToArray();

            _ = _context.TestA.ProjectTo<TestB>(mapper.ConfigurationProvider).ToArray();

Expression and DTO

AutoMapper has many extensions. Here I introduce AutoMapper.Extensions.ExpressionMapping, which can be searched in Nuget.

AutoMapper.Extensions.ExpressionMapping This extension implements a large number of expression tree queries. This library implements the IMapper extension.

if:

    public class DataDBContext: DbContext
    {
        public DbSet<TestA> TestA {get; set;}
    }

... ...
    DataDBContext data = ... ...

Configuration:

        private static readonly MapperConfiguration configuration = new MapperConfiguration(cfg =>
        {
            cfg.AddExpressionMapping();
            cfg.CreateMap<TestA, TestB>();
        });

Suppose, you want to implement the filtering function:

            // It's of no use
            Expression<Func<TestA, bool>> filter = item => item.A> 0;
            var f = mapper.MapExpression<Expression<Func<TestA, bool>>>(filter);
            var someA = data.AsQueryable().Where(f); // data is _context or conllection

Of course, this code is of no use.

You can implement custom extension methods, expression trees, and operate DTOs more conveniently.

Here is an example:

    public static class Test
    {
        // It's of no use
        //public static TB ToType<TA, TB>(this TA a, IMapper mapper, Expression<Func<TA, TB>> func)
        //{
        // //Func<TA, TB> f1 = mapper.MapExpression<Expression<Func<TA, TB>>>(func).Compile();
        // //TB result = f1(a);

        // return mapper.MapExpression<Expression<Func<TA, TB>>>(func).Compile()(a);
        //}

        public static IEnumerable<TB> ToType<TA, TB>(this IEnumerable<TA> list, IMapper mapper, Expression<Func<TA, TB>> func)
        {
            var one = mapper.MapExpression<Expression<Func<TA, TB>>>(func).Compile();
            List<TB> bList = new List<TB>();
            foreach (var item in list)
            {
                bList.Add(one(item));
            }
            return bList;
        }
    }

When you query, you can use this extension like this:

_ = _context.TestA.ToArray().ToType(mapper, item => mapper.Map<TestB>(item));
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注