自己的映射器或有关ExpressionTrees的一些知识

图片

今天我们将讨论如何编写您的AutoMapper 。 是的,我真的很想告诉你,但是我不能。 事实是这样的解决方案非常大,具有反复试验的历史,并且在应用方面也走了很长一段路。 我只能了解其工作原理,为那些想了解“制图者”工作机制的人提供一个起点。 您甚至可以说我们会写我们的自行车。

免责声明


我再次提醒您:我们将编写一个原始映射器。 如果您突然决定修改它并在产品中使用它-不要这样做。 采用现成的解决方案,该解决方案知道该主题领域中的一堆问题,并且已经知道如何解决这些问题。 有一些或多或少的重要原因来编写和使用自行车映射器:

  • 需要一些特殊的自定义。
  • 您需要在自己的条件下获得最佳性能,并准备填充锥体。
  • 您想了解映射器的工作原理。
  • 你就像骑自行车。

所谓的“映射器”一词是什么?


这是负责获取对象并将其转换(复制其值)到另一个对象的子系统。 典型的任务是将DTO转换为业务层对象。 最原始的映射器“遍历”数据源的属性,并将它们与将要输出的数据类型的属性进行比较。 匹配后,将从源中提取值并将其写入对象,这将是转换的结果。 在此过程中的某个地方,很可能仍需要创建此“结果”。

对于使用者,mapper是一种提供以下接口的服务:

public interface IMapper<out TOut> { TOut Map(object source); } 

我强调:这是最原始的界面,以我的观点,它便于解释。 实际上,我们很可能将使用更特定的映射器(IMapper <TIn,TOut>)或更通用的外观(IMapper),后者本身将为指定类型的输入输出对象选择特定的映射器。

天真的实现


注意:即使是简单的mapper实现,也需要ReflectionExpressionTrees的基础知识。 如果您没有按照链接进行操作或没有听到有关这些技术的任何信息,请阅读。 我保证世界永远不会一样。

但是,我们正在编写您自己的映射器。 首先,让我们获取将要输出的数据类型的所有属性( PropertyInfo )(在下文中,我将其称为TOut )。 这很简单:我们知道类型,因为我们正在编写使用TOut类型参数化的泛型类的实现。 接下来,使用Type类的实例,获取其所有属性。

 Type outType = typeof(TOut); PropertyInfo[] outProperties = outType.GetProperties(); 

获取属性时,我省略了功能。 例如,其中某些可能没有设置函数,某些可能被属性标记为忽略,某些可能具有特殊访问权限。 我们正在考虑最简单的选择。

我们走得更远。 能够创建类型为TOut的实例将是很好的,也就是说,我们可以将传入对象“映射”到该对象中。 在C#中,有几种方法可以做到这一点。 例如,我们可以这样做:System.Activator.CreateInstance()。 甚至只是新的TOut(),但是为此您需要为TOut创建一个限制,您不希望在通用接口中这样做。 但是,我们都对ExpressionTrees有所了解,这意味着我们可以这样做:

 ConstructorInfo outConstructor = outType.GetConstructor(Array.Empty<Type>()); Func<TOut> activator = outConstructor == null ? throw new Exception($"Default constructor for {outType.Name} not found") : Expression.Lambda<Func<TOut>>(Expression.New(outConstructor)).Compile(); 

为什么这样 因为我们知道Type类的实例可以提供有关它具有哪些构造函数的信息-这在我们决定开发映射器以便将任何数据传递给构造函数的情况下非常方便。 另外,我们了解了有关ExpressionTrees的更多信息,即它们允许斑块创建和编译代码,然后可以重用它们。 在这种情况下,它实际上是一个类似于()=> new TOut()的函数。

现在,您需要编写主映射器方法,该方法将复制值。 我们将以最简单的方式进行操作:遍历入口处出现的对象的属性,并在传出对象的属性中查找具有相同名称的属性。 如果找到-复制-如果没有-继续。

 TOut outInstance = _activator(); PropertyInfo[] sourceProperties = source.GetType().GetProperties(); for (var i = 0; i < sourceProperties.Length; i++) { PropertyInfo sourceProperty = sourceProperties[i]; string propertyName = sourceProperty.Name; if (_outProperties.TryGetValue(propertyName, out PropertyInfo outProperty)) { object sourceValue = sourceProperty.GetValue(source); outProperty.SetValue(outInstance, sourceValue); } } return outInstance; 

因此,我们完全形成了BasicMapper类。 您可以在这里熟悉他的测试。 请注意,源可以是任何特定类型的对象或匿名对象。

表演和拳击


反射很棒,但是很慢。 而且,频繁使用它会增加内存流量,这意味着它会加载GC,这会进一步降低应用程序的速度。 例如,我们只使用了PropertyInfo.SetValuePropertyInfo.GetValue方法。 GetValue方法返回一个对象,该对象中包装了某个值(装箱)。 这意味着我们从头开始获得了分配。

映射器通常位于需要将一个对象变成另一个对象的位置...不,不是一个,而是很多对象。 例如,当我们从数据库中获取某些东西时。 在这里,我希望看到正常的性能,并且不会在基本操作上丢失内存。

我们该怎么办? ExpressionTrees将再次帮助我们。 事实是.NET允许您“动态”创建和编译代码:我们以对象表示形式对其进行描述,说出将在什么地方使用它...然后对其进行编译。 几乎没有魔术。

编译的映射器


实际上,一切都相对简单:我们已经使用Expression.New(ConstructorInfo)做了新的事情。 您可能已经注意到,静态New方法的调用方式与运算符完全相同。 事实是,几乎所有C#语法都以Expression类的静态方法的形式反映出来。 如果缺少某些东西,则意味着您正在寻找所谓的 “语法糖。”

这是我们将在映射器中使用的一些操作:

  • 变量声明-Expression.Variable(类型,字符串)。 Type参数指示将创建什么类型的变量,而string是变量的名称。
  • 分配-Expression.Assign(表达式,表达式)。 第一个参数是我们分配的,第二个参数是我们分配的。
  • 对对象属性的访问是Expression.Property(Expression,PropertyInfo)。 Expression是属性的所有者,PropertyInfo是通过反射获得的属性的对象表示。

有了这些知识,我们就可以创建变量,访问对象的属性以及为对象的属性分配值。 最有可能的是,我们也了解到ExpressionTree需要编译成Func <object,TOut>形式的委托。 计划是这样的:我们得到一个包含输入数据的变量,创建一个TOut类型的实例,并创建一个将一个属性分配给另一个属性的表达式。

不幸的是,代码不是很紧凑,因此我建议您立即查看CompiledMapper的实现。 我只把关键点带到了这里。

首先,我们创建函数参数的对象表示。 由于它将对象作为输入,因此该对象将是参数。

 var parameter = Expression.Parameter(typeof(object), "source"); 

接下来,我们创建两个变量和一个Expression列表,在其中依次添加赋值表达式。 顺序很重要,因为当我们调用已编译的方法时,这就是命令的执行方式。 例如,我们不能为尚未声明的变量赋值。

此外,以与天真的实现相同的方式,我们遍历类型属性列表,并尝试按名称进行匹配。 但是,我们不是立即分配值,而是创建用于提取值和为每个关联属性分配值的表达式。

 Expression sourceValue = Expression.Property(sourceInstance, sourceProperty); Expression outValue = Expression.Property(outInstance, outProperty); expressions.Add(Expression.Assign(outValue, sourceValue)); 

重要一点:创建所有赋值操作后,我们需要从函数返回结果。 为此,列表中的最后一个表达式应该是Expression,其中包含我们创建的类的实例。 我在此行旁边留下了评论。 为什么与ExpressionTree中的return关键字相对应的行为看起来像这样? 恐怕这是一个单独的问题。 现在,我建议您很容易记住。

好了,到最后,我们必须编译我们构建的所有表达式。 我们对这里有什么兴趣? body变量包含函数的“ body”。 “正常功能”有身体吧? 好吧,我们用大括号括起来。 因此,Expression.Block就是这样。 由于花括号也是一个作用域,因此我们必须在此处传递将在其中使用的变量-在我们的示例中是sourceInstance和outInstance。

 var body = Expression.Block(new[] {sourceInstance, outInstance}, expressions); return Expression.Lambda<Func<object, TOut>>(body, parameter).Compile(); 

在输出中,我们得到Func <object,TOut>,即 可以将数据从一个对象转换为另一个对象的函数。 你问为什么会有这样的困难? 我提醒您,首先,我们希望在复制ValueType值时避免装箱,其次,我们希望放弃PropertyInfo.GetValue和PropertyInfo.SetValue方法,因为它们有些慢。

为什么不拳击呢? 因为编译的ExpressionTree是真实的IL,并且对于运行时来说,它看起来(几乎)类似于您的代码。 为什么“编译映射器”更快? 再说一次:因为它只是普通的IL。 顺便说一下,我们可以使用BenchmarkDotNet库轻松确认速度,并且可以在此处查看基准测试本身。
方法均值失误标准差比例已分配
自动贴图1,291.6我们3.3173我们3.1030我们1.00312.5 KB
Velo_BasicMapper11,987.0美元33.8389我们28.2570我们9.283437.5 KB
Velo_CompiledMapper341.3我们2.8230我们2.6407我们0.26312.5 KB

在“比率”列中,即使与AutoMapper(它是基准,即1)相比,“ CompiledMapper”(CompiledMapper)也显示出非常好的结果。 但是,我们不要高兴:与我们的自行车相比,AutoMapper具有明显更大的功能。 在此板块中,我只是想展示ExpressionTrees比“经典反射方法”要快得多。

总结


我希望我能够证明编写您的映射器非常简单。 Reflection和ExpressionTrees是开发人员用来解决许多不同任务的非常强大的工具。 依赖关系注入,序列化/反序列化,CRUD存储库,构建SQL查询,使用其他语言作为.NET应用程序的脚本-所有这些操作都使用Reflection,Reflection.Emit和ExpressionTrees完成。

映射器呢? Mapper是一个很好的例子,您可以在其中学习所有这些内容。

PS:如果您想要更多的ExpressionTrees,建议阅读有关如何使用此技术制作JSON转换器的信息

Source: https://habr.com/ru/post/zh-CN463961/


All Articles