Open Source Object Mapping Framework | A Small Framework Suitable for Learning

2023年10月15日 34点热度 0人点赞 0条评论
内容目录

Maomi.Mapper

Project address: https://github.com/whuanle/Maomi.Mapper

Note: This project is for educational purposes only and has poor performance; do not use it in production environments.

MaomiMapper is a framework that constructs object member mappings using expression trees, serving as an object mapping framework.

Although MaomiMapper's performance leaves much to be desired, the code is well-documented, making it suitable for readers to study reflection, expression trees, type conversions, and more.

Comparison of MaomiMapper and AutoMapper:

| Method | Mean | Error | StdDev | Gen0 | Allocated |
| -------------- | --------- | -------- | -------- | ------ | --------- |
| ASAutoMapper | 146.30 ns | 1.759 ns | 1.645 ns | 0.0362 | 304 B |
| ASMaomiMapper | 817.46 ns | 6.467 ns | 6.049 ns | 0.0935 | 784 B |
| ASDelegate | 668.56 ns | 5.050 ns | 4.724 ns | 0.0839 | 704 B |
| _AutoMapper | 67.56 ns | 0.438 ns | 0.410 ns | 0.0191 | 160 B |
| _MaomiMapper | 242.03 ns | 0.751 ns | 0.702 ns | 0.0315 | 264 B |
| _Delegate | 188.64 ns | 1.251 ns | 1.109 ns | 0.0267 | 224 B |

Methods starting with AS indicate type conversion.

Model classes used for testing:

	public class TestValue
	{
		public bool ValueA { get; set; } = true;
		public sbyte ValueB { get; set; } = 1;
		public byte ValueC { get; set; } = 2;
		public short ValueD { get; set; } = 3;
		public ushort ValueE { get; set; } = 4;
		public int ValueF { get; set; } = 5;
		public uint ValueG { get; set; } = 6;
		public long ValueH { get; set; } = 7;
		public ulong ValueI { get; set; } = 8;
		public float ValueJ { get; set; } = 9;
		public double ValueK { get; set; } = 10;
		public decimal ValueL { get; set; } = 11;
		public char ValueM { get; set; } = (Char)12;
	}
	public class TestB
	{
		public bool ValueA { get; set; } = true;
		public sbyte ValueB { get; set; } = 1;
		public byte ValueC { get; set; } = 2;
		public short ValueD { get; set; } = 3;
		public ushort ValueE { get; set; } = 4;
		public int ValueF { get; set; } = 5;
		public uint ValueG { get; set; } = 6;
		public long ValueH { get; set; } = 7;
		public ulong ValueI { get; set; } = 8;
		public float ValueJ { get; set; } = 9;
		public double ValueK { get; set; } = 10;
		public decimal ValueL { get; set; } = 11;
		public char ValueM { get; set; } = (Char)12;
	}
	public class TestBase<T>
	{
		public T ValueA { get; set; }
		public T ValueB { get; set; }
		public T ValueC { get; set; }
		public T ValueD { get; set; }
		public T ValueE { get; set; }
		public T ValueF { get; set; }
		public T ValueG { get; set; }
		public T ValueH { get; set; }
		public T ValueI { get; set; }
		public T ValueJ { get; set; }
		public T ValueK { get; set; }
		public T ValueL { get; set; }
	}

	public class TestC : TestBase<int> { }

	public class TestD
	{
		public bool ValueA { get; set; } = true;
		public sbyte ValueB { get; set; } = 1;
		public byte ValueC { get; set; } = 2;
		public short ValueD { get; set; } = 3;
		public ushort ValueE { get; set; } = 4;
		public int ValueF { get; set; } = 5;
		public uint ValueG { get; set; } = 6;
		public long ValueH { get; set; } = 7;
		public ulong ValueI { get; set; } = 8;
		public float ValueJ { get; set; } = 9;
		public double ValueK { get; set; } = 10;
		public decimal ValueL { get; set; } = 11;
		public char ValueM { get; set; } = (Char)12;
	}

Quick Start with MaomiMapper

Using the MaomiMapper framework is relatively simple, as shown in the following example:

var maomi = new MaomiMapper();
maomi
    .Bind<TestValue, TestB>()
    .Bind<TestValue, TestC>()
    .Bind<TestValue, TestD>();

maomi.Map<TestValue, TestD>(new TestValue());

Configuration

When mapping objects, you can configure the mapping logic, such as whether to create a new object when encountering member objects, whether to map private members, etc.

Usage is as follows:

        var mapper = new MaomiMapper();
        mapper.Bind<TestA, TestB>(option =>
        {
            option.IsObjectReference = false;
        }).Build();

Each type mapping can have a separate MapOption configured.

MapOption type:

	/// <summary>
	/// Mapping configuration
	/// </summary>
	public class MapOption
	{
		/// <summary>
		/// Include private fields
		/// </summary>
		public bool IncludePrivate { get; set; } = false;

		/// <summary>
		/// Auto map; if there are fields/properties without configured mapping rules, it will be automatically mapped
		/// </summary>
		public bool AutoMap { get; set; } = true;

		/// <summary>
		/// If the field property is an object and of the same type, retain the reference.<br />
		/// If set to false, it will create a new object and process the fields one by one.
		/// &lt;/summary&gt;
		public bool IsObjectReference { get; set; } = true;

		/// &lt;summary&gt;
		/// Configure time converter.<br />
		/// If b.Value is DateTime and a.Value is not DateTime, a converter must be configured; otherwise, an error will occur.
		/// &lt;/summary&gt;
		/// &lt;value&gt;&lt;/value&gt;
		public Func&lt;object, DateTime&gt;? ConvertDateTime { get; set; }
	}

Automatic Scanning

MaomiMapper supports scanning object mappings in assemblies and can be configured in two ways.

The first method uses an attribute class to designate the types that can be converted.

As shown in the code below, TestValueB indicates that it can be mapped as TestValueA type.

	public class TestValueA
	{
		public string ValueA { get; set; } = &quot;A&quot;;

		public string ValueB { get; set; } = &quot;B&quot;;

		public string ValueC { get; set; } = &quot;C&quot;;
	}

	[Map(typeof(TestValueA), IsReverse = true)]
	public class TestValueB
	{
		public string ValueA { get; set; }

		public string ValueB { get; set; }

		public string ValueC { get; set; }
	}

The second method is to implement IMapper and configure mapping rules within the file.

	public class MyMapper : IMapper
	{
		public override void Bind(MaomiMapper mapper)
		{
			mapper.Bind&lt;TestA, TestC&gt;(option =&gt; option.IsObjectReference = false);
			mapper.Bind&lt;TestA, TestD&gt;(option =&gt; option.IsObjectReference = false);
		}
	}

Additionally, you can inherit and implement the MapOptionAttribute attribute, then attach it to the type; during the assembly scanning for mappings, the framework will automatically configure it.

	[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
	public class MyMapOptionAttribute : MapOptionAttribute
	{
		public override Action&lt;MapOption&gt; MapOption =&gt; _option;
		private Action&lt;MapOption&gt; _option;
		public MyMapOptionAttribute()
		{
			_option = option =&gt;
			{
				option.IsObjectReference = false;
			};
		}
	}

	[MyMapOption]
	[Map(typeof(TestB), IsReverse = true)]
	public class TestA
	{
		public string ValueA { get; set; } = &quot;A&quot;;

		public string ValueB { get; set; } = &quot;B&quot;;

		public string ValueC { get; set; } = &quot;C&quot;;
		public TestValueA Value { get; set; }
	}

Configuring Field Mappings

You can use .Map to specify a field mapping rule.

maomi
    .Bind&lt;TestValue, TestB&gt;()
    .Map(a =&gt; a.ValueC + 1, b =&gt; b.ValueC).Build();

This equates to:

b.ValueC = a.ValueC + 1

If there are private fields to map, you can use the name field.

    public class TestD
    {
        public string ValueA { get; set; }
        public string ValueB;
        private string ValueC { get; set; }
        private string ValueD;
    }

    public class TestDD
    {
        public string ValueA { get; set; }
        public string ValueB;
        public string ValueC { get; set; }
        public string ValueD;
    }
        var mapper = new MaomiMapper();
        var build = mapper.Bind&lt;TestC, TestD&gt;(
            option =&gt;
            {
                option.IncludePrivate = true;
            })
            .Map(a =&gt; &quot;111&quot;, b =&gt; &quot;ValueC&quot;)
            .Build();
        mapper.Bind&lt;TestC, TestDD&gt;().Build();

This equates to:

b.ValueC = &quot;111&quot;

When configuring mappings, you can call the Build() method to automatically map other fields or properties. For instance, if the developer only configures the .ValueA property, leaving ValueB, ValueC, etc., unconfigured, calling Build() will cause the framework to complete the mapping for the other properties. If not configured, the framework will automatically invoke it during the first use of object mapping.

If reverse mapping is required, you can use BuildAndReverse().

           .BuildAndReverse(option =&gt;
			{
				option.IsObjectReference = false;
			});

Fields mapping can be ignored.

				// b.V = a.V + &quot;a&quot;
				.Map(a =&gt; a.V + &quot;a&quot;, b =&gt; b.V)
				// Ignore V1
				.Ignore(x =&gt; x.V1)
				// b.V2 = a.V
				.Map(a =&gt; a.V, b =&gt; &quot;V2&quot;)
				// b.V3 = &quot;666&quot;;
				.Map(a =&gt; &quot;666&quot;, b =&gt; &quot;V3&quot;)
				.Build();

Object Mapping

Given the following model classes:

    public class TestValue
    {
        public string ValueA { get; set; } = &quot;A&quot;;

        public string ValueB { get; set; } = &quot;B&quot;;

        public string ValueC { get; set; } = &quot;C&quot;;
    }

    public class TestA
    {
        public TestValue Value { get; set; }
    }
    public class TestB
    {
        public TestValue Value { get; set; }
    }

Both TestA and TestB types have properties of type TestValue; the framework defaults to using reference assignment, as shown:

testB.Value = testA.Value

The Value properties of the two objects reference the same object.

If a new instance needs to be created, use:

        var mapper = new MaomiMapper();
        mapper.Bind&lt;TestA, TestB&gt;(option =&gt;
        {
            // Create new instance
            option.IsObjectReference = false;
        }).Build();

If the Value properties are of different types, the framework will automatically map them as well. For example:

    public class TestA
    {
        public TestValueA Value { get; set; }
    }
    public class TestB
    {
        public TestValueB Value { get; set; }
    }

When both TestValueA and TestValueB are object types, the framework will automatically map to the next level.

Array and Collection Mapping

MaomiMapper can only handle arrays of the same type and uses direct assignment.

		public class TestA
		{
			public int[] Value { get; set; }
		}
		public class TestB
		{
			public int[] Value { get; set; }
		}
			var mapper = new MaomiMapper();
			mapper.Bind&lt;TestA, TestB&gt;(option =&gt;
			{
				option.IsObjectReference = true;
			}).BuildAndReverse(option =&gt;
			{
				option.IsObjectReference = false;
			});

			var a = new TestA
			{
				Value = new[] { 1, 2, 3 }
			};
			var b = mapper.Map&lt;TestA, TestB&gt;(a);

MaomiMapper can handle most collections, except for types like dictionaries.

处理相同类型的集合:

		public class TestC
		{
			public List<int> Value { get; set; }
		}
		public class TestD
		{
			public List<int> Value { get; set; }
		}
			var mapper = new MaomiMapper();
			mapper.Bind<TestC, TestD>(option =>
			{
				option.IsObjectReference = false;
			}).Build();

			var a = new TestA
			{
				Value = new[] { 1, 2, 3 }
			};
			var b = mapper.Map<TestA, TestB>(a);

相当于:

d.Value = new List<int>();
d.Value.AddRange(c.Value);

也可以处理不同类型的集合:

		public class TestE
		{
			public List<int> Value { get; set; }
		}
		public class TestF
		{
			public IEnumerable<int> Value { get; set; }
		}
		public class TestG
		{
			public HashSet<int> Value { get; set; }
		}
			var mapper = new MaomiMapper();
			mapper.Bind<TestE, TestF>(option =>
			{
				option.IsObjectReference = false;
			}).Build();

			var a = new TestE
			{
				Value = new List<int> { 1, 2, 3 }
			};
			var b = mapper.Map<TestE, TestF>(a);

以上 TestE、TestF、TestG 均可互转。

值类型互转

框架支持以下类型自动互转。

Boolean
SByte
Byte
Int16
UInt16
Int32
UInt32
Int64
UInt64
Single
Double
Decimal
Char

支持任何类型自动转换为 string,但是不支持 string 转换为其它类型。

对于时间类型的处理,可以手动配置转换函数:

	public class TestA
	{
		public string Value { get; set; }
	}
	public class TestB
	{
		public DateTime Value { get; set; }
	}

	[Fact]
	public void AS_Datetime()
	{
		var mapper = new MaomiMapper();
		mapper.Bind<TestA, TestB>(option =>
		{
            // 配置转换函数
			option.ConvertDateTime = value =>
			{
				if (value is string str)
					return DateTime.Parse(str);
				throw new Exception("未能转换为时间");
			};
		}).Build();
		var date = DateTime.Now;
		var a = mapper.Map<TestA, TestB>(new TestA()
		{
			Value = date.ToString()
		});

		Assert.Equal(date.ToString("yyyy/MM/dd HH:mm:ss"), a.Value.ToString("yyyy/MM/dd HH:mm:ss"));
	}

痴者工良

高级程序员劝退师

文章评论