AOP框架Dora.Interception 3.0 [1]: 编程体验

.NET Core正式发布之后,我为.NET Core度身定制的AOP框架Dora.Interception也升级到3.0。这个版本除了升级底层类库(.NET Standard 2.1)之外,我还对它进行大范围的重构甚至重新设计。这次重构大部分是在做减法,其目的在于使设计和使用更加简单和灵活,接下来我们就来体验一下在一个ASP.NET Core应用程序下如何使用Dora.Interception。

源代码下载
实例1(Console)
实例2(ASP.NET Core MVC + 注册可拦截服务)
实例3(ASP.NET Core MVC + 注册InterceptableServiceProviderFactory)
实例4(ASP.NET Core MVC + 拦截策略)
实例5(ASP.NET Core MVC + 策略脚本化)

一、演示场景

我们依然沿用“缓存”这个应用场景:我们创建一个缓存拦截器,并将其应用到某个方法上。缓存拦截器会将目标方法的返回值缓存起来。在缓存过期之前,提供相同参数列表的方法调用会直接返回缓存的数据,而无需执行目标方法。如下所示是作为缓存键类型的CacheKey的定义,可以看出缓存时针对”方法+参数列表”实施缓存的。

private class Cachekey
{
    public MethodBase Method { get; }
    public object[] InputArguments { get; }

    public Cachekey(MethodBase method, object[] arguments)
    {
        Method = method;
        InputArguments = arguments;
    }
    public override bool Equals(object obj)
    {
        if (!(obj is Cachekey another))
        {
            return false;
        }
        if (!Method.Equals(another.Method))
        {
            return false;
        }
        for (int index = 0; index < InputArguments.Length; index++)
        {
            var argument1 = InputArguments[index];
            var argument2 = another.InputArguments[index];
            if (argument1 == null && argument2 == null)
            {
                continue;
            }

            if (argument1 == null || argument2 == null)
            {
                return false;
            }

            if (!argument2.Equals(argument2))
            {
                return false;
            }
        }
        return true;
    }

    public override int GetHashCode()
    {
        int hashCode = Method.GetHashCode();
        foreach (var argument in InputArguments)
        {
            hashCode = hashCode ^ argument.GetHashCode();
        }
        return hashCode;
    }
}

二、定义拦截器

作为Dora.Interception区别于其他AOP框架的最大特性,我们注册的拦截器类型无需实现某个预定义的接口,因为我们采用基于“约定”的拦截器定义方式。基于约定方式定义的缓存拦截器类型CacheInterceptor定义如下。

public class CacheInterceptor
{
    private readonly IMemoryCache _cache;
    private readonly MemoryCacheEntryOptions _options;
    public CacheInterceptor(IMemoryCache cache, IOptions<MemoryCacheEntryOptions> optionsAccessor)
    {
        _cache = cache;
        _options = optionsAccessor.Value;
    }

    public async Task InvokeAsync(InvocationContext context)
    {
        var key = new Cachekey(context.Method, context.Arguments);
        if (_cache.TryGetValue(key, out object value))
        {
            context.ReturnValue = value;
        }
        else
        {
            await context.ProceedAsync();
            _cache.Set(key, context.ReturnValue, _options);
        }
    }
}

按照约定,拦截器类型只需要定义成一个普通的“公共、实例”类型即可。拦截操作需要定义在约定的InvokeAsync方法中,该方法的返回类型为Task,并且包含一个InvocationContext类型的参数。InvocationContext类型封装了当前方法的调用上下文,我们可以利用它获取当前的方法和输入参数等信息。InvocationContext的ReturnValue 属性表示方法调用的返回结果,CacheInterceptor正式通过设置该属性从而实现将方法返回值进行缓存的目的。

如上面的代码片段所示,在InvokeAsync方法中,我们先判断针对当前的参数参数列表是否具有缓存的结果,如果有的话我们直接将它作为InvocationContext上下文的ReturnValue属性。如果从缓存中找不到对应的结果,在通过调用InvocationContext上下文的ProceedAsync方法执行目标方法(也可能是后续拦截器),并将新的结果缓存起来。

三、依赖注入

Dora.Interception是为.NET Core度身定制的轻量级AOP框架。由于依赖注入已经成为了.NET Core基本的编程方式,所以Dora.Interception和.NET Core的依赖注入框架进行了无缝整合。正因为如此,当我们在定义拦截器的时候可以将依赖服务直接注入到构造函数中。对于上面定义的CacheInterceptor来说,由于我们直接使用的是.NET Core提供的基于内存的缓存框架,所以我们直接将所需的IMemoryCache 服务和提供配置选项的IOptions<MemoryCacheEntryOptions> 服务注入到构造函数中。

除了构造函数注入,我们还支持针对InvokeAsync方法的“方法注入”。也就是说我们可以将上述的两个依赖服务以如下的方式注入到InvokeAsync方法中。

public class CacheInterceptor
{
    public async Task InvokeAsync(InvocationContext context, IMemoryCache cache, IOptions<MemoryCacheEntryOptions> optionsAccessor)
    {
        var key = new Cachekey(context.Method, context.Arguments);
        if (cache.TryGetValue(key, out object value))
        {
            context.ReturnValue = value;
        }
        else
        {
            await context.ProceedAsync();
            cache.Set(key, context.ReturnValue, optionsAccessor.Value);
        }
    }
}

针对拦截器类型的两种依赖注入方式并不是等效的,它们之间的差异体现在服务实例的生命周期上。由于拦截器对象自身属于一个Singleton服务,所以我们不能在它的构造函数中注入一个Scoped服务,否则依赖服务将不能按照期望的方式被释放。Scoped服务只能注入到InvokeAsync方法中,因为该方法注入的服务实例是根据当前Scope的IServiceProvider提供的(对于ASP.NET Core应用来说,就是当前HttpContext上下文的RequestServices)。

四、注册拦截器

AOP的本质对方法调用进行拦截,并在调用目标方法之前执行应用的拦截器,所以我们定义的拦截器最终需要注册到一个或者多个方法上。Dora.Interception刻意将“拦截器”和“拦截器注册”分离开来,因为拦截器具有不同的注册方式。

在类型或者方法上标注特性是我们常用的拦截器注册方式,为此我们为CacheInterceptor定义了如下这个CacheReturnValueAttribute。CacheReturnValueAttribute继承自抽象类型InterceptorAttribute,在重写的Use方法中,我们只需要调用作为参数的IInterceptorChainBuilder对象的Use<TInterceptor>方法将指定的拦截器添加到拦截器链条(同一个方法上可能同时应用多个拦截器)。

[AttributeUsage(AttributeTargets.Method)]
public class CacheReturnValueAttribute : InterceptorAttribute
{
    public override void Use(IInterceptorChainBuilder builder)
    {
        builder.Use<CacheInterceptor>(Order);
    }
}

Use<TInterceptor>方法的泛型参数表示对应拦截器的类型,它的第一个参数表示指定的拦截器在整个链条上的位置。这个值就是InterceptorAttribute的Order属性值。如果拦截器类型构造函数中定义了一些无法通过依赖注入框架提供的参数,我们在调用Use<TInterceptor>方法时可以利用后面的params参数来指定。

如果你觉得将拦截器类型和对应的特性分开定义比较烦,也可以将两者合二为一,我们只需要将InvokeAsync方法按照如下的方式转移到InterceptorAttribute类型中就可以了。由于它自身就是一个拦截器,我们在Use方法中会调用IInterceptorChainBuilder对象非泛型Use方法,并将自身作为第一个参数。

[AttributeUsage(AttributeTargets.Method)]
public class CacheReturnValueAttribute : InterceptorAttribute
{
    public async Task InvokeAsync(InvocationContext context, IMemoryCache cache, IOptions<MemoryCacheEntryOptions> optionsAccessor)
    {
        var key = new Cachekey(context.Method, context.Arguments);
        if (cache.TryGetValue(key, out object value))
        {
            context.ReturnValue = value;
        }
        else
        {
            await context.ProceedAsync();
            cache.Set(key, context.ReturnValue, optionsAccessor.Value);
        }
    }
    public override void Use(IInterceptorChainBuilder builder)
    {
        builder.Use(this, Order);
    }
}

为了能够很直观地看到针对方法返回值的缓存,我们定义了如下这个表示系统时钟的ISystemClock的服务接口。该接口具有唯一的GetCurrentTime方法返回当前的时间,方法参数用于控制行为方法的时间类型(UTC或者Local)。实现类型SystemClock标注了我们定义的InterceptorAttribute特性。

public interface ISystemClock
{
    DateTime GetCurrentTime(DateTimeKind dateTimeKind);
}

public class SystemClock : ISystemClock
{
    [CacheReturnValue(Order = 1)]
    public DateTime GetCurrentTime(DateTimeKind dateTimeKind)
    {
        return dateTimeKind switch
        {
            DateTimeKind.Local => DateTime.UtcNow.ToLocalTime(),
            DateTimeKind.Unspecified => DateTime.Now,
            _ => DateTime.UtcNow,
        };
    }
}

五、注册可被拦截的服务

接下来我们在一个ASP.NET Core MVC应用中演示针对ISystemClock服务提供时间的缓存。如下所示的是应用承载程序和注册Startup类型的定义。为了让依赖注入框架提供的ISystemClock服务是可以被拦截的,我们调用了IServiceCollection接口的AddSingletonInterceptable<TService, TImplementation>扩展方法。由于CacheInterceptor利用.NET Core内存缓存框架来存储方法返回值,所以我们还调用了AddMemoryCache扩展方法注册了相关服务。

public class Program
{
    public static void Main(string[] args)
    {
        Host.CreateDefaultBuilder()
                .ConfigureWebHostDefaults(buider => buider.UseStartup<Startup>())
                .Build()
                .Run();
    }
}

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddMemoryCache()
            .AddInterception()
            .AddSingletonInterceptable<ISystemClock, SystemClock>()
            .AddRouting()
            .AddControllers();
    }

    public void Configure(IApplicationBuilder app)
    {
        app
            .UseRouting()
            .UseEndpoints(endpoints => endpoints.MapControllers());
    }
}

我们定义了如下这个HomeController,并在其构造函数中注入了ISystemClock服务。在Action方法Index中,我们利用ISystemClock服务在1秒时间间隔内两次提供当前时间,并将这两个时间呈现在浏览器上。调用ISystemClock的GetCurrentTime方法指定的时间类型(UTC或者Local)是利用查询字符串提供的。

public class HomeController : Controller
{
    private readonly ISystemClock _clock;
    public HomeController(ISystemClock clock)
    {
        _clock = clock ?? throw new ArgumentNullException(nameof(clock));
    }

    [HttpGet("/{kind?}")]
    public async Task Index(string kind="local")
    {
        DateTimeKind dateTimeKind = string.Compare(kind, "utc", true) == 0
            ? DateTimeKind.Utc
            : DateTimeKind.Local;

        Response.ContentType = "text/html";
        await Response.WriteAsync("<html><body><ul>");
        for (int i = 0; i < 2; i++)
        {
            await Response.WriteAsync($"<li>{_clock.GetCurrentTime(dateTimeKind)}</li>");
            await Task.Delay(1000);
        }
        await Response.WriteAsync("</ul><body></html>");
    }
}

运行程序后,我们利用浏览器对定义在HomeController中的Action方法Index发起请求。如下图所示,由于缓存的存在,只要指定的时间类型一样,返回的时间就是一样的。

六、保留现有的服务注册方式

在上面的示例演示中,为了让依赖注入框架提供的ISystemClock服务能够被拦截,我们不得不调用自定义的AddSingletonInterceptable<TService, TImplementation>扩展方法扩展方法来注册服务。如果你不喜欢这种方式,我们还提供了另一种解决方案,那就是按照如下的方式调用IHostBuilder的UseInterceptableServiceProvider扩展方法注册我们自定义的InterceptableServiceProviderFactory

public class Program
{
    public static void Main(string[] args)
    {
        Host.CreateDefaultBuilder()
                .UseInterceptableServiceProvider()
                .ConfigureWebHostDefaults(buider => buider.UseStartup<Startup>())
                .Build()
                .Run();
    }
}

一旦我们按照上面的当时完成了针对InterceptableServiceProviderFactory的注册之后,我们将可以将针对ISystemClock服务的注册还原成我们熟悉的方式。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddMemoryCache()
            .AddInterception()
            .AddSingleton<ISystemClock, SystemClock>()
            .AddRouting()
            .AddControllers();
    }

    public void Configure(IApplicationBuilder app)
    {
        app
            .UseRouting()
            .UseEndpoints(endpoints => endpoints.MapControllers());
    }
}

七、基于策略的拦截器注册方式

Dora.Interception提供了扩展点使我们可以实现任意的拦截器注册方式。除了默认提供的针对“特性标注”的方式之外,我们还提供了一种针对策略的注册方式。这里的策略旨在提供这样的表达:将某种类型的拦截器应用到某个类型的某个方法或者属性上。如果我们没有将CacheReturnValueAttribute特性标注到SystemClock的GetCurrentTime方法上,我们可以将承载程序修改成如下的形式。

public class Program
{
    public static void Main(string[] args)
    {
        Host.CreateDefaultBuilder()
            .UseInterceptableServiceProvider(configure: Configure)
            .ConfigureWebHostDefaults(buider => buider.UseStartup<Startup>())
            .Build()
            .Run();

        static void Configure(InterceptionBuilder interceptionBuilder)
        {
            interceptionBuilder.AddPolicy(policyBuilder => policyBuilder
                .For<CacheReturnValueAttribute>(order: 1, cache => cache
                    .To<SystemClock>(target => target
                        .IncludeMethod(clock => clock.GetCurrentTime(default)))));
        }
    }
}

如上面的代码片段所示,我们在调用IHostBuilder的UseInterceptableServiceProvider扩展方法的时候指定了一个Action<InterceptionBuilder>对象,它通过调用InterceptionBuilder 对象的AddPolicy扩展方法通过明确的语义将CacheReturnValueAttribute应用到SystemClock的GetCurrentTime方法上。由于不论是指定类型还是方法都是采用“强类型”的方式,所以有效避免了出错的可能性。

八、策略脚本化

如果希望在不修改现有程序代码的前提下自由的修改拦截策略,我们可以将策略脚本化。在这里我们使用的脚本语言就是C#,所以我们可以将上面提供的策略代码放在一个C#脚本中。比如我们在根目录下创建一个interception.dora文件,并在其中定义如下的策略。

policyBuilder
    .For<CacheReturnValueAttribute>(1, cache => cache
        .To<SystemClock>(clock => clock
            .IncludeMethod(it => it.GetCurrentTime(default))));

为了使用这个策略脚本,我们需要对承载程序作相应修改。如下面的代码片段所示,我们同样调用了InterceptionBuilder 的AddPolicy方法,但是这次我们指定的是策略脚本文件名。为了能够识别脚本文件中的类型,我们提供了一个Action<PolicyFileBuilder>对象,并调用PolicyFileBuilder的AddReferences方法添加了程序集引用,调用AddImports方法导入了命名空间。

public class Program
{
    public static void Main(string[] args)
    {
        Host.CreateDefaultBuilder()
            .UseInterceptableServiceProvider(configure: Configure)
                .ConfigureWebHostDefaults(buider => buider.UseStartup<Startup>())
                .Build()
                .Run();

        static void Configure(InterceptionBuilder interceptionBuilder)
        {
            interceptionBuilder.AddPolicy("Interception.dora", script => script
                .AddReferences(Assembly.GetExecutingAssembly())
                .AddImports("App"));
        }
    }
}

原文地址:https://www.cnblogs.com/artech/p/dora-interception-3-01.html

时间: 2024-10-07 08:02:24

AOP框架Dora.Interception 3.0 [1]: 编程体验的相关文章

AOP框架Dora.Interception 3.0 [3]: 拦截器设计

对于所有的AOP框架来说,多个拦截器最终会应用到某个方法上.这些拦截器按照指定的顺序构成一个管道,管道的另一端就是针对目标方法的调用.从设计角度来将,拦截器和中间件本质是一样的,那么我们可以按照类似的模式来设计拦截器. 一.InvocationContext 我们为整个拦截器管道定义了一个统一的执行上下文,并将其命名为InvocationContext.如下面的代码片段所示,我们可以利用InvocationContext对象得到方法调用上下文的相关信息,其中包括两个方法(定义在接口和实现类型),

AOP框架Dora.Interception 3.0 [4]: 基于特性的拦截器注册

按照单一职责的原则,拦截器只负责需要的拦截操作的执行,至于它采用何种方式应用到目标方法上,以及它在整个拦截器管道中的位置则属于“拦截器注册”的范畴.Dora.Interception提供了几种典型的注册方法,用户也可以根据自己的需求实现自己的注册方式. 一.IInterceptorProvider 一般来说,每一个拦截器类型都对应着一个IInterceptorProvider实现类型,后者利用其Use方法负责将前者放置到拦截器管道指定的位置.如下面的代码所示,IInterceptorProvid

Dora.Interception,为.NET Core度身打造的AOP框架 [1]:更加简练的编程体验

很久之前开发了一个名为Dora.Interception的开源AOP框架(github地址:https://github.com/jiangjinnan/Dora,如果你觉得这个这框架还有那么一点价值,请不吝多点一颗星),最近对它作了一些改进(包括编程模式和性能,目前最新版本2.1.4).一直以来我对软件设计秉承的一个理念就是:好的设计应该是简单的设计.和其他AOP框架相比,虽然Dora.Interception提供的编程模式已经显得足够简单,但是我觉得还应该再简单点,再简单点.这个新版本对拦截

Dora.Interception, 为.NET Core度身打造的AOP框架[4]:演示几个典型应用

为了帮助大家更深刻地认识Dora.Interception,并更好地将它应用到你的项目中,我们通过如下几个简单的实例来演示几个常见的AOP应用在Dora.Interception下的实现.对于下面演示的实例,它们仅仅是具有指导性质的应用,所以我会尽可能地简化,如果大家需要将相应的应用场景移植到具体的项目开发中,需要做更多的优化.源代码从这里下载. 目录一.对输入参数的格式化二.对参数的自动化验证三.对方法的返回值进行自动缓存 一.对输入参数的格式化 我们有一些方法对输入参数在格式上由一些要求,但

Dora.Interception,为.NET Core度身打造的AOP框架 [2]:以约定的方式定义拦截器

上一篇<更加简练的编程体验>提供了最新版本的Dora.Interception代码的AOP编程体验,接下来我们会这AOP框架的编程模式进行详细介绍,本篇文章着重关注的是拦截器的定义.采用“基于约定”的Interceptor定义方式是Dora.Interception区别于其他AOP框架的一个显著特征,要了解拦截器的编程约定,就得先来了解一下Dora.Interception中针对方法调用的拦截是如何实现的. 一.针对实例的拦截 总地来说,Dora.Interception针对方法调用的拦截机制

Dora.Interception,为.NET Core度身打造的AOP框架 [4]:与依赖注入框架的无缝集成

Dora.Interception最初的定位就是专门针对.NET Core的AOP框架,所以在整个迭代过程中我大部分是在做减法.对于.NET Core程序开发来说,依赖注入已经成为无处不在并且“深入骨髓”的东西,不论是在进行业务应用的开发,还是进行基础组件的开发,依赖注入是实现“松耦合”最为理想的方式(没有之一).对于绝大部分AOP框架来说,它们最终都会体现为创建一个能够拦截的“代理对象”来实现对方法调用的拦截,但是.NET Core中针对服务实例的提供完全由通过IServiceProvider

Dora.Interception,为.NET Core度身打造的AOP框架 [5]:轻松地实现与其他AOP框架的整合

这里所谓的与第三方AOP框架的整合不是说改变Dora.Interception现有的编程,而是恰好相反,即在不改变现有编程模式下采用第三方AOP框架或者自行实现的拦截机制.虽然我们默认提供基于IL Emit实现方式,并且对IL指令进行了深度的优化,但是如果我们真的具有更好的选择,我们可以通过简单的扩展完成对底层拦截机制改变. 一.IInterceptingProxyFactory 对于Dora.Interception来说,方法调用之所有能够被拦截的根源在于我们改变了服务实例的提供方式,原来的对

C# 6 与 .NET Core 1.0 高级编程 - 38 章 实体框架核心(上)

译文,个人原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 38 章 实体框架核心(上)),不对的地方欢迎指出与交流. 章节出自<Professional C# 6 and .NET Core 1.0>.水平有限,各位阅读时仔细分辨,唯望莫误人子弟. 附英文版原文:Professional C# 6 and .NET Core 1.0 - 38 Entity Framework Core ------------------------------- 本章内容 En

spring框架(2)— 面相切面编程AOP

spring框架(2)- 面相切面编程AOP AOP(Aspect Oriented Programming),即面向切面编程. 可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善.OOP引入封装.继承.多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合.不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能.日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性.异常处