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发起请求。如下图所示,由于缓存的存在,只要指定的时间类型一样,返回的时间就是一样的。

640?wx_fmt=png

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

在上面的示例演示中,为了让依赖注入框架提供的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"
));
}
}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/313476.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

分布式缓存——一致性哈希算法

本文主要来自&#xff1a;http://www.zsythink.net/archives/1182 摘录防丢失 一致性哈希算法定义 一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希&#xff08;DHT&#xff09;实现算法&#xff0c;设计目标是为了解决因特网中的热点(Hot spot)问题&#xff0c;初…

H264学习_基本数据结构

&#xfeff;&#xfeff;原文地址:http://blog.csdn.net/yangzhongxuan/article/details/8003494 名词解释 场和帧 &#xff1a; 视频的一场或一帧可用来产生一个编码图像。在电视中&#xff0c;为减少大面积闪烁现象&#xff0c;把一帧分成两个隔行的场。 片&#xff1a…

【巴马火麻茶】调节三高、治疗失眠、排毒减肥,轻松get长寿的秘密!

水&#xff0c;无色无味&#xff0c;是人类的生命源泉对于长期坐办公室的程序员朋友来说每日都得喝水但又觉得寡淡的白开水缺少点乐趣于是&#xff0c;茶叶就成了大家泡水的首选今天&#xff0c;队长要为大家介绍一款养生好茶巴马火麻茶每天一杯&#xff0c;轻松get长寿的秘密火…

「合规」震惊!地图可视化竟能如此玩,零门槛,全免费,效果远胜主流作图工具!...

在数据可视化领域&#xff0c;早晚会遇上地图可视化的需求&#xff0c;一个高大上的地图可视化&#xff0c;瞬间拔高整个报告的层次。Excel催化剂有幸接触并将地图可视化完全落地于Excel中完成。相对主流Python、R、PowerBI等作图工具&#xff0c;在易用性、灵活性和功能的强大…

Opencv SVM demo

&#xfeff;&#xfeff;转载自http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/ml/introduction_to_svm/introduction_to_svm.html 支持向量机 (SVM) 是一个类分类器&#xff0c;正式的定义是一个能够将不同类样本在样本空间分隔的超平面。 换句话说&#xff…

BeetleX之FastHttpApi服务使用详解

BeetleX是一个轻量级高性能的开源TCP通讯应用框架&#xff0c;通过BeetleX可以轻松扩展不同场的TCP应用服务和客户端组件。框架开源地址:https://github.com/IKende/BeetleX组件特点高性能TCP通讯组件,轻易实现百万级RPS通讯交互提供完善的会话管理机制和协议分析规则提供简洁的…

康威定律

本文来自&#xff1a;http://www.dockone.io/article/2691 1、概述 微服务架构是一种非常流行的新概念&#xff0c;即便可供以借鉴的经验比较少&#xff0c;当然不能阻挡它成为热门话题与研究对象。 令人惊讶地是&#xff0c;其实微服务的概念早在五十多年前就已经被提出&…

Cannot find or open the PDB file

&#xfeff;&#xfeff;http://blog.chinaunix.net/uid-11765716-id-3074932.html 遇到问题 SVM_demo.exe (Win32): Loaded ...\SVM_demo\Debug\SVM_demo.exe. Symbols loaded. SVM_demo.exe (Win32): Loaded C:\Windows\System32\ntdll.dll. Symbols loaded. SVM_demo.exe…

基于ASP.NET Core 3.0的ABP v0.21已发布

在微软发布仅仅一个小时后, 基于ASP.NET Core 3.0的ABP v0.21也紧跟着发布了.v0.21没有新功能.它只是升级到稳定的ASP.NET Core 3.0. 查看v0.20发行说明以获取新功能,增强功能和错误修复.关于v1.0ABP框架越来越接近v1.0.我们打算在今年10月中旬发布1.0. 现在,我们将完善测试和文…

SOA和微服务

一、面向服务的架构SOA SOA代表了面向服务的架构。 SOA是一种使用松耦合的黑盒子服务构建业务应用的体系架构&#xff0c;这些服务可以通过编排连接在一起以实现特定的功能。 面向服务的架构&#xff08;Service-Oriented Architecture&#xff09;是一种软件体系结构&#x…

[ASP.NET Core 3框架揭秘] 跨平台开发体验: Windows [上篇]

微软在千禧年推出 .NET战略&#xff0c;并在两年后推出第一个版本的.NET Framework和IDE&#xff08;Visual Studio.NET 2002&#xff0c;后来改名为Visual Studio&#xff09;&#xff0c;如果你是一个资深的.NET程序员&#xff0c;相信传统的.NET应用的开发方式已经深深地烙印…

下一代微服务架构基础:ServiceMesh?

最近&#xff0c;ServiceMesh(服务网格) 概念在社区里头非常火&#xff0c;有人提出 2018 年是 ServiceMesh 年&#xff0c;还有人提出 ServiceMesh 是下一代的微服务架构基础。作为架构师&#xff0c;如果你现在还不了解 ServiceMesh 的话&#xff0c;是否感觉有点落伍了&…

ASP.NET Core 2.0 Web API项目升级到ASP.NET Core 3.0概要笔记

本文结构先决条件升级目标框架&#xff08;Target Framework&#xff09;的版本过时的IHostingEnvironment与IApplicationLifetime对象Endpoint Routing与AddMvc中间件Swashbuckle.AspNetCore总结手头有个ASP.NET Core 2.0的项目&#xff0c;打算将里面的依赖包进行一个版本升级…

常见消息队列对比

一、消息队列概述 消息队列中间件是分布式系统中重要的组件&#xff0c;主要解决应用解耦&#xff0c;异步消息&#xff0c;流量削锋等问题&#xff0c;实现高性能&#xff0c;高可用&#xff0c;可伸缩和最终一致性架构。目前使用较多的消息队列有ActiveMQ&#xff0c;Rabbit…

Precision-Recall Curve

原文出自&#xff1a;http://blog.csdn.net/pirage/article/details/9851339 最近一直在做相关推荐方面的研究与应用工作&#xff0c;召回率与准确率这两个概念偶尔会遇到&#xff0c; 知道意思&#xff0c;但是有时候要很清晰地向同学介绍则有点转不过弯来。 召回率和准确率是…

2019 中国.NET 开发者峰会正式启动

2014年微软组织并成立.NET基金会&#xff0c;微软在成为主要的开源参与者的道路上又前进了一步。2014年以来已经有众多知名公司加入.NET基金会&#xff0c;Google&#xff0c;微软&#xff0c;AWS三大云厂商已经齐聚.NET基金会&#xff0c;在平台项目中&#xff0c;.NET平台上有…

聊一聊顺序消息(RocketMQ顺序消息的实现机制)

本文来自&#xff1a;https://www.cnblogs.com/hzmark/p/orderly_message.html 当我们说顺序时&#xff0c;我们在说什么&#xff1f; 日常思维中&#xff0c;顺序大部分情况会和时间关联起来&#xff0c;即时间的先后表示事件的顺序关系。 比如事件A发生在下午3点一刻&#…

如何摆脱「技术思维」的惯性?

大家好&#xff0c;我是Z哥。虽然从标题上看&#xff0c;这篇文章是写给“技术人”的&#xff0c;但从广义上来说&#xff0c;只要你是一位以理性见长的人&#xff0c;那么这篇文章要讲的东西可能会与你有关。先问大家一个问题。如果你现在打算做一件事A&#xff0c;它的目的是…

RocketMq重试及消息不丢失机制

1、消息重试机制 由于MQ经常处于复杂的分布式系统中&#xff0c;考虑网络波动、服务宕机、程序异常因素&#xff0c;很有可能出现消息发送或者消费失败的问题。因此&#xff0c;消息的重试就是所有MQ中间件必须考虑到的一个关键点。如果没有消息重试&#xff0c;就可能产生消息…

cmake编译opencv3.0

本文参照了 http://www.huqiwen.com/2012/11/27/compile-opencv-243-in-visual-studio-2012/ 安装CMake 从CMake的官方网站下载最新版的CMake。http://www.cmake.org/cmake/resources/software.html&#xff0c;选择Windows (Win32 Installer)平台的进行下载。 安装时请勾选…