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
屬性,未配置 ValueB
、ValueC
等,則調用 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"));
}