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

Norns.Urd 轻量级 AOP 框架

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.50/5 (6投票s)

2020年12月14日

MIT

12分钟阅读

viewsIcon

8839

Norns.Urd 是一个基于 emit 实现的轻量级 AOP 框架,它支持动态代理。

欢迎来到 Norns.Urd

build GitHub

Github: https://github.com/fs7744/Norns.Urd

Norns.urd 是一个基于 emit 实现的轻量级 AOP 框架,它支持动态代理。

它基于 netstandard2.0。

完成这个框架的主要目的是出于以下个人愿望:

  • 一次性实现静态 AOP 和动态 AOP
  • 如何让一个 AOP 框架不仅仅只做动态代理,还能与 Microsoft.Extensions.DependencyInjection 等其他 DI 框架协同工作?
  • 如何让 AOP 同时兼容同步和异步方法,并将实现选项完全留给用户?

希望这个库能对您有所帮助。

顺便说一句,如果您不熟悉 AOP,可以看看这篇文章:面向切面编程

简单的基准测试

这只是一个简单的基准测试,并不代表全部场景。

CastleAspectCore 是优秀的库。

Norns.urd 的许多实现都参考了 CastleAspectCore 的源代码。

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 找到完整的示例代码。

  1. 创建 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);
             }
         }
     }
  2. WeatherForecastController 的方法设置为 virtual

     [ApiController]
     [Route("[controller]")]
     public class WeatherForecastController : ControllerBase
     {
         [HttpGet]
         public virtual IEnumerable<WeatherForecast> Get() => test.Get();
     }
  3. 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();
     }
  4. 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()));
     }
  5. 运行。

    您将在控制台中看到此输出

     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 语言限制,因此存在 AbstractInterceptorAttributeAbstractInterceptor 这两个类。

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 作为示例来完成,这里有一个简短的演示。

  1. 如果第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);
        }
    }
  2. 定义声明式客户端
    [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;
    }
  3. 注册客户端
    services.AddTransient<IAddTest>();
    services.ConfigureAop();
  4. 使用它
        [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.IMemoryCacheMicrosoft.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. 1. 添加包 `Norns.Urd.HttpClient.NewtonsoftJson`
  2. 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 的实现前提

  1. 支持同步/异步方法,用户可以选择同步或异步。

    • 这样做的优点是工作量加倍。同步和异步完全分为两个实现。

    • 提供给用户的 Interceptor 接口需要提供一个方案,将同步和异步结合在一组实现代码中。毕竟,不能强制用户实现两组代码。许多场景的用户不需要为同步和异步之间的差异实现两组代码。

  2. 没有内置 DI 实现,但与 DI 兼容性良好。

    • 如果内置 DI 容器可以使对通用场景的支持非常简单,毕竟,从 DI 容器中实例化对象必须有明确的类型,但是,现在有这么多实现库,我不想为某些场景实现很多功能(是我真的懒,还是库写不长)。

    • 但是 DI 容器的解耦做得很好,我经常从中受益并减少了大量的代码更改,所以 AOP 库必须考虑基于 DI 容器的支持,在这种情况下,DI 支持打开了通用/自定义实例化方法以提供支持,DI 和 AOP 内部都必须提供用户调用方法,否则它就无法工作(所以算下来,我真的懒吗?我是在给自己挖坑吗?)

如何解决这些问题?

目前的解决方案不一定完美,但暂时解决了问题(如果有更好的解决方案,请告诉我,我迫切需要学习)。

提供了哪些拦截器编写模式给用户?

我过去遇到过一些其他的 AOP 实现框架,其中许多都需要将拦截代码分为方法前/方法后/异常处理等。我个人认为这种形式在一定程度上影响了拦截器实现的思维方式,我总觉得不够流畅。

但是 ASP.NET Core 中间件 感觉很好,如下图和代码所示:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/index/_static/request-delegate-pipeline.png?view=aspnetcore-5.0

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日:初始版本
© . All rights reserved.