65.9K
CodeProject 正在变化。 阅读更多。
Home

Mimick - 一个Fody面向切面编织框架

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2019年1月23日

CPOL

16分钟阅读

viewsIcon

18676

一个用于自动化依赖注入、契约验证和自定义面向切面的装饰器实现的托管库

Mimick

Mimick框架的源代码可在GitHub上找到,最新版本可从NuGet安装,如下所示:

如果未使用NuGet进行包管理,GitHub页面上还包含了构建说明,即在解决方案的基目录中运行以下命令:

dotnet publish -c Release

引言

Mimick框架是一个面向切面的(AOP)编织工具,旨在减轻设计和实现应用程序结构过程中一些重复且耗时的任务。该框架提供了几个有用的实用程序来协助开发应用程序,包括:

  • 契约验证
  • 配置解析
  • 依赖注入
  • 计划任务

本文旨在提供框架的背景信息,介绍编织的工作原理,以及框架如何在应用程序中配置和使用。

该框架同时支持.NET 4.6.1及更高版本,以及.NET Standard 2.0及更高版本。

背景

该框架的设计源于我当时使用Xamarin开发的一个小型移动项目。我发现自己希望自动化各种不同的任务,例如自动解析服务实现和验证方法参数。在试图寻找提供这些功能的现有库时,我发现了Fody编织框架,并开始使用各种插件组合来完成我的需求。

工作完成后不久,我开始研究开发自己的一些插件的变体。更具体地说,我想知道是否可以构建一个框架,提供一套可重用且能够与现有插件结合使用的良好功能。

Fody

Mimick框架利用Fody编织框架来实现面向切面的装饰器支持。Fody是一个库,当安装到项目后,会引入一个构建任务,该任务在项目二进制文件编译完成后执行,并允许修改(或“编织”)二进制文件以包含修改。

安装

Mimick框架的安装可以通过NuGet实现。可以通过Visual Studio包管理器界面安装FodyMimick.Fody包,或者在包管理器控制台中运行以下命令:

Install-Package Fody
Install-Package Mimick.Fody

安装完包后,为了让Fody能够正确找到并运行Mimick编织器,必须在项目目录的根目录下存在一个文件。如果不存在,请在目录根目录下创建一个名为FodyWeavers.xml的XML文档,并确保该文件包含以下内容:

<?xml version="1.0" encoding="utf-8"?>
<Weavers>
  <Mimick />
</Weavers>

完成上述步骤后,重新构建项目并确保项目成功构建。构建输出窗口中可能会显示有关Fody的消息,但如果这些消息不存在,可能是因为构建输出的详细程度设置已隐藏它们。

配置

Mimick框架需要一些小的配置才能完全运行。如果框架仅用于契约验证属性,则不需要此配置;但是,如果打算使用任何其他功能(如依赖注入、配置解析、调度),则必须配置框架。

框架的配置应在应用程序的入口点进行。这可能会因正在开发的应用程序类型而异。例如,这可能是一个Main()方法,或者一个OnStartup()方法等。

要开始配置框架,请收集与正在运行的应用程序相关联的框架的当前实例。单个框架实例将存在于应用程序的整个生命周期内,直到被释放。

IFrameworkContext context = FrameworkContext.Current;

IFrameworkContext接口包含用于配置框架的属性,如下所述。

配置上下文

框架的配置上下文用于设置可以从中解析配置值的源。该上下文包含一个Register方法,应使用该方法引入源。

Register方法接受IConfigurationSource的实现作为参数。有一些可用的配置源,还可以通过添加附加的NuGet包来获得其他配置源。

KeyValueConfigurationSource

这是一个基本源,它从IDictionary<string, string>实现中读取值。源字典被认为是可变的,因此配置值可以在配置源的生命周期内更改。

var values = new Dictionary<string, string>();
values.Add("A", "Apple");
values.Add("B", "Banana");

context.ConfigurationContext.Register(new KeyValueConfigurationSource(values));

XmlConfigurationSource

这是一个源,它处理XML文档的内容(可以来自文件、流或XmlDocument值),并将文档的元素公开为配置源。从XML配置源解析配置值时,必须使用XPath来标识包含该值的元素或属性。

<?xml version="1.0" encoding="utf-8"?>
<Config>
  <Id>1234</Id>
  <Application Name="Test" Version="1.0" />
</Config>
context.ConfigurationContext.Register(new XmlConfigurationSource("document.xml"));

var id = context.Resolve("//Config/Id");
var name = context.Resolve("//Config/Application/@Name");

JsonConfigurationSource

这是一个源,它处理JSON文档的内容(可以来自文件或流),并将文档的对象公开为配置源。从JSON配置源解析配置值时,应使用点(.)分隔配置的完整路径。

{ "Configuration": { "Id": "1234", "Name": "Test" } }
context.ConfigurationContext.Register(new JsonConfigurationSource("document.json"));

var id = context.Resolve("Configuration.Id");
var name = context.Resolve("Configuration.Name");
注意

JSON配置仅通过安装独立的Mimick.Config.Json包可用,该包依赖于Newtonsoft.Json包。

YamlConfigurationSource

这是一个源,它处理YAML文档的内容(可以来自文件或流)。该源应以与JSON配置源相同的方式使用。

Configuration
  Id: 1
  Name: Test
context.ConfigurationContext.Register(new YamlConfigurationSource("document.yaml"));

var id = context.Resolve("Configuration.Id");
var name = context.Resolve("Configuration.Name");
注意

YAML配置仅通过安装独立的Mimick.Config.Yaml包可用,该包依赖于YamlDotNet.Signed包。

组件上下文

框架的组件上下文用于将类注册为默认依赖项容器的依赖项。可以通过调用IFrameworkContext.SetComponentContext方法来替换组件上下文,这对于引入自定义依赖项容器非常有用。

该上下文包含注册和解析依赖项的方法。框架的依赖项应在应用程序启动时、在任何类使用之前进行注册。

var components = context.ComponentContext;

components.Register<Service>();
components.Register<IService, Service>();

components.Register(typeof(Service));
components.Register(typeof(IService), typeof(Service));

components.Register(new Service());
components.Register<IService>(new Service());

该上下文还会尝试自动解析接口契约及其具体实现类。因此,如果您有一个实现服务接口的服务类,上下文将推断出这一点并在后台注册绑定。上下文不会绑定到标准.NET和netstandard库中定义的接口。

该上下文还支持程序集的注册,在框架初始化时会扫描这些程序集,查找所有用ComponentConfiguration属性装饰的类。可以通过提供类型引用或直接注册程序集来注册程序集。

var components = context.ComponentContext;

components.RegisterAssembly<Program>();
components.RegisterAssembly(Assembly.GetExecutingAssembly());

该上下文还接受组件名称,因此可以注册相同类或接口的一个或多个实例。稍后可以使用命名限定符进行解析。

var components = context.ComponentContext;

components.Register<IService, Service1>("svc1");
components.Register<IService, Service2>("svc2");

组件也可以使用类实现上的Component属性进行注册,如下面的属性部分所述。

初始化

配置和组件上下文配置完成后,必须初始化框架才能正确填充依赖项容器并启动任何计划任务。这通过调用框架上下文实例上的Initialize方法来实现。

context.Initialize();

关机

当应用程序终止时,建议关闭框架以释放任何后台任务并释放可能需要处置的任何组件。框架实现了IDisposable接口,该接口会级联到各个上下文。

context.Dispose();

属性

框架公开了许多不同的属性,其中一些专门用于框架的利用,另一些则引入了AOP来自动化某些行为。以下部分列出了一些核心属性。

配置

指示关联类是配置提供程序。任何用此属性装饰的类,如果其包含的程序集已在组件上下文中注册,则在框架初始化期间会被解析,以提供配置值和组件定义。

[Configuration]
public class ApplicationConfiguration
{

}

组件 (Component)

指示关联类是依赖注入系统的组件。该属性也可以应用于类中用Configuration属性装饰的方法和属性,以指示成员的返回值是组件。

[Component]
public class Service
{

}

-

[Configuration]
public class ApplicationConfiguration
{
    [Component]
    public Service Service { get; }

    [Component]
    public Service GetService() { }
}

该属性支持提供一个作用域,该作用域决定了依赖项在框架中的持久性,以及一组可选的名称。

提供

指示关联的方法或属性生成一个配置值。这允许通过配置上下文源扩展配置。该属性需要一个配置名称,稍后可以解析。该属性仅在用Configuration属性装饰的类中检测到。

[Configuration]
public class ApplicationConfiguration
{
    [Provide("application.name")]
    public string Name { get; }

    [Provide("application.time")]
    public DateTime GetTime() => DateTime.Now;
}

自动装配

指示关联的属性、参数或字段应使用组件上下文中存在的依赖项进行填充。成员将在首次访问时被填充,类似于延迟填充。如果应用于方法,则该属性适用于所有可选参数。

public class Service
{
    [Autowire]
    private Dependency dependency;

    [Autowire]
    public Dependency Dependency { get; set; }

    [Autowire]
    public Service(Dependency dependency = null) { }

    public void Execute([Autowire] Dependency dependency = null) { }
}

指示关联的属性、参数或字段应使用常量、动态或变量值进行填充。该属性需要一个表达式,该表达式将在成员首次访问时进行评估。

在解析配置值时,请确保配置名称用大括号({})括起来,并在其中放置配置名称。

public class Service
{
    [Value("Test")]
    private String text; // "Test"

    [Value("123")]
    private int number; // 123

    [Value("1 + 2 + 3")]
    private int added; // 6

    [Value("1 + 3 * (8 / 4) - 1 / 2 + 4")]
    private double complex; // 10.5

    [Value("'Value is ' + (1 + 2)")]
    private String concatenated;

    [Value("{application.name}")]
    private String name;
}

请注意,您也可以直接使用Value类来解析和评估表达式。这可以通过在构造函数中提供表达式来实现:

using Mimick.Values;

var value = new Value("10 + 15 * 2 - {x} + 5");
value.Variables.First(x => x.Expression == "x").Value = 20;

var result = value.Evaluate(); // 25

更多

还有更多可用的属性,可以在GitHub属性页面上找到文档。

调度器

调度器系统作为配置上下文的一部分实现。当Mimick框架初始化并发现组件类时,框架会注册这些类,然后扫描它们以查找用Scheduled属性装饰的方法。

Scheduled属性支持以毫秒为单位指定的时间间隔,该间隔表示自框架完成初始化以来应调用方法的间隔。计划支持Cron表达式,但框架所使用的库没有签名包。

[Component]
public class BackgroundTasks
{
    [Scheduled(60000)]
    public static void ExecuteStatic()
    {
        Console.WriteLine($"I have been executed statically");
    }

    [Scheduled(90000)]
    public void Execute()
    {
        Console.WriteLine($"I have been executed against the component instance");
    }
}

所有计划的方法都会被放入一个任务上下文中,该上下文会启动一个后台线程来监视需要执行的任务(名为“Mimick Schedule Thread”),该线程会频繁轮询计划方法的集合以执行。当方法准备好执行时,该方法会通过TaskFactory.StartNew()调用来调用。

需要注意的是,调用永远不会重叠,而是会在上次调用成功后的间隔执行。例如,如果一个计划方法的时间间隔为30秒,但执行需要40秒,则第二次调用将在框架初始化后的100秒发生(30秒初始间隔 + 40秒处理时间 + 30秒间隔)。

装饰器

Mimick提供了可用于自定义装饰器和拦截器模式的接口。属性应实现一个反映预期操作的接口。例如,一个预期验证属性或参数值的属性应分别实现相关的IPropertySetInterceptorIParameterInterceptor接口。

有一些基本的属性和接口可用于自定义装饰器或拦截器的实现方式以及可用信息。

CompilationOptions

一个属性,当用于装饰器或拦截器属性时,提供用于自定义属性在编译时如何集成的附加选项。

[CompilationOptions(Scope = AttributeScope.Instanced)]
public class InterceptorAttribute : Attribute, IMethodInterceptor { }

CopyArguments属性指示在拦截后,任何更新的方法参数或返回值是否应复制回其原始变量。此选项默认禁用,因此当打算引入一个更改或解析值的拦截器时,可能需要启用此选项。

Scope属性确定拦截器属性的实例化方式。应尽早缓存属性,以防止不必要的减速。可用的选项是:

  • Adhoc:每次需要时都创建属性:从不缓存。
  • Instanced:在创建新对象实例时创建一次属性。
  • MultiInstanced:在创建新对象实例时,每个成员创建一个属性。
  • Singleton:每个应用程序创建一个属性。
  • MultiSingleton:每个成员每个应用程序创建一个属性。

CompilationImplements

一个属性,当用于装饰器或拦截器属性时,指示该属性应实现与成员关联的类上的一个接口。该属性需要一个接口类型,并且还期望关联的属性实现相同的接口。

[CompilationImplements(typeof(IComparable))]
public class ComparableAttribute : Attribute, IComparable
{
    public int CompareTo(object obj) { .. }
}

当一个类被编译并具有实现属性时,会添加接口方法,并将实现路由到属性。一个被编译的类可能看起来像:

public class CompareClass : IComparable
{
    private readonly IComparable __attribute = new ComparableAttribute();

    public int CompareTo(object obj) => __attribute.CompareTo(obj);
}

IInstanceAware

一个接口,当由装饰器或拦截器属性实现时,指示在创建属性时应使用当前对象实例的实例引用来更新属性。Instance属性会更新为当前对象实例,除非属性被作用域为单例或多例。

public class InterceptorAttribute : Attribute, IInstanceAware
{
    public object Instance { get; set; }
}

请记住,Instance属性的值将在属性构造函数调用后才会被赋值。

IMemberAware

一个接口,当由装饰器或拦截器属性实现时,指示应使用有关与属性关联的成员的信息来更新属性。Member属性会使用成员信息进行更新。如果成员是参数,则值不会被设置。该接口应仅与作用域为adhoc或multi-instanced的属性一起使用。

public class InterceptorAttribute : Attribute, IMemberAware
{
    public MemberInfo Member { get; set; }
}

请记住,Member属性的值将在属性构造函数调用后才会被赋值。

IRequireInitialization

一个接口,当由装饰器或拦截器属性实现时,指示该属性在构造后需要进一步初始化。Initialize方法将在构造函数、IInstanceAwareIMemberAware属性赋值之后调用。

public class InterceptorAttribute : Attribute, IRequireInitialization
{
    public void Initialize() { }
}

IParameterInterceptor

这是一个拦截器接口,用于在方法体开始处拦截参数。实现此接口的属性可以与方法和参数关联,前者适用于方法的所有参数。

拦截器事件参数包含有关对象实例和被拦截参数的信息。此外,Value属性包含调用方法时参数的值。此值可以更新为新值,但需要启用CopyArguments标志才能将此更改级联到方法。

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter)]
public class InterceptorAttribute : Attribute, IParameterInterceptor
{
    public void OnEnter(ParameterInterceptionArgs e)
    {
        var message = $"Parameter {e.Parameter.Name} has a value of {e.Value}";
    }

    public void OnException(ParameterInterceptionArgs e, Exception ex)
    {
        ..
    }

    public void OnExit(ParameterInterceptionArgs e)
    {
        ..
    }
}

IMethodInterceptor

这是一个拦截器接口,用于拦截方法调用。实现此接口的属性可以与方法、参数或类关联,后者适用于所有方法。

拦截器事件参数包含有关对象实例和被拦截方法的信息。Arguments属性可用于获取或设置传递给方法的参数,但必须启用CopyArguments标志才能将任何更改级联到方法。

Cancel属性指示方法不再需要进一步处理。这不会阻止其他拦截器执行,但会跳过方法体的处理。

Return属性包含方法的返回结果。在OnEnter方法中,这将始终是返回类型的默认值。在OnExit方法中,这将是从方法体返回的值。与Arguments属性类似,此值可以更新,并将在方法返回之前生效。如果方法不返回值,则此值将始终为null

[AttributeUsage(AttributeTargets.Method)]
public class InterceptorAttribute : Attribute, IMethodInterceptor
{
    public void OnEnter(MethodInterceptionArgs e)
    {
        var message = $"Method {e.Method.Name} has been intercepted";
    }

    public void OnException(MethodInterceptionArgs e, Exception ex)
    {
        ..
    }

    public void OnExit(MethodInterceptionArgs e)
    {
        var message = $"Method {e.Method.Name} is returning {e.Return}";
    }
}

IMethodReturnInterceptor

这是一个拦截器接口,用于在方法体返回之前拦截方法体。实现此接口的属性可以与方法返回值关联。例如:

public class Example
{
    [return: Interceptor]
    public int GetId() => 1;
}

拦截器事件参数包含有关对象实例和被拦截方法的信息。Return属性遵循与IMethodInterceptor相同的条件。

[AttributeUsage(AttributeTargets.ReturnValue)]
public class InterceptorAttribute : Attribute, IMethodReturnInterceptor
{
    public void OnReturn(MethodReturnInterceptionArgs e)
    {
        var message = $"Method {e.Method.Name} is returning {e.Return}";
    }
}

IPropertyGetInterceptor

这是一个拦截器接口,用于拦截属性getter方法。实现此接口的属性可以与属性或字段关联,后者将转换为属性,并且所有引用都将被替换为对新属性的引用。

拦截器事件参数包含有关对象实例和被拦截属性的信息。在OnExit方法中,Value属性可以更新为新值,以使该值从属性返回,并将其级联到属性的setter(如果适用)。

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class InterceptorAttribute : Attribute, IPropertyGetInterceptor
{
    public void OnGet(PropertyInterceptionArgs e)
    {
        var message = $"Property {e.Property.Name} getter is called";
    }

    public void OnException(PropertyInterceptionArgs e, Exception ex)
    {
        ..
    }

    public void OnExit(PropertyInterceptionArgs e)
    {
        var message = $"Property {e.Property.Name} is returning {e.Value}";
    }
}

IPropertySetInterceptor

这是一个拦截器接口,用于拦截属性setter方法。实现此接口的属性可以与属性或字段关联,遵循与IPropertyGetInterceptor相同的规则。

拦截器事件参数包含有关对象实例和被拦截属性的信息。在OnSet方法中,Value属性可以更新为新值,以使该值级联到setter。

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class InterceptorAttribute : Attribute, IPropertySetInterceptor
{
    public void OnSet(PropertyInterceptionArgs e)
    {
        var message = $"Property {e.Property.Name} is being set to {e.Value}";
    }


    public void OnException(PropertyInterceptionArgs e, Exception ex)
    {
        ..
    }

    public void OnExit(PropertyInterceptionArgs e)
    {
        ..
    } 
}

示例

让我们为特定需求创建一个属性。我们需要一个属性来拦截字段、属性和参数,如果它们是有效的数字类型,则将值替换为随机数。考虑到我们需要拦截这些成员,我们必须利用IParameterInterceptor(用于参数)和IPropertyGetInterceptor(用于字段和属性)接口。

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Method | 
AttributeTargets.Parameter | AttributeTargets.Property)]
public class RandomAttribute : Attribute, IParameterInterceptor, IPropertyGetInterceptor
{
}

该属性必须支持更改被拦截成员的值,因此我们必须启用CopyArguments标志,以确保在编译程序集时能够捕获到这一点。我们还知道该属性不需要任何参数或成员特定的选项,因为它应该更新任何数字类型,因此该属性可以存储为单例。

[CompilationOptions(CopyArguments = true, Scope = AttributeScope.Singleton)]
...

接下来,该属性应保留对Random类的引用,以便在所有调用中都能使用它。Random类不是线程安全的,因此应引入一个伴随方法来生成下一个随机值。由于该属性将被创建为单例,因此我们可以安全地引入一个字段来存储值。

private readonly Random random = new Random();

private double GetNextRandom(double min, double max)
{
    lock (random) return min + (random.NextDouble() * (max - min));
}

接下来,需要实现用于生成新值的方法。此方法将定义一次,并在IParameterInterceptorIPropertyGetInterceptor方法中同时使用。

private object GetGenerated(Type type)
{
    switch (Type.GetTypeCode(type))
    {
        case TypeCode.Byte:
            return (byte)GetNextRandom(byte.MinValue, byte.MaxValue);
        case TypeCode.SByte:
            return (sbyte)GetNextRandom(sbyte.MinValue, sbyte.MaxValue);
        case TypeCode.Int16:
            return (short)GetNextRandom(short.MinValue, short.MaxValue);
        ...
    }

    return null;
}

最后,需要实现拦截方法来支持新逻辑。GetGenerated方法的默认行为是,如果提供的类型不是兼容的数字,则返回null,因此我们可以使用它来检查是否真的需要更新被拦截成员的值。

// IParameterInterceptor.OnEnter
public void OnEnter(ParameterInterceptionArgs e)
{
    var value = GetGenerated(e.Parameter.ParameterType);

    if (value != null)
        e.Value = value;
}

// IPropertyGetInterceptor.OnExit
public void OnExit(PropertyInterceptionArgs e)
{
    var value = GetGenerated(e.Property.PropertyType);

    if (value != null)
        e.Value = value;
}

这样就完成了拦截器。其他IParameterInterceptorIPropertyGetInterceptor方法也应实现,但可以留空,因为冗余或空方法会自动内联。如果该属性现在用于字段、属性或参数,则每次访问成员时,其值都将生成一个随机值。

摘要

本文旨在提供有关配置和使用Mimick框架的一些背景知识。该框架旨在用作应用程序的基础层,并尽可能多地公开选项来提供这一点。如果您发现框架有任何问题,请在GitHub项目上提交一个问题,或在此处留下评论。

历史

  • 2020年6月10日 - 更新了存储库链接
  • 2019年1月18日 - 文章的初始版本
© . All rights reserved.