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,谢谢!!!

相关推荐
七夜zippoe7 天前
Spring与MyBatis整合原理及事务管理
java·spring·mybatis·事务·mapper
七夜zippoe9 天前
MyBatis核心源码解析 从SqlSession到Mapper接口的绑定过程
java·mybatis·mapper·sqlsession·缓存机制
阿里嘎多哈基米4 个月前
SQL 层面行转列
数据库·sql·状态模式·mapper·行转列
xiangji4 个月前
微软.net表达式编译居然有bug?
表达式·expression·emit
xiangji4 个月前
PocoEmit字典增强功能
mapper·emit·类型转化
xiangji4 个月前
如何使用PocoEmit.Mapper替代AutoMapper
mapper·emit
xiangji4 个月前
婶可忍叔不可忍的AutoMapper,你还用吗?
automapper
胡斌附体7 个月前
vue父子组件通信的使用, 跟新v-model
vue·v-model·使用场景·emit·子父组件通信·change事件
abcnull7 个月前
mybatis的mapper对应的xml写法
xml·sql·spring·mybatis·mapper