Norns.Urd 轻量级 AOP 框架
Norns.Urd 是一个基于 emit 实现的轻量级 AOP 框架,它支持动态代理。
欢迎来到 Norns.Urd
Github: https://github.com/fs7744/Norns.Urd
Norns.urd
是一个基于 emit 实现的轻量级 AOP 框架,它支持动态代理。
它基于 netstandard2.0。
完成这个框架的主要目的是出于以下个人愿望:
- 一次性实现静态 AOP 和动态 AOP
- 如何让一个 AOP 框架不仅仅只做动态代理,还能与
Microsoft.Extensions.DependencyInjection
等其他 DI 框架协同工作? - 如何让 AOP 同时兼容同步和异步方法,并将实现选项完全留给用户?
希望这个库能对您有所帮助。
顺便说一句,如果您不熟悉 AOP,可以看看这篇文章:面向切面编程。
简单的基准测试
这只是一个简单的基准测试,并不代表全部场景。
Castle
和 AspectCore
是优秀的库。
Norns.urd
的许多实现都参考了 Castle
和 AspectCore
的源代码。
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.1256 (1909/November2018Update/19H2)
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.101
[Host] : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
DefaultJob : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
方法 | 平均 | Error(错误) | 标准差 | Gen 0 | Gen 1 | Gen 2 | 已分配 |
TransientInstanceCallSyncMethodWhenNoAop | 66.89 纳秒 | 0.534 纳秒 | 0.473 纳秒 | 0.0178 | - | - | 112 字节 |
TransientInstanceCallSyncMethodWhenNornsUrd | 142.65 纳秒 | 0.373 纳秒 | 0.331 纳秒 | 0.0534 | - | - | 336 字节 |
TransientInstanceCallSyncMethodWhenCastle | 214.54 纳秒 | 2.738 纳秒 | 2.286 纳秒 | 0.0815 | - | - | 512 字节 |
TransientInstanceCallSyncMethodWhenAspectCore | 518.27 纳秒 | 3.595 纳秒 | 3.363 纳秒 | 0.1030 | - | - | 648 字节 |
TransientInstanceCallAsyncMethodWhenNoAop | 111.56 纳秒 | 0.705 纳秒 | 0.659 纳秒 | 0.0408 | - | - | 256 字节 |
TransientInstanceCallAsyncMethodWhenNornsUrd | 222.59 纳秒 | 1.128 纳秒 | 1.055 纳秒 | 0.0763 | - | - | 480 字节 |
TransientInstanceCallAsyncMethodWhenCastle | 245.23 纳秒 | 1.295 纳秒 | 1.211 纳秒 | 0.1044 | - | - | 656 字节 |
TransientInstanceCallAsyncMethodWhenAspectCore | 587.14 纳秒 | 2.245 纳秒 | 2.100 纳秒 | 0.1373 | - | - | 864 字节 |
快速入门
这是一个简单的全局拦截器示例。您可以在 Examples.WebApi 找到完整的示例代码。
-
创建 ConsoleInterceptor.cs
using Norns.Urd; using Norns.Urd.Reflection; using System; using System.Threading.Tasks; namespace Examples.WebApi { public class ConsoleInterceptor : AbstractInterceptor { public override async Task InvokeAsync (AspectContext context, AsyncAspectDelegate next) { Console.WriteLine($"{context.Service.GetType(). GetReflector().FullDisplayName}. {context.Method.GetReflector().DisplayName}"); await next(context); } } }
-
将
WeatherForecastController
的方法设置为virtual
[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { [HttpGet] public virtual IEnumerable<WeatherForecast> Get() => test.Get(); }
-
AddControllersAsServices
:// This method gets called by the runtime. // Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddControllersAsServices(); }
-
将
GlobalInterceptor
添加到i
// This method gets called by the runtime. // Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddControllersAsServices(); services.ConfigureAop(i => i.GlobalInterceptors.Add(new ConsoleInterceptor())); }
-
运行。
您将在控制台中看到此输出
Norns.Urd.DynamicProxy.Generated.WeatherForecastController_Proxy_Inherit. IEnumerable<WeatherForecast> Get()
基本原理
本文概述了开发 Norns.Urd.Interceptors
所需的关键主题。
拦截器
在 Norns.urd
中,Interceptor
是用户可以插入到方法中的逻辑的核心。
拦截器结构定义
Interceptor
定义了标准的 IInterceptor
结构。
public interface IInterceptor
{
// Users can customize the interceptor Order with Order,
// sorted by ASC, in which both the global interceptor
// and the display interceptor are included
int Order { get; }
// Synchronous interception method
void Invoke(AspectContext context, AspectDelegate next);
// Asynchronous interception method
Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);
// You can set how the interceptor chooses whether to filter or not to intercept a method,
// in addition to the NonAspectAttribute and global NonPredicates
// that can influence filtering
bool CanAspect(MethodInfo method);
}
拦截器连接类型
从实际设计来看,拦截器只有 IInterceptor
这一个统一的定义,但由于 C# 的单继承和 Attribute 语言限制,因此存在 AbstractInterceptorAttribute
和 AbstractInterceptor
这两个类。
AbstractInterceptorAttribute(显示拦截器)
public abstract class AbstractInterceptorAttribute : Attribute, IInterceptor
{
public virtual int Order { get; set; }
public virtual bool CanAspect(MethodInfo method) => true;
// If the user wants to reduce the performance penalty of
// converting an asynchronous method to a synchronous call in a
// synchronous interceptor method by default,
// he can choose to overload the implementation.
public virtual void Invoke(AspectContext context, AspectDelegate next)
{
InvokeAsync(context, c =>
{
next(c);
return Task.CompletedTask;
}).ConfigureAwait(false)
.GetAwaiter()
.GetResult();
}
// The default is to implement only the asynchronous interceptor method
public abstract Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);
}
一个 interceptor
实现的示例:
public class AddTenInterceptorAttribute : AbstractInterceptorAttribute
{
public override void Invoke(AspectContext context, AspectDelegate next)
{
next(context);
AddTen(context);
}
private static void AddTen(AspectContext context)
{
if (context.ReturnValue is int i)
{
context.ReturnValue = i + 10;
}
else if(context.ReturnValue is double d)
{
context.ReturnValue = d + 10.0;
}
}
public override async Task InvokeAsync(AspectContext context, AsyncAspectDelegate next)
{
await next(context);
AddTen(context);
}
}
InterceptorAttribute 拦截器用法
- 接口/类/方法:您可以像这样设置
Attribute
[AddTenInterceptor] public interface IGenericTest<T, R> : IDisposable { // or //[AddTenInterceptor] T GetT(); }
- 也可以在全局拦截器中设置
public void ConfigureServices(IServiceCollection services) { services.ConfigureAop (i => i.GlobalInterceptors.Add(new AddTenInterceptorAttribute())); }
AbstractInterceptor
与 AbstractInterceptorAttribute
几乎相同,但不是 `Attribute
`,不能用于相应的场景,只能用于拦截器本身的使用。它为不想简化“Attribute
”场景的用户提供了一个创建 Interceptor
的途径。
InterceptorInterceptor 用法
只能在全局拦截器中设置
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureAop(i => i.GlobalInterceptors.Add(new AddSixInterceptor()));
}
全局拦截器与显示拦截器
- 全局拦截器是拦截所有代理方法的拦截器。只需声明一次,全局有效。
public void ConfigureServices(IServiceCollection services) { services.ConfigureAop(i => i.GlobalInterceptors.Add(new AddSixInterceptor())); }
- 显示拦截器必须在所有需要显式声明的地方使用
AbstractInterceptorAttribute
。[AddTenInterceptor] public interface IGenericTest<T, R> : IDisposable { // or //[AddTenInterceptor] T GetT(); }
所以用户选择方便的即可。
拦截器过滤模式
Norns.Urd
提供了以下三种过滤方法:
- 全局过滤
services.ConfigureAop(i => i.NonPredicates.AddNamespace("Norns") .AddNamespace("Norns.*") .AddNamespace("System") .AddNamespace("System.*") .AddNamespace("Microsoft.*") .AddNamespace("Microsoft.Owin.*") .AddMethod("Microsoft.*", "*"));
- 根据过滤器
[NonAspect] public interface IGenericTest<T, R> : IDisposable { }
- 拦截器本身过滤
public class ParameterInjectInterceptor : AbstractInterceptor { public override bool CanAspect(MethodInfo method) { return method.GetReflector().Parameters.Any(i => i.IsDefined<InjectAttribute>()); } }
AOP 限制
- 当服务类型是类时,只有虚方法和子类的方法才能被代理拦截。
- 当某个类型的某个方法的参数是
readonly struct
时,无法代理。
接口和抽象类的默认实现
如果使用 DI 框架注册了“接口”和“抽象类”但没有实际实现,Norns.urd
会实现默认的子类型。
为什么此功能可用?
这是为了支持声明式编码的思想,提供一些低级的实现支持,以便更多的学生可以定制自己的声明式库并简化代码,例如实现一个声明式的 HttpClient
。
默认实现限制
- 不支持属性注入。
Norns.urd
生成的默认实现是返回类型的默认值。
演示
我们将以一个简单的 httpClient
作为示例来完成,这里有一个简短的演示。
- 如果第10步是我们的 HTTP 调用逻辑,那么我们可以将所有“加10”的逻辑放在拦截器中。
public class AddTenAttribute : AbstractInterceptorAttribute { public override void Invoke(AspectContext context, AspectDelegate next) { next(context); AddTen(context); } private static void AddTen(AspectContext context) { if (context.ReturnValue is int i) { context.ReturnValue = i + 10; } else if(context.ReturnValue is double d) { context.ReturnValue = d + 10.0; } } public override async Task InvokeAsync (AspectContext context, AsyncAspectDelegate next) { await next(context); AddTen(context); } }
- 定义声明式客户端
[AddTen] public interface IAddTest { int AddTen(); // The default implementation in the interface is not replaced by norns.urd, // which provides some scenarios where users can customize the implementation logic public int NoAdd() => 3; }
- 注册客户端
services.AddTransient<IAddTest>(); services.ConfigureAop();
- 使用它
[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { IAddTest a; public WeatherForecastController(IAddTest b) { a = b; } [HttpGet] public int GetAddTen() => a.AddTen(); }
InjectAttribute
InjectAttribute
是对接口和抽象类的默认实现的功能补充。
特别是在创建声明式客户端等方面时,并且您提供自定义设置,例如接口的默认接口实现。
用户可能需要从 DI 获取实例,因此有两种方法可以对其进行补充。
ParameterInject
方法参数可以设置为 InjectAttribute
。
- 当参数为
null
时,将尝试从 DI 获取实例。 - 当参数不为
null
时,传递的值不会被覆盖,保留传递的参数值。
示例
public interface IInjectTest
{
public ParameterInjectTest T([Inject] ParameterInjectTest t = null) => t;
}
PropertyInject
public interface IInjectTest
{
[Inject]
ParameterInjectInterceptorTest PT { get; set; }
}
FieldInject
根据行业编码约定,不建议使用未赋值的 FIELD
,因此此功能可能会导致代码审查问题,需要修复。
public class ParameterInjectTest : IInjectTest
{
[Inject]
ParameterInjectInterceptorTest ft;
}
FallbackAttribute
public class DoFallbackTest
{
[Fallback(typeof(TestFallback))] // just need set Interceptor Type
public virtual int Do(int i)
{
throw new FieldAccessException();
}
[Fallback(typeof(TestFallback))]
public virtual Task<int> DoAsync(int i)
{
throw new FieldAccessException();
}
}
public class TestFallback : AbstractInterceptor
{
public override void Invoke(AspectContext context, AspectDelegate next)
{
context.ReturnValue = (int)context.Parameters[0];
}
public override Task InvokeAsync(AspectContext context, AsyncAspectDelegate next)
{
var t = Task.FromResult((int)context.Parameters[0]);
context.ReturnValue = t;
return t;
}
}
Polly
Polly 是 .NET 的弹性与瞬态故障处理库。
在此,通过 Norns.urd
,Polly 的各种功能被集成到更友好的函数中。
使用 Norns.Urd + Polly,只需 EnablePolly() 即可。
示例:
new ServiceCollection()
.AddTransient<DoTimeoutTest>()
.ConfigureAop(i => i.EnablePolly())
TimeoutAttribute
[Timeout(seconds: 1)] // timeout 1 seconds,
// when timeout will throw TimeoutRejectedException
double Wait(double seconds);
[Timeout(timeSpan: "00:00:00.100")] // timeout 100 milliseconds,
// only work on async method when no CancellationToken
async Task<double> WaitAsync(double seconds, CancellationToken cancellationToken = default);
[Timeout(timeSpan: "00:00:01")] // timeout 1 seconds, but no work on async method
// when no CancellationToken
async Task<double> NoCancellationTokenWaitAsync(double seconds);
RetryAttribute
[Retry(retryCount: 2, ExceptionType = typeof(AccessViolationException))] // retry 2 times
// when if throw Exception
void Do()
CircuitBreakerAttribute
[CircuitBreaker(exceptionsAllowedBeforeBreaking: 3, durationOfBreak: "00:00:01")]
//or
[AdvancedCircuitBreaker(failureThreshold: 0.1, samplingDuration: "00:00:01",
minimumThroughput: 3, durationOfBreak: "00:00:01")]
void Do()
BulkheadAttribute
[Bulkhead(maxParallelization: 5, maxQueuingActions: 10)]
void Do()
CacheAttribute
Norns.urd
本身不提供任何实际的缓存实现。
但基于 Microsoft.Extensions.Caching.Memory.IMemoryCache
和 Microsoft.Extensions.Caching.Distributed.IDistributedCache
,实现了 CacheAttribute
这个调用适配器。
缓存策略
Norns.urd
适配三种时间策略模式:
AbsoluteExpiration
绝对过期,意味着在设定的时间点过期。
[Cache(..., AbsoluteExpiration = "1991-05-30 00:00:00")] void Do()
AbsoluteExpirationRelativeToNow
过期发生在当前时间加上设定时间之后,意味着缓存设置生效时间(1991-05-30 00:00:00)+ 缓存生效时间(05:00:00)=(1991-05-30 05:00:00)时过期。
[Cache(..., AbsoluteExpirationRelativeToNow = "00:05:00")] // Live for 5 minutes void Do()
启用内存缓存
IServiceCollection.ConfigureAop(i => i.EnableMemoryCache())
启用分布式缓存
目前默认提供了一个针对 ‘system.text.json’ 的序列化适配器。
IServiceCollection.ConfigureAop
(i => i.EnableDistributedCacheSystemTextJsonAdapter(/*You can specify your own Name*/))
.AddDistributedMemoryCache() // You can switch to any DistributedCache implementation
SlidingExpiration
滑动过期,意味着在缓存有效期内的任何访问都会将窗口有效期向后推移,只有在没有访问且缓存过期时才会失效。
[Cache(..., SlidingExpiration = "00:00:05")] void Do()
使用缓存
单级缓存
[Cache(cacheKey: "T", SlidingExpiration = "00:00:01")] // Does not specify
// a cache name CacheOptions.DefaultCacheName = "memory"
public virtual Task<int> DoAsync(int count);
多级缓存
[Cache(cacheKey: nameof(Do), AbsoluteExpirationRelativeToNow = "00:00:01",
Order = 1)] // It is first fetched from the memory cache and expires after 1 second
[Cache(cacheKey: nameof(Do), cacheName:"json", AbsoluteExpirationRelativeToNow = "00:00:02",
Order = 2)] // When the memory cache is invalidated,
// it will be fetched from the DistributedCache
public virtual int Do(int count);
自定义缓存配置
通常,我们需要动态获取缓存配置,只需继承‘ICacheOptionGenerator
’即可自定义配置。
例如:
public class ContextKeyFromCount : ICacheOptionGenerator
{
public CacheOptions Generate(AspectContext context)
{
return new CacheOptions()
{
CacheName = "json",
CacheKey = context.Parameters[0],
SlidingExpiration = TimeSpan.Parse("00:00:01")
};
}
}
尝试并使用:
[Cache(typeof(ContextKeyFromCount))]
public virtual Task<int> DoAsync(string key, int count);
如何自定义新的分布式缓存序列化适配器
只需继承 ISerializationAdapter
即可。
例如:
public class SystemTextJsonAdapter : ISerializationAdapter
{
public string Name { get; }
public SystemTextJsonAdapter(string name)
{
Name = name;
}
public T Deserialize<T>(byte[] data)
{
return JsonSerializer.Deserialize<T>(data);
}
public byte[] Serialize<T>(T data)
{
return JsonSerializer.SerializeToUtf8Bytes<T>(data);
}
}
注册
public static IAspectConfiguration EnableDistributedCacheSystemTextJsonAdapter
(this IAspectConfiguration configuration, string name = "json")
{
return configuration.EnableDistributedCacheSerializationAdapter
(i => new SystemTextJsonAdapter(name));
}
HttpClient
这里的 HttpClient 是对 `System.Net.Http` 下的 HttpClient 的封装,这样大家就可以通过简单定义接口来实现 http 调用,可以减少一些重复的代码编写。
如何使用 HttpClient
1. 添加包 Norns.Urd.HttpClient
dotnet add package Norns.Urd.HttpClient
2. 启用 HttpClient
new ServiceCollection()
.ConfigureAop(i => i.EnableHttpClient())
3. 定义 HttpClient 接口
示例:
[BaseAddress("https://.:5000")]
public interface ITestClient
{
[Get("WeatherForecast/file")]
[AcceptOctetStream]
Task<Stream> DownloadAsync();
[Post("WeatherForecast/file")]
[OctetStreamContentType]
Task UpoladAsync([Body]Stream f);
}
4. 添加到 ServiceCollection
new ServiceCollection()
.AddSingleton<ITestClient>() // Just set the life cycle according to your own needs, and you don’t need to write specific implementations, Norns.Urd.HttpClient will generate the corresponding IL code for you
.ConfigureAop(i => i.EnableHttpClient())
5. 通过 DI 使用即可,例如
[ApiController]
[Route("[controller]")]
public class ClientController : ControllerBase
{
private readonly ITestClient client;
public ClientController(ITestClient client)
{
this.client = client;
}
[HttpGet("download")]
public async Task<object> DownloadAsync()
{
using var r = new StreamReader(await client.DownloadAsync());
return await r.ReadToEndAsync();
}
}
HttpClient 的功能
如何设置 URL
BaseAddress
如果某个网站域名或基础 API 地址被多个接口使用,您可以在接口上使用 `BaseAddressAttribute`。
示例:
[BaseAddress("https://.:5000")]
public interface ITestClient
使用 HTTP 方法设置 URL
支持的 HTTP 方法:
- GetAttribute
- PostAttribute
- PutAttribute
- DeleteAttribute
- PatchAttribute
- OptionsAttribute
- HeadAttribute
(当上述方法不够用时,可以继承自定义实现 `HttpMethodAttribute`)
所有这些 HTTP 方法都支持 URL 配置,并且有两种方式支持:
静态配置
[Post("https://.:5000/money/getData/")]
public Data GetData()
动态配置
默认情况下,它支持通过键从 `IConfiguration` 获取 URL 配置。
[Post("configKey", IsDynamicPath = true)]
public Data GetData()
如果这种简单的配置形式不能满足您的需求,您可以实现 `IHttpRequestDynamicPathFactory` 接口来替换配置实现,并且已实现的类只需要在 IOC 容器中注册。
实现示例可以参考 `ConfigurationDynamicPathFactory`。
路由参数设置
如果某些 URL 路由参数需要动态设置,可以通过 `RouteAttribute` 设置,例如:
[Post("getData/{id}")]
public Data GetData([Route]string id)
如果参数名与 URL 中的设置不匹配,可以通过 `Alias =` 设置,例如:
[Post("getData/{id}")]
public Data GetData([Route(Alias = "id")]string number)
如何设置查询字符串
查询字符串参数可以在方法参数列表中设置。
[Post("getData")]
public Data GetData([Query]string id);
//or
[Post("getData")]
public Data GetData([Query(Alias = "id")]string number);
URL 的结果都是 `getData?id=xxx`。
参数类型支持基本类型和类。
当它是类时,将类的属性作为参数。
因此,当属性名与定义不匹配时,您可以在属性上使用 `[Query(Alias = "xxx")] ` 进行指定。
如何设置请求体
请求体可以通过在方法参数列表中设置 `BodyAttribute` 来指定参数。
注意,只有第一个带有 `BodyAttribute` 的参数才会生效,例如:
public void SetData([Body]Data data);
序列化器将根据设置的 Request Content-Type 来选择以序列化请求体。
如何设置响应体
要指定响应体类型,只需在方法的返回类型中写上所需类型。以下是支持的类型:
- void(忽略反序列化)
- Task(忽略反序列化)
- ValueTask(忽略反序列化)
- T
- Task<T>
- ValueTask<T>
- HttpResponseMessage
- Stream(仅在 Content-Type 为 application/octet-stream 时有效)
示例:
public Data GetData();
如何设置 Content-Type
请求或响应的 Content-Type 都会影响序列化和反序列化的选择。
默认支持 json/xml 的序列化和反序列化,可以这样设置:
- JsonContentTypeAttribute
- XmlContentTypeAttribute
- OctetStreamContentTypeAttribute
示例:
[OctetStreamContentType]
public Data GetData([Body]Stream s);
对应的 Accept 设置为:
- AcceptJsonAttribute
- AcceptXmlAttribute
- AcceptOctetStreamAttribute
示例:
[AcceptOctetStream]
public Stream GetData();
json 序列化器默认为 `System.Text.Json`。
将 json 序列化器更改为 NewtonsoftJson
- 1. 添加包 `Norns.Urd.HttpClient.NewtonsoftJson`
- 2. 在 ioc 中注册,例如:
new ServiceCollection().AddHttpClientNewtonsoftJosn()
自定义序列化器
当现有序列化器不足以满足需求时,
只需实现 `IHttpContentSerializer` 并注册到 ioc 容器中。
自定义 Header
除了上面提到的 Header,还可以添加其他 Header。
还有以下两种方式:
在接口或方法的静态配置中使用 `HeaderAttribute`。
[Header("x-data", "money")]
public interface ITestClient {}
//or
[Header("x-data", "money")]
public Data GetData();
方法参数的动态配置。
public Data GetData([SetRequestHeader("x-data")]string header);
自定义 HttpRequestMessageSettingsAttribute
当现有的 `HttpRequestMessageSettingsAttribute` 不足以满足需求时,
只需继承 `HttpRequestMessageSettingsAttribute` 来实现自己的功能,
在相应的接口/方法中使用即可。
通过参数设置获取响应 Header
当有时我们需要获取响应返回的 Header 时,
我们可以通过 out 参数 + `OutResponseHeaderAttribute` 来获取响应 Header 的值。
(注意:只有同步方法,out 参数才能工作)
示例:
public Data GetData([OutResponseHeader("x-data")] out string header);
如何设置 HttpClient
MaxResponseContentBufferSize
[MaxResponseContentBufferSize(20480)]
public interface ITestClient {}
//or
[MaxResponseContentBufferSize(20480)]
public Data GetData()
Timeout
[Timeout("00:03:00")]
public interface ITestClient {}
//or
[Timeout("00:03:00")]
public Data GetData()
ClientName
当您需要结合 HttpClientFactory 获取特殊设置的 HttpClient 时,可以通过 `ClientNameAttribute` 指定。
示例
[ClientName("MyClient")]
public interface ITestClient {}
//or
[ClientName("MyClient")]
public Data GetData()
您可以这样获取指定的 HttpClient:
services.AddHttpClient("MyClient", i => i.MaxResponseContentBufferSize = 204800);
HttpCompletionOption
调用 HttpClient 时的 CompletionOption 参数也可以设置。
HttpCompletionOption.ResponseHeadersRead 是默认配置。
示例
[HttpCompletionOption(HttpCompletionOption.ResponseContentRead)]
public interface ITestClient {}
//or
[HttpCompletionOption(HttpCompletionOption.ResponseContentRead)]
public Data GetData()
全局 HttpRequestMessage 和 HttpResponseMessage 处理程序
如果您需要在全局范围内处理 HttpRequestMessage 和 HttpResponseMessage,例如:
- 链路追踪 ID 设置
- 自定义处理响应异常
可以通过实现 `IHttpClientHandler` 并注册到 ioc 容器中来使用。
例如,默认的状态码检查,例如:
public class EnsureSuccessStatusCodeHandler : IHttpClientHandler
{
public int Order => 0;
public Task SetRequestAsync(HttpRequestMessage message, AspectContext context, CancellationToken token)
{
return Task.CompletedTask;
}
public Task SetResponseAsync(HttpResponseMessage resp, AspectContext context, CancellationToken token)
{
resp.EnsureSuccessStatusCode();
return Task.CompletedTask;
}
}
当然,如果不需要 StatusCode 检查处理,可以直接在 ioc 容器中清除,例如:
services.RemoveAll<IHttpClientHandler>();
// Then add your own processing
services.AddSingleton<IHttpClientHandler, xxx>();
Norns.Urd 的一些设计
Norns.Urd 的实现前提
-
支持同步/异步方法,用户可以选择同步或异步。
-
这样做的优点是工作量加倍。同步和异步完全分为两个实现。
-
提供给用户的
Interceptor
接口需要提供一个方案,将同步和异步结合在一组实现代码中。毕竟,不能强制用户实现两组代码。许多场景的用户不需要为同步和异步之间的差异实现两组代码。
-
-
没有内置 DI 实现,但与 DI 兼容性良好。
-
如果内置 DI 容器可以使对通用场景的支持非常简单,毕竟,从 DI 容器中实例化对象必须有明确的类型,但是,现在有这么多实现库,我不想为某些场景实现很多功能(是我真的懒,还是库写不长)。
-
但是 DI 容器的解耦做得很好,我经常从中受益并减少了大量的代码更改,所以 AOP 库必须考虑基于 DI 容器的支持,在这种情况下,DI 支持打开了通用/自定义实例化方法以提供支持,DI 和 AOP 内部都必须提供用户调用方法,否则它就无法工作(所以算下来,我真的懒吗?我是在给自己挖坑吗?)
-
如何解决这些问题?
目前的解决方案不一定完美,但暂时解决了问题(如果有更好的解决方案,请告诉我,我迫切需要学习)。
提供了哪些拦截器编写模式给用户?
我过去遇到过一些其他的 AOP 实现框架,其中许多都需要将拦截代码分为方法前/方法后/异常处理等。我个人认为这种形式在一定程度上影响了拦截器实现的思维方式,我总觉得不够流畅。
但是 ASP.NET Core 中间件 感觉很好,如下图和代码所示:
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
拦截器也应该能够做到这一点,所以拦截器代码应该看起来像这样:
public class ConsoleInterceptor
{
public async Task InvokeAsync(Context context, Delegate next)
{
Console.WriteLine("Hello, World!");
await next(context);
}
}
同步和异步方法如何分开?它们如何组合?用户如何选择实现同步或异步或两者都实现?
public delegate Task AsyncAspectDelegate(AspectContext context);
public delegate void AspectDelegate(AspectContext context);
// resolution:
// Create two sets of call chains that make a complete differentiating
// between Sync and Async by AspectDelegate and AsyncAspectDelegate,
// depending on the intercepted method itself
public abstract class AbstractInterceptor : IInterceptor
{
public virtual void Invoke(AspectContext context, AspectDelegate next)
{
InvokeAsync(context, c =>
{
next(c);
return Task.CompletedTask;
}).ConfigureAwait(false)
.GetAwaiter()
.GetResult();
}
// merge:
// Implements transformation method content by default so that various interceptors
// can be mixed into a call chain for Middleware
public abstract Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);
// User autonomous selection:
// Providing both the Sync and Async interceptor methods can be overloaded
// and the user can choose
// So the user can call special non-asynchronous optimization code in Async,
// and needless to say await in Sync will affect performance.
// If you think it affects performance, you can reload yourself if you care.
// If you don't care, you can choose
}
没有 DI 以及如何支持其他 DI?
DI 框架有注册类型,我们可以使用 emit 来生成代理类,替换原始注册,从而实现兼容。
当然,每个 DI 框架都需要一些自定义实现代码来支持(唉,又增加了工作量)。
如何支持 AddTransient<IMTest>(x => new NMTest())?
由于此 DI 框架的使用方式,无法通过 Func
函数获取要使用的实际类型。相反,它只能根据 IMTest
的定义通过 emit 生成桥接代理类型。伪代码如下:
interface IMTest
{
int Get(int i);
}
class IMTestProxy : IMTest
{
IMTest instance = (x => new NMTest())();
int Get(int i) => instance.Get(i);
}
如何支持 .AddTransient(typeof(IGenericTest<,>), typeof(GenericTest<,>))?
唯一的困难是,生成诸如 Get<T>()
这样的方法调用并不容易,因为 IL 需要反射找到特定的方法,例如 Get<int>()
、Get<bool>()
等,它不能模糊地写成 Get<T>()
。
解决此问题的唯一方法是推迟实际调用,直到运行时调用被重新生成为特定的调用。伪代码大致如下:
interface GenericTest<T,R>
{
T Get<T>(T i) => i;
}
class GenericTestProxy<T,R> : GenericTest<T,R>
{
T Get<T>(T i) => this.GetType().GetMethod("Get<T>").Invoke(i);
}
历史
- 2020年12月14日:初始版本