PocoEmit遥遥领先于AutoMapper之循环引用

一、什么是循环引用

循环引用就是类型相互依赖

1. 比如A类有B类的属性,B类也有A类的属性

  • 这有什么问题呢?
  • 编写生成A的代码需要遍历A的所有属性
  • 构造B类型属性是A代码的一部分,B代码又含有A类型属性
  • 这就是一个编译死循环

2. 其他循环引用的例子

  • 链表结构只有一个类型也是类型循环引用
  • A-B-C-A等更长的引用链条也会构成类型循环引用

二、举个树状结构的Case

  • 树状结构在实际应用中很常见

1. 导航菜单代码

导航菜单是一个典型的树状结构

csharp 复制代码
public class Menu
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public List<Menu> Children { get; set; }
    public static Menu GetMenu()
    {
        var programs = new Menu { Id = 2, Name = "Programs", Description = "程序" };
        var documents = new Menu { Id = 3, Name = "Documents", Description = "文档" };
        var settings = new Menu { Id = 4, Name = "Settings", Description = "设置" };
        var help = new Menu { Id = 5, Name = "Help", Description = "帮助" };
        var run = new Menu { Id = 6, Name = "Run", Description = "运行" };
        var shutdown = new Menu { Id = 7, Name = "Shut Down", Description = "关闭" };
        var start = new Menu { Id = 1, Name = "Start", Description = "开始", Children = [programs, documents, settings, help, run, shutdown] };
        return start;
    }
}

2. 把Menu转化为MenuDTO

2.1 PocoEmit执行代码

  • 代码中多加了UseCollection
  • 如果全局开启了集合就不需要这行代码
csharp 复制代码
var menu = Menu.GetMenu();
var mapper = PocoEmit.Mapper.Create()
    .UseCollection();
var dto = mapper.Convert<Menu, MenuDTO>(menu);

2.2 执行效果如下:

json 复制代码
{
  "$id": "1",
  "Id": 1,
  "Name": "Start",
  "Description": "\u5F00\u59CB",
  "Children": {
    "$id": "2",
    "$values": [
      {
        "$id": "3",
        "Id": 2,
        "Name": "Programs",
        "Description": "\u7A0B\u5E8F",
        "Children": null
      },
      {
        "$id": "4",
        "Id": 3,
        "Name": "Documents",
        "Description": "\u6587\u6863",
        "Children": null
      },
      {
        "$id": "5",
        "Id": 4,
        "Name": "Settings",
        "Description": "\u8BBE\u7F6E",
        "Children": null
      },
      {
        "$id": "6",
        "Id": 5,
        "Name": "Help",
        "Description": "\u5E2E\u52A9",
        "Children": null
      },
      {
        "$id": "7",
        "Id": 6,
        "Name": "Run",
        "Description": "\u8FD0\u884C",
        "Children": null
      },
      {
        "$id": "8",
        "Id": 7,
        "Name": "Shut Down",
        "Description": "\u5173\u95ED",
        "Children": null
      }
    ]
  }
}

3. 与AutoMapper性能对比如下

Method Mean Error StdDev Median Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Auto 320.14 ns 0.420 ns 0.484 ns 320.10 ns 5.51 0.10 0.0751 0.0003 1296 B 2.95
AutoFunc 289.80 ns 6.580 ns 7.313 ns 295.77 ns 4.98 0.15 0.0751 0.0003 1296 B 2.95
Poco 58.17 ns 1.031 ns 1.103 ns 58.17 ns 1.00 0.03 0.0255 - 440 B 1.00
PocoFunc 48.10 ns 1.059 ns 1.087 ns 49.06 ns 0.83 0.02 0.0255 - 440 B 1.00
  • AutoMapper耗时是Poco的5倍多
  • AutoMapper内存是Poco的近3倍
  • 哪怕是用上AutoMapper内部生成的委托也挽救不了多少局面

4. 我们增加无循环引用再测试一下

4.1 无循环引用菜单

csharp 复制代码
public class Menu0
{
    public int ParentId { get; set; }
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

    public static List<Menu0> GetMenus()
    {
        var start = new Menu0 { Id = 1, Name = "Start", Description = "开始", ParentId = 0 };
        var programs = new Menu0 { Id = 2, Name = "Programs", Description = "程序", ParentId = 1 };
        var documents = new Menu0 { Id = 3, Name = "Documents", Description = "文档", ParentId = 1 };
        var settings = new Menu0 { Id = 4, Name = "Settings", Description = "设置", ParentId = 1 };
        var help = new Menu0 { Id = 5, Name = "Help", Description = "帮助", ParentId = 1 };
        var run = new Menu0 { Id = 6, Name = "Run", Description = "运行" , ParentId = 1 };
        var shutdown = new Menu0 { Id = 7, Name = "Shut Down", Description = "关闭", ParentId = 1 };
        return [start, programs, documents, settings, help, run, shutdown];
    }
}

4.2 性能测试如下

Method Mean Error StdDev Median Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Auto 320.14 ns 0.420 ns 0.484 ns 320.10 ns 5.51 0.10 0.0751 0.0003 1296 B 2.95
Auto0 110.60 ns 1.130 ns 1.302 ns 110.30 ns 1.90 0.04 0.0264 - 456 B 1.04
AutoFunc 289.80 ns 6.580 ns 7.313 ns 295.77 ns 4.98 0.15 0.0751 0.0003 1296 B 2.95
Poco 58.17 ns 1.031 ns 1.103 ns 58.17 ns 1.00 0.03 0.0255 - 440 B 1.00
Poco0 60.80 ns 0.176 ns 0.202 ns 60.73 ns 1.05 0.02 0.0227 - 392 B 0.89
PocoFunc 48.10 ns 1.059 ns 1.087 ns 49.06 ns 0.83 0.02 0.0255 - 440 B 1.00
  • Auto0是AutoMapper把Menu0列表转化为DTO的case
  • Poco0是Poco把Menu0列表转化为DTO的case
  • AutoMapper循环引用处理耗时和内存都是列表的3倍
  • Poco循环引用处理和列表性能差不多
  • 当然就算是无循环引用的列表处理,AutoMapper耗时也几乎是Poco的两倍
  • 这充分说明AutoMapper处理循环引用是有问题的

5. 先对比一下AutoMapper有无循环引用的代码

5.1 AutoMapper无循环引用的代码如下

csharp 复制代码
T __f<T>(System.Func<T> f) => f();
(Func<List<Menu0>, List<Menu0DTO>, ResolutionContext, List<Menu0DTO>>)((
    List<Menu0> source, 
    List<Menu0DTO> mapperDestination, 
    ResolutionContext context) => //List<Menu0DTO>
    (source == null) ? 
        new List<Menu0DTO>() : 
        __f(() => {
            try
            {
                List<Menu0DTO> collectionDestination = null;
                List<Menu0DTO> passedDestination = null;
                passedDestination = mapperDestination;
                collectionDestination = passedDestination ?? new List<Menu0DTO>();
                collectionDestination.Clear();
                List<Menu0>.Enumerator enumerator = default;
                Menu0 item = null;
                enumerator = source.GetEnumerator();
                try
                {
                    while (true)
                    {
                        if (enumerator.MoveNext())
                        {
                            item = enumerator.Current;
                            collectionDestination.Add(((Func<Menu0, Menu0DTO, ResolutionContext, Menu0DTO>)((
                                Menu0 source_1, 
                                Menu0DTO destination, 
                                ResolutionContext context) => //Menu0DTO
                                (source_1 == null) ? 
                                    (destination == null) ? (Menu0DTO)null : destination : 
                                    __f(() => {
                                        Menu0DTO typeMapDestination = null;
                                        typeMapDestination = destination ?? new Menu0DTO();
                                        try
                                        {
                                            typeMapDestination.ParentId = source_1.ParentId;
                                        }
                                        catch (Exception ex)
                                        {
                                            throw TypeMapPlanBuilder.MemberMappingError(
                                                ex,
                                                default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
                                        }
                                        try
                                        {
                                            typeMapDestination.Id = source_1.Id;
                                        }
                                        catch (Exception ex)
                                        {
                                            throw TypeMapPlanBuilder.MemberMappingError(
                                                ex,
                                                default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
                                        }
                                        try
                                        {
                                            typeMapDestination.Name = source_1.Name;
                                        }
                                        catch (Exception ex)
                                        {
                                            throw TypeMapPlanBuilder.MemberMappingError(
                                                ex,
                                                default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
                                        }
                                        try
                                        {
                                            typeMapDestination.Description = source_1.Description;
                                        }
                                        catch (Exception ex)
                                        {
                                            throw TypeMapPlanBuilder.MemberMappingError(
                                                ex,
                                                default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
                                        }
                                        return typeMapDestination;
                                    })))
                            .Invoke(
                                item,
                                (Menu0DTO)null,
                                context));
                        }
                        else
                        {
                            goto LoopBreak;
                        }
                    }
                    LoopBreak:;
                }
                finally
                {
                    enumerator.Dispose();
                }
                return collectionDestination;
            }
            catch (Exception ex)
            {
                throw MapperConfiguration.GetMappingError(
                    ex,
                    default(MapRequest)/*NOTE: Provide the non-default value for the Constant!*/);
            }
        }));

5.2 AutoMapper循环引用的代码如下

csharp 复制代码
T __f<T>(System.Func<T> f) => f();
(Func<Menu, MenuDTO, ResolutionContext, MenuDTO>)((
    Menu source, 
    MenuDTO destination, 
    ResolutionContext context) => //MenuDTO
    (source == null) ? 
        (destination == null) ? (MenuDTO)null : destination : 
        __f(() => {
            MenuDTO typeMapDestination = null;
            ResolutionContext.CheckContext(ref context);
            return ((MenuDTO)context.GetDestination(
                source,
                typeof(MenuDTO))) ?? 
                __f(() => {
                    typeMapDestination = destination ?? new MenuDTO();
                    context.CacheDestination(
                        source,
                        typeof(MenuDTO),
                        typeMapDestination);
                    typeMapDestination;
                    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
                    {
                        typeMapDestination.Description = source.Description;
                    }
                    catch (Exception ex)
                    {
                        throw TypeMapPlanBuilder.MemberMappingError(
                            ex,
                            default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
                    }
                    try
                    {
                        List<Menu> resolvedValue = null;
                        List<MenuDTO> mappedValue = null;
                        resolvedValue = source.Children;
                        mappedValue = (resolvedValue == null) ? 
                            new List<MenuDTO>() : 
                            context.MapInternal<List<Menu>, List<MenuDTO>>(
                                resolvedValue,
                                (destination == null) ? (List<MenuDTO>)null : 
                                    typeMapDestination.Children,
                                (MemberMap)default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
                        typeMapDestination.Children = mappedValue;
                    }
                    catch (Exception ex)
                    {
                        throw TypeMapPlanBuilder.MemberMappingError(
                            ex,
                            default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
                    }
                    return typeMapDestination;
                });
        }));
5.2.2 List<Menu>转List<MenuDTO>
csharp 复制代码
T __f<T>(System.Func<T> f) => f();
(Func<List<Menu>, List<MenuDTO>, ResolutionContext, List<MenuDTO>>)((
    List<Menu> source,
    List<MenuDTO> mapperDestination,
    ResolutionContext context) => //List<MenuDTO>
    (source == null) ?
        new List<MenuDTO>() :
        __f(() => {
            try
            {
                List<MenuDTO> collectionDestination = null;
                List<MenuDTO> passedDestination = null;
                ResolutionContext.CheckContext(ref context);
                passedDestination = mapperDestination;
                collectionDestination = passedDestination ?? new List<MenuDTO>();
                collectionDestination.Clear();
                List<Menu>.Enumerator enumerator = default;
                Menu item = null;
                enumerator = source.GetEnumerator();
                try
                {
                    while (true)
                    {
                        if (enumerator.MoveNext())
                        {
                            item = enumerator.Current;
                            collectionDestination.Add(((Func<Menu, MenuDTO, ResolutionContext, MenuDTO>)((
                                Menu source_1,
                                MenuDTO destination,
                                ResolutionContext context) => //MenuDTO
                                (source_1 == null) ?
                                    (destination == null) ? (MenuDTO)null : destination :
                                    __f(() => {
                                        MenuDTO typeMapDestination = null;
                                        ResolutionContext.CheckContext(ref context);
                                        return ((MenuDTO)context.GetDestination(
                                            source_1,
                                            typeof(MenuDTO))) ??
                                            __f(() => {
                                                typeMapDestination = destination ?? new MenuDTO();
                                                context.CacheDestination(
                                                    source_1,
                                                    typeof(MenuDTO),
                                                    typeMapDestination);
                                                typeMapDestination;
                                                try
                                                {
                                                    typeMapDestination.Id = source_1.Id;
                                                }
                                                catch (Exception ex)
                                                {
                                                    throw TypeMapPlanBuilder.MemberMappingError(
                                                        ex,
                                                        default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
                                                }
                                                try
                                                {
                                                    typeMapDestination.Name = source_1.Name;
                                                }
                                                catch (Exception ex)
                                                {
                                                    throw TypeMapPlanBuilder.MemberMappingError(
                                                        ex,
                                                        default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
                                                }
                                                try
                                                {
                                                    typeMapDestination.Description = source_1.Description;
                                                }
                                                catch (Exception ex)
                                                {
                                                    throw TypeMapPlanBuilder.MemberMappingError(
                                                        ex,
                                                        default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
                                                }
                                                try
                                                {
                                                    List<Menu> resolvedValue = null;
                                                    List<MenuDTO> mappedValue = null;
                                                    resolvedValue = source_1.Children;
                                                    mappedValue = (resolvedValue == null) ?
                                                        new List<MenuDTO>() :
                                                        context.MapInternal<List<Menu>, List<MenuDTO>>(
                                                            resolvedValue,
                                                            (destination == null) ? (List<MenuDTO>)null :
                                                                typeMapDestination.Children,
                                                            (MemberMap)default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
                                                    typeMapDestination.Children = mappedValue;
                                                }
                                                catch (Exception ex)
                                                {
                                                    throw TypeMapPlanBuilder.MemberMappingError(
                                                        ex,
                                                        default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);
                                                }
                                                return typeMapDestination;
                                            });
                                    })))
                            .Invoke(
                                item,
                                (MenuDTO)null,
                                context));
                        }
                        else
                        {
                            goto LoopBreak;
                        }
                    }
                    LoopBreak:;
                }
                finally
                {
                    enumerator.Dispose();
                }
                return collectionDestination;
            }
            catch (Exception ex)
            {
                throw MapperConfiguration.GetMappingError(
                    ex,
                    default(MapRequest)/*NOTE: Provide the non-default value for the Constant!*/);
            }
        }));

5.3 AutoMapper有无循环引用的代码分析如下

  • 循环引用的代码有2段,1段处理Menu,另1段处理List<Menu>
  • 直接对比处理List<Menu>部分
  • 很明显有循环引用部分多了不少特殊代码
5.3.1 AutoMapper循环引用多出以下代码
  • ResolutionContext.CheckContext消耗内存
  • context.GetDestination消耗内存和cpu
  • context.CacheDestination消耗内存和cpu
  • context.MapInternal用于调用代码
5.3.2 AutoMapper代码总结
  • MapInternal用于解决编译死循环的问题
  • GetDestination和CacheDestination用于解决执行死循环的问题
  • 但是这个case没有对象重复引用,没有执行死循环
  • 也就是说这里的GetDestination和CacheDestination只是消耗内存和cpu做无用功
  • 更让人无法接受的是,做这些无用功的消耗居然是正常代码的好几倍
  • 在无循环引用代码中ResolutionContext就是个摆设,无任何作用

6. 执行死循环该怎么处理呢

  • .net序列化给了我们答案
  • 序列化默认不支持对象循环引用,需要特殊配置,这是为了照顾大部分情况下的性能

6.1 序列化对象循环引用代码

csharp 复制代码
Node node9 = new() { Id = 9, Name = "node9" };
Node node8 = new() { Id = 8, Name = "node8", Next = node9 };
Node node7 = new() { Id = 7, Name = "node7", Next = node8 };
Node node6 = new() { Id = 6, Name = "node6", Next = node7 };
Node node5 = new() { Id = 5, Name = "node5", Next = node6 };
Node node4 = new() { Id = 4, Name = "node4", Next = node5 };
Node node3 = new() { Id = 3, Name = "node3", Next = node4 };
Node node2 = new() { Id = 2, Name = "node2", Next = node3 };
Node node1 = new() { Id = 1, Name = "node1", Next = node2 };
node9.Next = node1; // 形成环
var referenceJson = JsonSerializer.Serialize(dto, new JsonSerializerOptions{
   ReferenceHandler = ReferenceHandler.Preserve,
    WriteIndented = true
});
referenceJson.Display();
  • 如果以上代码不配置ReferenceHandler会报错
  • 异常信息为A possible object cycle was detected...

7. 对比Poco循环引用处理的代码

7.1 Poco循环引用处理的代码如下

csharp 复制代码
(Func<Menu, MenuDTO>)((Menu source) => //MenuDTO
{
    MenuDTO dest = null;
    if ((source != (Menu)null))
    {
        dest = new MenuDTO();
        List<Menu> Children = null;
        dest.Id = source.Id;
        dest.Name = source.Name;
        dest.Description = source.Description;
        Children = source.Children;
        if ((Children != null))
        {
            dest.Children = default(CompiledConverter<List<Menu>, List<MenuDTO>>)/*NOTE: Provide the non-default value for the Constant!*/.Convert(Children);
        }
    }
    return dest;
});

7.2 代码对比AutoMapper

  • AutoMapper生成代码量是Poco的3倍多
  • CompiledConverter.Convert对应AutoMapper的context.MapInternal
  • 本case中Poco无多余缓存处理,节省了大量cpu和内存
  • 如果有对象循环引用Poco该怎么办呢

三、再举个环形链表的Case

  • 链表是类型循环引用
  • 环形链表又是对象循环引用
  • 中国传统有九九归一的说法,以此为例

1. 九九归一代码

csharp 复制代码
public class Node
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Node Next { get; set; }
    public static Node GetNode()
    {
        Node node9 = new() { Id = 9, Name = "node9" };
        Node node8 = new() { Id = 8, Name = "node8", Next = node9 };
        Node node7 = new() { Id = 7, Name = "node7", Next = node8 };
        Node node6 = new() { Id = 6, Name = "node6", Next = node7 };
        Node node5 = new() { Id = 5, Name = "node5", Next = node6 };
        Node node4 = new() { Id = 4, Name = "node4", Next = node5 };
        Node node3 = new() { Id = 3, Name = "node3", Next = node4 };
        Node node2 = new() { Id = 2, Name = "node2", Next = node3 };
        Node node1 = new() { Id = 1, Name = "node1", Next = node2 };
        node9.Next = node1; // 形成环
        return node1;
    }
}

2. PocoEmit配置缓存解决对象循环引用问题

  • ComplexCached.Circle表示只有检测到循环引用才开启缓存
  • ComplexCached.Circle策略基本等同AutoMapper
  • 默认是ComplexCached.Never,不开启缓存
csharp 复制代码
var node = Node.GetNode();
var manager = PocoEmit.Mapper.Create(new MapperOptions { Cached = ComplexCached.Circle });
var dto = manager.Convert<Node, NodeDTO>(node);

2.1 执行效果如下:

json 复制代码
{
  "$id": "1",
  "Id": 1,
  "Name": "node1",
  "Next": {
    "$id": "2",
    "Id": 2,
    "Name": "node2",
    "Next": {
      "$id": "3",
      "Id": 3,
      "Name": "node3",
      "Next": {
        "$id": "4",
        "Id": 4,
        "Name": "node4",
        "Next": {
          "$id": "5",
          "Id": 5,
          "Name": "node5",
          "Next": {
            "$id": "6",
            "Id": 6,
            "Name": "node6",
            "Next": {
              "$id": "7",
              "Id": 7,
              "Name": "node7",
              "Next": {
                "$id": "8",
                "Id": 8,
                "Name": "node8",
                "Next": {
                  "$id": "9",
                  "Id": 9,
                  "Name": "node9",
                  "Next": {
                    "$ref": "1"
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

2.2 与AutoMapper性能对比如下

Method Mean Error StdDev Median Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Auto 678.3 ns 12.65 ns 14.06 ns 666.1 ns 1.78 0.04 0.0936 0.0004 1616 B 4.49
AutoFunc 632.7 ns 4.18 ns 4.64 ns 628.8 ns 1.66 0.02 0.0936 0.0004 1616 B 4.49
Poco 381.8 ns 2.66 ns 3.07 ns 382.0 ns 1.00 0.01 0.0208 - 360 B 1.00
PocoFunc 365.4 ns 2.73 ns 2.92 ns 366.9 ns 0.96 0.01 0.0208 - 360 B 1.00
  • 首先可以看出Poco和AutoMapper执行耗时都挺高的
  • 所以建议大家使用AutoMapper尽量避免类型循环引用
  • 使用Poco也建议大家尽量避免对象循环引用
  • Poco性能好不少,差不多2倍
  • 内存分配上Poco优势更明显,AutoMapper分配了4倍多的内存

3. PocoEmit还可以通过GetContextConvertFunc来控制对象缓存

3.1 GetContextConvertFunc调用代码

  • GetContextConvertFunc是强制开启缓存,忽略mapper的缓存配置
  • 并设置当前类型必须缓存
csharp 复制代码
var node = Node.GetNode();
var manager = PocoEmit.Mapper.Create();
Func<IConvertContext, Node, NodeDTO> contextFunc = manager.GetContextConvertFunc<Node, NodeDTO>();
using var context = SingleContext<Node, NodeDTO>.Pool.Get();
var dto = _pocoContextFunc(context, _node);
  • 需要特别强调,context是用来做缓存的,不是专门用来做处理循环引用的
  • 巧的是缓存能解决对象循环引用问题
  • 缓存除了处理对象循环引用当然还有其他用处

3.2 加入GetContextConvertFunc对比如下

Method Mean Error StdDev Median Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Auto 657.2 ns 8.03 ns 8.92 ns 664.7 ns 1.81 0.04 0.0936 0.0004 1616 B 4.49
AutoFunc 621.6 ns 8.09 ns 8.65 ns 614.4 ns 1.71 0.04 0.0936 0.0004 1616 B 4.49
Poco 363.5 ns 6.60 ns 7.60 ns 359.6 ns 1.00 0.03 0.0208 - 360 B 1.00
PocoFunc 349.1 ns 1.51 ns 1.74 ns 348.8 ns 0.96 0.02 0.0208 - 360 B 1.00
PocoContextFunc 350.8 ns 3.15 ns 3.63 ns 350.0 ns 0.97 0.02 0.0208 - 360 B 1.00
  • PocoContextFunc性能和GetConvertFunc差不多
  • 主要影响性能的是缓存的读写
  • 该方法通过暴露IConvertContext参数给自定义和配置提供了想象空间
  • 是不是可以通过实现IConvertContext来实现想要的逻辑和性能

四、重复引用非循环的Case

1. 一个突击小组的代码

  • 小组只有3人
  • 1人做队长
  • 1人做通讯员
csharp 复制代码
public class SoldierTeam
{
    public Soldier Leader { get; set; }
    public Soldier Courier { get; set; }
    public List<Soldier> Members { get; set; }
    public static SoldierTeam GetTeam()
    {
        var leader = new Soldier { Name = "张三" };
        var courier = new Soldier { Name = "李四" };
        var other = new Soldier { Name = "王二" };
        var team = new SoldierTeam
        {
            Leader = leader,
            Courier = courier,
            Members = new List<Soldier>
            {
                leader,
                courier,
                other
            }
        };
        return team;
    }
}
public class Soldier
{
    public string Name { get; set; }
}

2. Poco默认情况下转化为5个对象

  • 这明显并不是用户想要的结果
  • AutoMapper可以通过PreserveReferences配置跟踪引用(就是缓存)
csharp 复制代码
var manager = PocoEmit.Mapper.Create()
    .UseCollection();
var team = SoldierTeam.GetTeam();
var dto = manager.Convert<SoldierTeam, SoldierTeamDTO>(team);
// dtoList.Length == 5
var dtoList = dto.Members.Concat([dto.Leader, dto.Courier]).Distinct().ToArray();

3. Poco配置缓存可以解决问题

  • ComplexCached.Always表示可能需要缓存就开启
  • 实际是检测有类被属性多次引用就开启缓存
  • 或有循环引用也开启缓存

3.1 Poco缓存转化代码

csharp 复制代码
var manager = PocoEmit.Mapper.Create(new MapperOptions { Cached = ComplexCached.Always })
    .UseCollection();
var team = SoldierTeam.GetTeam();
var dto = manager.Convert<SoldierTeam, SoldierTeamDTO>(team);
// dtoList.Length == 3
var dtoList = dto.Members.Concat([dto.Leader, dto.Courier]).Distinct().ToArray();

3.2 Poco生成以下代码

csharp 复制代码
T __f<T>(System.Func<T> f) => f();
(Func<SoldierTeam, SoldierTeamDTO>)((SoldierTeam source) => //SoldierTeamDTO
{
    SoldierTeamDTO dest = null;
    IConvertContext context = null;
    if ((source != (SoldierTeam)null))
    {
        context = ConvertContext.Create();
        if ((source != (SoldierTeam)null))
        {
            dest = new SoldierTeamDTO();
            context.SetCache<SoldierTeam, SoldierTeamDTO>(
                source,
                dest);
            Soldier Leader = null;
            Soldier Courier = null;
            List<Soldier> Members = null;
            Leader = source.Leader;
            if ((Leader != null))
            {
                // { The block result will be assigned to `dest.Leader`
                SoldierDTO dest_1 = null;
                dest.Leader = context.TryGetCache<Soldier, SoldierDTO>(
                    Leader,
                    out dest_1) ? dest_1 : 
                    __f(() => {
                        SoldierDTO dest_2 = null;
                        if ((Leader != (Soldier)null))
                        {
                            dest_2 = new SoldierDTO();
                            context.SetCache<Soldier, SoldierDTO>(
                                Leader,
                                dest_2);
                            dest_2.Name = Leader.Name;
                        }
                        return dest_2;
                    });
                // } end of block assignment;
            }
            Courier = source.Courier;
            if ((Courier != null))
            {
                // { The block result will be assigned to `dest.Courier`
                SoldierDTO dest_3 = null;
                dest.Courier = context.TryGetCache<Soldier, SoldierDTO>(
                    Courier,
                    out dest_3) ? dest_3 : 
                    __f(() => {
                        SoldierDTO dest_4 = null;
                        if ((Courier != (Soldier)null))
                        {
                            dest_4 = new SoldierDTO();
                            context.SetCache<Soldier, SoldierDTO>(
                                Courier,
                                dest_4);
                            dest_4.Name = Courier.Name;
                        }
                        return dest_4;
                    });
                // } end of block assignment;
            }
            Members = source.Members;
            if ((Members != null))
            {
                // { The block result will be assigned to `dest.Members`
                List<SoldierDTO> dest_5 = null;
                dest.Members = context.TryGetCache<List<Soldier>, List<SoldierDTO>>(
                    Members,
                    out dest_5) ? dest_5 : 
                    __f(() => {
                        List<SoldierDTO> dest_6 = null;
                        if ((Members != (List<Soldier>)null))
                        {
                            dest_6 = new List<SoldierDTO>(Members.Count);
                            context.SetCache<List<Soldier>, List<SoldierDTO>>(
                                Members,
                                dest_6);
                            int index = default;
                            int len = default;
                            index = 0;
                            len = Members.Count;
                            while (true)
                            {
                                if ((index < len))
                                {
                                    Soldier sourceItem = null;
                                    SoldierDTO destItem = null;
                                    sourceItem = Members[index];
                                    // { The block result will be assigned to `destItem`
                                        SoldierDTO dest_7 = null;
                                        destItem = context.TryGetCache<Soldier, SoldierDTO>(
                                            sourceItem,
                                            out dest_7) ? dest_7 : 
                                            __f(() => {
                                                SoldierDTO dest_8 = null;
                                                if ((sourceItem != (Soldier)null))
                                                {
                                                    dest_8 = new SoldierDTO();
                                                    context.SetCache<Soldier, SoldierDTO>(
                                                        sourceItem,
                                                        dest_8);
                                                    dest_8.Name = sourceItem.Name;
                                                }
                                                return dest_8;
                                            });
                                        // } end of block assignment;
                                    dest_6.Add(destItem);
                                    index++;
                                }
                                else
                                {
                                    goto forLabel;
                                }
                            }
                            forLabel:;
                        }
                        return dest_6;
                    });
                // } end of block assignment;
            }
        }
        context.Dispose();
    }
    return dest;
});

4. Poco通过GetContextConvertFunc也可以处理

4.1 GetContextConvertFunc转化代码

csharp 复制代码
var manager = PocoEmit.Mapper.Create()
    .UseCollection();
var team = SoldierTeam.GetTeam();
Func<IConvertContext, SoldierTeam, SoldierTeamDTO> func = manager.GetContextConvertFunc<SoldierTeam, SoldierTeamDTO>();
using var context = SingleContext<Soldier, SoldierDTO>.Pool.Get();
var dto = func(context, team);
// dtoList.Length == 3
var dtoList = dto.Members.Concat([dto.Leader, dto.Courier]).Distinct().ToArray();

4.2 Poco生成以下代码

csharp 复制代码
T __f<T>(System.Func<T> f) => f();
(Func<IConvertContext, SoldierTeam, SoldierTeamDTO>)((
    IConvertContext context, 
    SoldierTeam source) => //SoldierTeamDTO
{
    SoldierTeamDTO dest = null;
    if ((source != (SoldierTeam)null))
    {
        dest = new SoldierTeamDTO();
        context.SetCache<SoldierTeam, SoldierTeamDTO>(
            source,
            dest);
        Soldier Leader = null;
        Soldier Courier = null;
        List<Soldier> Members = null;
        Leader = source.Leader;
        if ((Leader != null))
        {
            // { The block result will be assigned to `dest.Leader`
            SoldierDTO dest_1 = null;
            dest.Leader = context.TryGetCache<Soldier, SoldierDTO>(
                Leader,
                out dest_1) ? dest_1 : 
                __f(() => {
                    SoldierDTO dest_2 = null;
                    if ((Leader != (Soldier)null))
                    {
                        dest_2 = new SoldierDTO();
                        context.SetCache<Soldier, SoldierDTO>(
                            Leader,
                            dest_2);
                        dest_2.Name = Leader.Name;
                    }
                    return dest_2;
                });
            // } end of block assignment;
        }
        Courier = source.Courier;
        if ((Courier != null))
        {
            // { The block result will be assigned to `dest.Courier`
            SoldierDTO dest_3 = null;
            dest.Courier = context.TryGetCache<Soldier, SoldierDTO>(
                Courier,
                out dest_3) ? dest_3 : 
                __f(() => {
                    SoldierDTO dest_4 = null;
                    if ((Courier != (Soldier)null))
                    {
                        dest_4 = new SoldierDTO();
                        context.SetCache<Soldier, SoldierDTO>(
                            Courier,
                            dest_4);
                        dest_4.Name = Courier.Name;
                    }
                    return dest_4;
                });
            // } end of block assignment;
        }
        Members = source.Members;
        if ((Members != null))
        {
            // { The block result will be assigned to `dest.Members`
            List<SoldierDTO> dest_5 = null;
            dest.Members = context.TryGetCache<List<Soldier>, List<SoldierDTO>>(
                Members,
                out dest_5) ? dest_5 : 
                __f(() => {
                    List<SoldierDTO> dest_6 = null;
                    if ((Members != (List<Soldier>)null))
                    {
                        dest_6 = new List<SoldierDTO>(Members.Count);
                        context.SetCache<List<Soldier>, List<SoldierDTO>>(
                            Members,
                            dest_6);
                        int index = default;
                        int len = default;
                        index = 0;
                        len = Members.Count;
                        while (true)
                        {
                            if ((index < len))
                            {
                                Soldier sourceItem = null;
                                SoldierDTO destItem = null;
                                sourceItem = Members[index];
                                // { The block result will be assigned to `destItem`
                                    SoldierDTO dest_7 = null;
                                    destItem = context.TryGetCache<Soldier, SoldierDTO>(
                                        sourceItem,
                                        out dest_7) ? dest_7 : 
                                        __f(() => {
                                            SoldierDTO dest_8 = null;
                                            if ((sourceItem != (Soldier)null))
                                            {
                                                dest_8 = new SoldierDTO();
                                                context.SetCache<Soldier, SoldierDTO>(
                                                    sourceItem,
                                                    dest_8);
                                                dest_8.Name = sourceItem.Name;
                                            }
                                            return dest_8;
                                        });
                                    // } end of block assignment;
                                dest_6.Add(destItem);
                                index++;
                            }
                            else
                            {
                                goto forLabel;
                            }
                        }
                        forLabel:;
                    }
                    return dest_6;
                });
            // } end of block assignment;
        }
    }
    return dest;
});

5. 性能测试如下

Method Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
Auto 306.8 ns 10.60 ns 12.21 ns 1.39 0.06 0.0459 792 B 4.12
AutoFunc 259.1 ns 1.32 ns 1.47 ns 1.18 0.02 0.0459 792 B 4.12
Poco 220.2 ns 2.95 ns 3.39 ns 1.00 0.02 0.0111 192 B 1.00
PocoFunc 206.8 ns 2.26 ns 2.61 ns 0.94 0.02 0.0111 192 B 1.00
PocoContextFunc 207.4 ns 2.74 ns 3.15 ns 0.94 0.02 0.0111 192 B 1.00
  • PocoFunc性能和PocoContextFunc性能差不多
  • 如果喜欢隔离配置的同学,可以使用缓存配置方案
  • 如果喜欢集中配置的同学,可以使用GetContextConvertFunc
  • AutoMapper耗时1.4倍,内存占用4倍多

五、总结

1. 与AutoMapper处理循环引用的原理是一样的

  • 用其他对象调用,代替当时尚未编译的代码处理编译死循环
  • 使用缓存解决执行死循环
  • 缓存操作比原本对象转化耗时多太多,请大家慎用缓存

2. AutoMapper处理粗犷一点

  • 所有对象转化都加上下文对象,哪怕完全用不上
  • 检测到循环引用就加读写缓存,拖累到性能
  • 用了AutoMapper如果感觉获取数据慢,可以查一下是否有循环引用
  • 如果AutoMapper转化数据比实际数据库操作还慢也不要太过惊讶
  • 这里链接园内大佬的一篇文章: https://www.cnblogs.com/dudu/p/5863042.html

3. Poco处理就细致的多

  • 只有需要时才加上下文
  • 上下文来自内存池,用完回收复用,节约内存
  • 用户可以通过配置或GetContextConvertFunc选择性开启缓存
  • 自定义IConvertContext可以提供更多想象的空间
  • 另外无论是否开启缓存,Poco的性能都优于AutoMapper

另外源码托管地址: https://github.com/donetsoftwork/MyEmit ,欢迎大家直接查看源码。

gitee同步更新:https://gitee.com/donetsoftwork/MyEmit

如果大家喜欢请动动您发财的小手手帮忙点一下Star,谢谢!!!

相关推荐
阿里嘎多哈基米18 天前
SQL 层面行转列
数据库·sql·状态模式·mapper·行转列
xiangji21 天前
微软.net表达式编译居然有bug?
表达式·expression·emit
xiangji23 天前
PocoEmit字典增强功能
mapper·emit·类型转化
xiangji1 个月前
如何使用PocoEmit.Mapper替代AutoMapper
mapper·emit
xiangji1 个月前
婶可忍叔不可忍的AutoMapper,你还用吗?
automapper
胡斌附体3 个月前
vue父子组件通信的使用, 跟新v-model
vue·v-model·使用场景·emit·子父组件通信·change事件
abcnull4 个月前
mybatis的mapper对应的xml写法
xml·sql·spring·mybatis·mapper
三天不学习10 个月前
C# AutoMapper 10个常用方法总结
c#·automapper·对象映射
Heaven6451 年前
Mapper代理开发
java·mybatis·mapper·代理开发