嬸可忍叔不可忍的AutoMapper,你還用嗎?
AutoMapper是讓人又愛又恨的項目
- 愛它是因為它解決了一些問題,很多項目都有用,下載量很大,受眾很廣。
- 恨它是因為它諸多反人類的設計。
- 為此本人開源項目PocoEmit對標AutoMapper。
1. AutoMapper反人類設計
1.1 AutoMapper注冊代碼
services.AddAutoMapper(cfg => cfg.CreateMap<User, UserDTO>());
User和UserDTO除了類名不一樣,其他都一樣,怎么看這行代碼都多余。
需要轉化的類型越多,多余的代碼就越多。
類型轉化不應該就是個靜態方法嗎?而且AutoMapper注冊卻依賴容器,Mapper對象也是從容器獲取。
本人覺得AutoMapper設計的太反人類了。
1.2 PocoEmit對于大部分轉化是不需要手動配置
- PocoEmit可以輕松的定義靜態實例。
- PocoEmit靜態實例可以用來定義靜態委托字段,當靜態方法用。
UserDTO dto = PocoEmit.Mapper.Default.Convert<User, UserDTO>(new User());
public static readonly Func<User, UserDTO> UserDTOConvert = PocoEmit.Mapper.Default.GetConvertFunc<User, UserDTO>();
2. AutoMapper的性能差強人意
2.1 以下是AutoMapper官網例子與PocoEmit.Mapper的對比
- Customer轉化為CustomerDTO(嵌套多個子對象、數組及列表)。
- Auto是執行AutoMapper的IMapper.Map方法。
- Poco是執行PocoEmit.Mapper的IMapper.Convert方法。
- PocoFunc是執行PocoEmit.Mapper生成的委托。
| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|
| Auto | 89.30 ns | 1.006 ns | 1.118 ns | 90.17 ns | 1.46 | 0.03 | 0.0260 | 448 B | 1.08 |
| Poco | 61.31 ns | 1.036 ns | 1.194 ns | 61.25 ns | 1.00 | 0.03 | 0.0241 | 416 B | 1.00 |
| PocoFunc | 42.56 ns | 0.066 ns | 0.073 ns | 42.56 ns | 0.69 | 0.01 | 0.0223 | 384 B | 0.92 |
- Auto耗時比Poco多50%左右。
- Auto耗時是PocoFunc的兩倍多。
2.2 能不能用AutoMapper生成委托來提高性能呢
- 既可以說能也可以說不能。
- 說能是因為AutoMapper確實提供了該功能。
- 說不能是因為AutoMapper沒打算給用戶用。
2.2.1 AutoMapper生成委托有點麻煩
var configuration = _auto.ConfigurationProvider.Internal();
var mapRequest = new MapRequest(new TypePair(typeof(Customer), typeof(CustomerDTO)));
Func<Customer, CustomerDTO, ResolutionContext, CustomerDTO> autoFunc = configuration.GetExecutionPlan<Customer, CustomerDTO>(mapRequest);
作為對比PocoEmit.Mapper就簡單的多了
Func<Customer, CustomerDTO> pocoFunc = PocoEmit.Mapper.Default.GetConvertFunc<Customer, CustomerDTO>();
2.2.2 調用AutoMapper生成的委托更麻煩
- 參數ResolutionContext沒有公開的構造函數,也找不到公開的實例。
- 只能通過反射獲得ResolutionContext的實例。
var field = typeof(AutoMapper.Mapper).GetField("_defaultContext", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
ResolutionContext resolutionContext = field.GetValue(_auto) as ResolutionContext;
2.2.3 加入AutoMapper生成委托再對比一下
| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|
| Auto | 89.30 ns | 1.006 ns | 1.118 ns | 90.17 ns | 1.46 | 0.03 | 0.0260 | 448 B | 1.08 |
| AutoFunc | 56.04 ns | 0.103 ns | 0.119 ns | 56.03 ns | 0.91 | 0.02 | 0.0260 | 448 B | 1.08 |
| Poco | 61.31 ns | 1.036 ns | 1.194 ns | 61.25 ns | 1.00 | 0.03 | 0.0241 | 416 B | 1.00 |
| PocoFunc | 42.56 ns | 0.066 ns | 0.073 ns | 42.56 ns | 0.69 | 0.01 | 0.0223 | 384 B | 0.92 |
- AutoMapper生成委托確實也快了不少。
- 從百分比來看即使不生成委托,AutoMapper也慢不了多少?沒有數量級的區別,能忍? --- 反問句
2.3 簡單類型轉化對比
- User轉UserDTO,只有兩個簡單屬性
| Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|
| Auto | 35.436 ns | 0.0455 ns | 0.0505 ns | 1.57 | 0.0019 | 32 B | 0.50 |
| AutoFunc | 4.159 ns | 0.0847 ns | 0.0906 ns | 0.18 | 0.0019 | 32 B | 0.50 |
| Poco | 22.607 ns | 0.1754 ns | 0.1801 ns | 1.00 | 0.0037 | 64 B | 1.00 |
| PocoFunc | 3.818 ns | 0.0176 ns | 0.0180 ns | 0.17 | 0.0019 | 32 B | 0.50 |
- Auto耗時是AutoFunc差不多十倍,差出一個數量級了(回答了前面的反問)
- AutoFunc耗時比PocoFunc稍多,這說明AutoMapper復雜類型轉化性能非常不好,簡單類型轉化可能還能湊合
- 關鍵是性能好生成的委托AutoMapper不給用啊,“嬸可忍叔不可忍”啊!
3. AutoMapper生成的代碼能通過代碼審核嗎?
3.1 AutoMapper官網那個例子生成以下代碼
T __f<T>(System.Func<T> f) => f();
CustomerDTO _autoMap(Customer source, CustomerDTO destination, ResolutionContext context)
{
return (source == null) ?
(destination == null) ? (CustomerDTO)null : destination :
__f(() => {
CustomerDTO typeMapDestination = null;
typeMapDestination = destination ?? new CustomerDTO();
try
{
typeMapDestination.Id = source.Id;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
try
{
typeMapDestination.Name = source.Name;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
try
{
Address resolvedValue = null;
Address mappedValue = null;
resolvedValue = source.Address;
mappedValue = (resolvedValue == null) ? (Address)null :
((Func<Address, Address, ResolutionContext, Address>)((
Address source_1,
Address destination_1,
ResolutionContext context) => //Address
(source_1 == null) ?
(destination_1 == null) ? (Address)null : destination_1 :
__f(() => {
Address typeMapDestination_1 = null;
typeMapDestination_1 = destination_1 ?? new Address();
try
{
typeMapDestination_1.Id = source_1.Id;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
try
{
typeMapDestination_1.Street = source_1.Street;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
try
{
typeMapDestination_1.City = source_1.City;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
try
{
typeMapDestination_1.Country = source_1.Country;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
return typeMapDestination_1;
})))
.Invoke(
resolvedValue,
(destination == null) ? (Address)null :
typeMapDestination.Address,
context);
typeMapDestination.Address = mappedValue;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
try
{
Address resolvedValue_1 = null;
AddressDTO mappedValue_1 = null;
resolvedValue_1 = source.HomeAddress;
mappedValue_1 = (resolvedValue_1 == null) ? (AddressDTO)null :
((Func<Address, AddressDTO, ResolutionContext, AddressDTO>)((
Address source_2,
AddressDTO destination_2,
ResolutionContext context) => //AddressDTO
(source_2 == null) ?
(destination_2 == null) ? (AddressDTO)null : destination_2 :
__f(() => {
AddressDTO typeMapDestination_2 = null;
typeMapDestination_2 = destination_2 ?? new AddressDTO();
try
{
typeMapDestination_2.Id = source_2.Id;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
try
{
typeMapDestination_2.City = source_2.City;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
try
{
typeMapDestination_2.Country = source_2.Country;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
return typeMapDestination_2;
})))
.Invoke(
resolvedValue_1,
(destination == null) ? (AddressDTO)null :
typeMapDestination.HomeAddress,
context);
typeMapDestination.HomeAddress = mappedValue_1;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
try
{
Address[] resolvedValue_2 = null;
AddressDTO[] mappedValue_2 = null;
resolvedValue_2 = source.Addresses;
mappedValue_2 = (resolvedValue_2 == null) ?
Array.Empty<AddressDTO>() :
__f(() => {
AddressDTO[] destinationArray = null;
int destinationArrayIndex = default;
destinationArray = new AddressDTO[resolvedValue_2.Length];
destinationArrayIndex = default(int);
int sourceArrayIndex = default;
Address sourceItem = null;
sourceArrayIndex = default(int);
while (true)
{
if ((sourceArrayIndex < resolvedValue_2.Length))
{
sourceItem = resolvedValue_2[sourceArrayIndex];
destinationArray[destinationArrayIndex++] = ((Func<Address, AddressDTO, ResolutionContext, AddressDTO>)((
Address source_2,
AddressDTO destination_2,
ResolutionContext context) => //AddressDTO
(source_2 == null) ?
(destination_2 == null) ? (AddressDTO)null : destination_2 :
__f(() => {
AddressDTO typeMapDestination_2 = null;
typeMapDestination_2 = destination_2 ?? new AddressDTO();
try
{
typeMapDestination_2.Id = source_2.Id;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
try
{
typeMapDestination_2.City = source_2.City;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
try
{
typeMapDestination_2.Country = source_2.Country;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
return typeMapDestination_2;
})))
.Invoke(
sourceItem,
(AddressDTO)null,
context);
sourceArrayIndex++;
}
else
{
goto LoopBreak;
}
}
LoopBreak:;
return destinationArray;
});
typeMapDestination.Addresses = mappedValue_2;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
try
{
List<Address> resolvedValue_3 = null;
List<AddressDTO> mappedValue_3 = null;
resolvedValue_3 = source.WorkAddresses;
mappedValue_3 = (resolvedValue_3 == null) ?
new List<AddressDTO>() :
__f(() => {
List<AddressDTO> collectionDestination = null;
List<AddressDTO> passedDestination = null;
passedDestination = (destination == null) ? (List<AddressDTO>)null :
typeMapDestination.WorkAddresses;
collectionDestination = passedDestination ?? new List<AddressDTO>();
collectionDestination.Clear();
List<Address>.Enumerator enumerator = default;
Address item = null;
enumerator = resolvedValue_3.GetEnumerator();
try
{
while (true)
{
if (enumerator.MoveNext())
{
item = enumerator.Current;
collectionDestination.Add(((Func<Address, AddressDTO, ResolutionContext, AddressDTO>)((
Address source_2,
AddressDTO destination_2,
ResolutionContext context) => //AddressDTO
(source_2 == null) ?
(destination_2 == null) ? (AddressDTO)null : destination_2 :
__f(() => {
AddressDTO typeMapDestination_2 = null;
typeMapDestination_2 = destination_2 ?? new AddressDTO();
try
{
typeMapDestination_2.Id = source_2.Id;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
try
{
typeMapDestination_2.City = source_2.City;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
try
{
typeMapDestination_2.Country = source_2.Country;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
return typeMapDestination_2;
})))
.Invoke(
item,
(AddressDTO)null,
context));
}
else
{
goto LoopBreak_1;
}
}
LoopBreak_1:;
}
finally
{
enumerator.Dispose();
}
return collectionDestination;
});
typeMapDestination.WorkAddresses = mappedValue_3;
}
catch (Exception ex)
{
throw TypeMapPlanBuilder.MemberMappingError(
ex,
default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
}
return typeMapDestination;
});
}
3.2 以下是PocoEmit.Mapper生成的代碼
CustomerDTO _pocoConvert(Customer source)
{
CustomerDTO dest = null;
if ((source != (Customer)null))
{
dest = new CustomerDTO();
Address member0 = null;
Address member1 = null;
Address[] member2 = null;
List<Address> member3 = null;
dest.Id = source.Id;
dest.Name = source.Name;
member0 = source.Address;
if ((member0 != null))
{
dest.Address = member0;
}
member1 = source.HomeAddress;
if ((member1 != null))
{
// { The block result will be assigned to `dest.HomeAddress`
AddressDTO dest_1 = null;
if ((member1 != (Address)null))
{
dest_1 = new AddressDTO();
dest_1.Id = member1.Id;
dest_1.City = member1.City;
dest_1.Country = member1.Country;
}
dest.HomeAddress = dest_1;
// } end of block assignment;
}
member2 = source.Addresses;
if ((member2 != null))
{
// { The block result will be assigned to `dest.Addresses`
int count = default;
AddressDTO[] dest_2 = null;
int index = default;
Address sourceItem = null;
count = member2.Length;
dest_2 = new AddressDTO[count];
index = 0;
while (true)
{
if ((index < count))
{
sourceItem = member2[index];
// { The block result will be assigned to `dest_2[index]`
AddressDTO dest_3 = null;
if ((sourceItem != (Address)null))
{
dest_3 = new AddressDTO();
dest_3.Id = sourceItem.Id;
dest_3.City = sourceItem.City;
dest_3.Country = sourceItem.Country;
}
dest_2[index] = dest_3;
// } end of block assignment
index++;
}
else
{
goto forLabel;
}
}
forLabel:;
dest.Addresses = dest_2;
// } end of block assignment;
}
member3 = source.WorkAddresses;
if ((member3 != null))
{
// { The block result will be assigned to `dest.WorkAddresses`
List<AddressDTO> dest_4 = null;
dest_4 = new List<AddressDTO>(member3.Count);
dest_4;
int index_1 = default;
int len = default;
index_1 = 0;
len = member3.Count;
while (true)
{
if ((index_1 < len))
{
Address sourceItem_1 = null;
AddressDTO destItem = null;
sourceItem_1 = member3[index_1];
// { The block result will be assigned to `destItem`
AddressDTO dest_5 = null;
if ((sourceItem_1 != (Address)null))
{
dest_5 = new AddressDTO();
dest_5.Id = sourceItem_1.Id;
dest_5.City = sourceItem_1.City;
dest_5.Country = sourceItem_1.Country;
}
destItem = dest_5;
// } end of block assignment;
dest_4.Add(destItem);
index_1++;
}
else
{
goto forLabel_1;
}
}
forLabel_1:;
dest.WorkAddresses = dest_4;
// } end of block assignment;
}
CustomerConvertBench.ConvertAddressCity(
source,
dest);
}
return dest;
}
3.3 簡單對比如下
- AutoMapper生成代碼三百多行,PocoEmit.Mapper一百多行,AutoMapper代碼量是兩倍以上
- AutoMapper生成大量try catch,哪怕是int對int賦值也要try
- AutoMapper用迭代器Enumerator訪問列表,PocoEmit.Mapper用索引器
- AutoMapper這些區別應該是導致性能差的部分原因
3.4 如何獲取AutoMapper生成的代碼
LambdaExpression expression = _auto.ConfigurationProvider.BuildExecutionPlan(typeof(Customer), typeof(CustomerDTO));
3.4.1 如果要查看更可讀的代碼推薦使用FastExpressionCompiler
- 可以使用nuget安裝
- 前面的例子就是使用FastExpressionCompiler再手動整理了一下
string code = FastExpressionCompiler.ToCSharpPrinter.ToCSharpString(expression);
3.4.2 PocoEmit獲取生成代碼更簡單
Expression<Func<Customer, CustomerDTO>> expression = PocoEmit.Mapper.Default.BuildConverter<Customer, CustomerDTO>();
string code = FastExpressionCompiler.ToCSharpPrinter.ToCSharpString(expression);
3.4.3 PocoEmit生成代碼擴展性
- PocoEmit可以獲取委托表達式自己來編譯委托
- PocoEmit通過PocoEmit.Builders.Compiler.Instance來編譯,可以對Instance進行覆蓋來擴展
- 通過實現Compiler類來擴展,只需要重寫CompileFunc和CompileAction兩個方法
- 可以使用FastExpressionCompiler來實現Compiler類
4. AutoMapper枚舉邏輯問題
public enum MyColor
{
None = 0,
Red = 1,
Green = 2,
Blue = 3,
}
ConsoleColor color = ConsoleColor.DarkBlue;
// Red
MyColor autoColor = _auto.Map<ConsoleColor, MyColor>(color);
// None
MyColor pocoColor = PocoEmit.Mapper.Default.Convert<ConsoleColor, MyColor>(color);
- AutoMapper先按枚舉名轉化,失敗再按值轉化,不支持的DarkBlue被AutoMapper轉化為Red
- 不同類型的枚舉值轉化沒有意義,定義枚舉可以不指定值
- AutoMapper這完全是犯了畫蛇添足的錯誤
- AutoMapper還有哪些槽點歡迎大家在評論區指出
5. PocoEmit可擴展架構
5.1 nuget安裝PocoEmit可獲得基礎功能
- 通過PocoEmit可以讀寫實體的屬性
- PocoEmit可以通過PocoEmit.Poco轉化基礎類型和枚舉
- PocoEmit.Poco支持注冊轉化表達式
5.2 nuget安裝PocoEmit.Mapper獲得更多功能
- PocoEmit.Mapper可以支持PocoEmit.Poco的所有功能
- PocoEmit.Mapper可以支持自定義實體類型(不支持集合(含數組、列表及字典)成員)的轉化和復制
5.3 nuget安裝PocoEmit.Collections擴展集合功能
- 通過UseCollection擴展方法給PocoEmit.Mapper增加集合功能
- 擴展后PocoEmit.Mapper支持集合(含數組、列表及字典)的轉化和復制
- 支持實體類型包含集合成員的轉化和復制
- 嫌麻煩的同學可以直接安裝PocoEmit.Collections并配置UseCollection
如何使用PocoEmit.Mapper替代AutoMapper請移步看下一篇 http://www.rzrgm.cn/xiangji/p/19062936
源碼托管地址: https://github.com/donetsoftwork/MyEmit ,也歡迎大家直接查看源碼。
gitee同步更新:https://gitee.com/donetsoftwork/MyEmit
如果大家喜歡請動動您發財的小手手幫忙點一下Star。
浙公網安備 33010602011771號