Introduction to AutoMapper

2020年12月19日 72点热度 0人点赞 1条评论
内容目录

Introduction to AutoMapper

You can install it by searching in NuGet. The version I'm currently using is 10.1.1, and the AutoMapper assembly is approximately 280KB.

AutoMapper's primary function is to map the values of fields from one object to the corresponding fields of another object. AutoMapper should be familiar to everyone, so I won't elaborate further.

Basic Usage of AutoMapper

Let's assume we have two types as follows:

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

public class TestB
{
    public int A { get; set; }
    public string B { get; set; }
    // Remaining 99 fields omitted
}</code></pre>

We can quickly copy all the field values from TestA to TestB using AutoMapper.

To 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 values of the fields from TestA to TestB.

            TestA a = new TestA();
        TestB b = mapper.Map&lt;TestB&gt;(a);</code></pre>

Mapping Configuration

In the previous example, we created a mapping from TestA to TestB using cfg.CreateMap<TestA, TestB>();. By default, AutoMapper will map all fields if no configuration is provided.

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

The constructor for MapperConfiguration is defined as follows:

public MapperConfiguration(Action<IMapperConfigurationExpression> configure);

This IMapperConfigurationExpression is a fluent function that allows you to define logic for each field in the mapping.

Let's modify the model classes as follows:

    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; }
}</code></pre>

Creating the mapping expression looks like this:

            MapperConfiguration configuration = new MapperConfiguration(cfg =>
            {
                // TestA -> TestB
                cfg.CreateMap<TestA, TestB>()
                // Left side represents TestB's fields, right side defines the logic for assigning values
                .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 mapping logic for a field, with two expressions ({expression1}, {expression2}), where expression1 represents the field in TestB to be mapped, and expression2 represents the logic determining where the value comes from.

There are several common sources for expression2:

  • .MapFrom() takes from TestA;
  • .AllowNull() sets null values;
  • .Condition() conditionally maps;
  • .ConvertUsing() for type conversions;

Here, I demonstrate how to use .ConvertUsing():

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

This allows converting a string to a Guid, with GuidConverter being a built-in converter in .NET. We can also create custom converters.

Of course, even without defining a converter, a string can be converted to a Guid by default because AutoMapper is quite smart.

Other details aren't discussed here; feel free to check the documentation for more information.

Mapping Validation

If TestA has fields that TestB does not, then those fields will not be copied; conversely, if TestB has fields that TestA does not, those fields will be left uninitialized.

By default, if the fields in TestA and TestB do not match well, there may be issues that cause some fields to be overlooked. Developers can use a validator to verify mapping.

Simply call:

configuration.AssertConfigurationIsValid();

This validation method should only be used in Debug mode.

When mappings have not been overridden

You can add a D field in TestB and then run the program; an error will be raised:

AutoMapper.AutoMapperConfigurationException

This happens because the D field in TestB has no corresponding mapping. This way, while we define the mapping relationships, we can avoid missing values.

Performance

When first using AutoMapper, one might wonder about its underlying principles, reflection, and performance?

Here, we will write an example and test it using 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 properties for TestB similarly.

    [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 = &quot;aaa&quot;,
        C = 1,
        D = &quot;aaa&quot;,
        E = 1,
        F = &quot;aaa&quot;,
        G = 1,
        H = &quot;aaa&quot;,
    };
    [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&lt;TestB&gt;(a);
    }
    [Benchmark]
    public TestA Get3()
    {
        return mapper.Map&lt;TestA&gt;(a);
    } 
}</code></pre>

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)

As we can see, the performance difference is about 10 times.

In scenarios where flexibility is increased, some performance is sacrificed; however, this does not pose significant performance issues in non-computationally heavy scenarios.

Profile Configuration

In addition to MapperConfiguration, we can also define mapping configurations using inherited Profiles for finer control and modularity. This approach is recommended in the ABP framework for modularization with AutoMapper.

Below is an example:

    public class MyProfile : Profile
    {
        public MyProfile()
        {
            // Details omitted
            base.CreateMap<TestA, TestB>().ForMember(... ...);
        }
    }

If we are using ABP, each module can define a Profiles folder to implement some Profile rules.

Is it too wasteful to define one Profile class for one mapping? Or too cluttered to define one Profile class per module? Different applications have their own architectures, so it's best to choose the granularity of Profiles according to the project architecture.

Dependency Injection

Dependency injection with AutoMapper is straightforward. We previously learned how to define mapping configurations using Profiles, which means we can conveniently use dependency injection frameworks for handling mappings.

We inject into ASP.NET Core's StartUp or ConsoleApp's IServiceCollection:

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

AutoMapper will automatically scan the types in the assembly and extract those that inherit from Profile.

If you want more granular control over AutoMapper, you can use:

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

The AutoMapper registered via .AddAutoMapper() has a lifecycle of transient.

If you prefer not to use Profiles, you can stick with the previous MapperConfiguration approach, as shown in the following example:

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

services.AddAutoMapper(configuration);

We can then utilize AutoMapper via dependency injection using the IMapper type:

public class HomeController {
    private readonly IMapper _mapper;
public HomeController(IMapper mapper)
{
    _mapper = mapper;
}

}

The IMapper interface has a .ProjectTo<>() method that helps handle 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, using:

            _ = _context.TestA.ProjectTo<TestB>(configuration).ToArray();
        _ = _context.TestA.ProjectTo&lt;TestB&gt;(mapper.ConfigurationProvider).ToArray();</code></pre>

Expressions and DTO

AutoMapper has many extensions. Here, I will introduce AutoMapper.Extensions.ExpressionMapping, which can be found in NuGet.

AutoMapper.Extensions.ExpressionMapping implements a large number of expression tree queries, and this library extends IMapper implementation.

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>();
        });

If you want to implement a filtering feature:

            // 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 collection

Of course, this piece of code is of no use.

You can implement custom extension methods and expression trees to operate on 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&lt;Expression&lt;Func&lt;TA, TB&gt;&gt;&gt;(func).Compile()(a);
    //}

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

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

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

痴者工良

高级程序员劝退师

文章评论