分享教學項目:開源一個對象映射框架

Maomi.Mapper

項目地址:https://github.com/whuanle/Maomi.Mapper
注:本項目用於教學目的,性能較差,請勿用於生產環境。

MaomiMapper 是一個使用表達式樹構造生成對象成員映射的框架,即對象映射框架,用於配合筆者其它系列文章,用於教學目的。

筆者此係列教程還沒有公開,是講解如何編寫各類框架的。

雖然 MaomiMapper 性能不啥樣,但是代碼註釋也寫得很齊全,適合讀者研究反射、表達式樹、類型轉換等代碼。

MamomiMapper 不是爲了對標 AutoMapper,而是用於教學目的。

MaomiMapper 與 AutoMapper 對比:

Method Mean Error StdDev Gen0 Allocated
ASAutoMapper 148.66 ns 1.781 ns 1.666 ns 0.0362 304 B
ASMaomiMapper 6,562.87 ns 14.360 ns 13.433 ns 0.2670 2265 B
_AutoMapper 69.21 ns 0.134 ns 0.105 ns 0.0191 160 B
_MaomiMapper 3,203.79 ns 11.527 ns 10.783 ns 0.1221 1040 B

AS 開頭的方法表示有類型轉換。

測試使用的模型類:

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

快速使用 MaomiMapper

MaomiMapper 框架的使用比較簡單,示例如下:

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

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

配置

在映射對象時,可以配置映射邏輯,比如碰到成員是對象時,是否開闢新對象,是否映射私有成員等。

使用方法如下:

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

每個類型映射都可以單獨配置一個 MapOption。

MapOption 類型:

	/// <summary>
	/// 映射配置
	/// </summary>
	public class MapOption
	{
		/// <summary>
		/// 包括私有字段
		/// </summary>
		public bool IncludePrivate { get; set; } = false;

		/// <summary>
		/// 自動映射,如果有字段/屬性沒有配置映射規則,則自動映射
		/// </summary>
		public bool AutoMap { get; set; } = true;

		/// <summary>
		/// 如果屬性字段是對象且爲相同類型,則保持引用。 <br />
		/// 如果設置爲 false,則會創建新的對象,再對字段逐個處理。
		/// </summary>
		public bool IsObjectReference { get; set; } = true;

		/// <summary>
		/// 配置時間轉換器。<br />
		///  如果 b.Value 是 DateTime,而 a.Value 不是 DateTime,則需要配置轉換器,否則會報錯。
		/// </summary>
		/// <value></value>
		public Func<object, DateTime>? ConvertDateTime { get; set; }
	}

自動掃描

MaomiMapper 支持掃描程序集中的對象映射,有兩種方法可以配置。

第一種方法是使用特性類,標識該類型可以轉換爲何種類型。

如下代碼所示,TestValueB 標識了其可以映射爲 TestValueA 類型。

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

		public string ValueB { get; set; } = "B";

		public string ValueC { get; set; } = "C";
	}

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

		public string ValueB { get; set; }

		public string ValueC { get; set; }
	}

第二種方法是實現 IMapper,在文件中配置映射規則。

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

此外,可以繼承實現 MapOptionAttribute 特性,然後附加到類型中,在掃描程序集映射時,框架會自動配置。

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

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

		public string ValueB { get; set; } = "B";

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

配置字段映射

可以使用 .Map 配置一個字段的映射規則。

maomi
    .Bind<TestValue, TestB>()
    .Map(a => a.ValueC + 1, b => b.ValueC).Build()

相當於:

b.ValueC = a.ValueC + 1

如果有私有字段需要映射,可以使用名稱字段。

    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<TestC, TestD>(
            option =>
            {
                option.IncludePrivate = true;
            })
            .Map(a => "111", b => "ValueC")
            .Build();
        mapper.Bind<TestC, TestDD>().Build();

相當於:

b.ValueC = "111"

在配置映射時,可以調用 Build() 方法,自動映射其它字段或屬性。比如開發者只配置了 .ValueA 屬性,未配置 ValueBValueC 等,則調用 Build() 時,框架會補全其它屬性對應的映射。如果未配置,框架則在第一次使用對象映射時自動調用。

如果需要反向映射,可以使用 BuildAndReverse()

           .BuildAndReverse(option =>
			{
				option.IsObjectReference = false;
			});

可以忽略字段映射。

				// b.V = a.V + "a"
				.Map(a => a.V + "a", b => b.V)
				// 忽略 V1
				.Ignore(x => x.V1)
				// b.V2 = a.V
				.Map(a => a.V, b => "V2")
				// b.V3 = "666";
				.Map(a => "666", b => "V3")
				.Build();

對象映射

有以下模型類:

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

        public string ValueB { get; set; } = "B";

        public string ValueC { get; set; } = "C";
    }

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

TestA 和 TestB 類型中,均有 TestValue 類型的屬性,框架默認使用引用賦值,示例:

testB.Value = testA.Value

兩個對象的 Value 屬性引用了同一個對象。

如果需要開闢新的實例,可以使用:

        var mapper = new MaomiMapper();
        mapper.Bind<TestA, TestB>(option =>
        {
            // 開闢新的實例
            option.IsObjectReference = false;
        }).Build();

如果兩者的 Value 屬性是不同類型對象,則框架也會自動映射。如:

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

TestValueA、TestValueB 均爲對象類型時,框架會自動映射下一層。

數組和集合映射

MaomiMapper 只能處理相同類型的數組,並且使用直接賦值的方法。

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

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

MaomiMapper 可以處理大多數集合,除了字典等類型。

處理相同類型的集合:

		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"));
	}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章