Norns.urd is a lightweight AOP framework based on emit which does dynamic proxy. Fundamentals has Global interceptors vs. attribute interceptors Interceptor filter mode The default implementation of Interface and Abstract Class InjectAttribute FallbackAttribute TimeoutAttribute RetryAttribute CircuitBreakerAttribute BulkheadAttribute
Welcome to Norns.Urd
Github: https://github.com/fs7744/Norns.Urd
Norns.urd
is a lightweight AOP framework based on emit which does dynamic proxy.
It is based on netstandard2.0.
The purpose of completing this framework mainly comes from the following personal wishes:
- Static AOP and dynamic AOP are implemented once
- How can an AOP framework only do dynamic proxy but work with other DI frameworks like
Microsoft.Extensions.DependencyInjection
- How can an AOP make both sync and async methods compatible and leave implementation options entirely to the user?
Hopefully, this library will be of some use to you.
By the way, if you’re not familiar with AOP, check out the article, Aspect-oriented programming.
Simple Benchmark
This is just a simple benchmark test, and does not represent the whole scenario.
Castle
and AspectCore
are excellent libraries.
Many implementations of Norns.urd
refer to the source code of Castle
and 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
Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
TransientInstanceCallSyncMethodWhenNoAop | 66.89 ns | 0.534 ns | 0.473 ns | 0.0178 | - | - | 112 B |
TransientInstanceCallSyncMethodWhenNornsUrd | 142.65 ns | 0.373 ns | 0.331 ns | 0.0534 | - | - | 336 B |
TransientInstanceCallSyncMethodWhenCastle | 214.54 ns | 2.738 ns | 2.286 ns | 0.0815 | - | - | 512 B |
TransientInstanceCallSyncMethodWhenAspectCore | 518.27 ns | 3.595 ns | 3.363 ns | 0.1030 | - | - | 648 B |
TransientInstanceCallAsyncMethodWhenNoAop | 111.56 ns | 0.705 ns | 0.659 ns | 0.0408 | - | - | 256 B |
TransientInstanceCallAsyncMethodWhenNornsUrd | 222.59 ns | 1.128 ns | 1.055 ns | 0.0763 | - | - | 480 B |
TransientInstanceCallAsyncMethodWhenCastle | 245.23 ns | 1.295 ns | 1.211 ns | 0.1044 | - | - | 656 B |
TransientInstanceCallAsyncMethodWhenAspectCore | 587.14 ns | 2.245 ns | 2.100 ns | 0.1373 | - | - | 864 B |
Quick Start
This is simple demo to do global interceptor. You can see the full code for the demo at Examples.WebApi.
-
Create 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);
}
}
}
-
Set WeatherForecastController
’s method as virtual
:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public virtual IEnumerable<WeatherForecast> Get() => test.Get();
}
-
AddControllersAsServices
:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddControllersAsServices();
}
-
Add GlobalInterceptor
to i
:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddControllersAsServices();
services.ConfigureAop(i => i.GlobalInterceptors.Add(new ConsoleInterceptor()));
}
-
Run.
You will see this in console:
Norns.Urd.DynamicProxy.Generated.WeatherForecastController_Proxy_Inherit.
IEnumerable<WeatherForecast> Get()
Fundamentals
This article provides an overview of key topics for understanding how to develop Norns.Urd.Interceptors
.
Interceptor
In Norns.urd
, Interceptor
is the core of the logic that a user can insert into a method.
Interceptor Structure Definition
The interceptor
defines the standard structure as IInterceptor
:
public interface IInterceptor
{
int Order { get; }
void Invoke(AspectContext context, AspectDelegate next);
Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);
bool CanAspect(MethodInfo method);
}
Interceptor Junction Type
Interceptors from actual design only IInterceptor
that a unified definition, but due to the single inheritance and csharp Attribute language limitation, so have a AbstractInterceptorAttribute
and AbstractInterceptor
two classes.
AbstractInterceptorAttribute (Display interceptor)
public abstract class AbstractInterceptorAttribute : Attribute, IInterceptor
{
public virtual int Order { get; set; }
public virtual bool CanAspect(MethodInfo method) => true;
public virtual void Invoke(AspectContext context, AspectDelegate next)
{
InvokeAsync(context, c =>
{
next(c);
return Task.CompletedTask;
}).ConfigureAwait(false)
.GetAwaiter()
.GetResult();
}
public abstract Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);
}
An example of an interceptor
implementation:
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 Interceptor Usage
- Interface / class / method: You can set the
Attribute
like:
[AddTenInterceptor]
public interface IGenericTest<T, R> : IDisposable
{
T GetT();
}
- It can also be set in the global interceptor:
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureAop
(i => i.GlobalInterceptors.Add(new AddTenInterceptorAttribute()));
}
AbstractInterceptor
And AbstractInterceptorAttribute
, almost identical, but not a `Attribute
`, cannot be used for corresponding scene, only in the use of the interceptor. In itself, it is provided for a user to create an Interceptor
that does not want to simplify the ‘Attribute
’ scenario.
InterceptorInterceptor Usage
Can only be set in a global interceptor:
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureAop(i => i.GlobalInterceptors.Add(new AddSixInterceptor()));
}
Global Interceptors vs. Display Interceptors
- A global interceptor is a method that intercepts all proxying methods. It only needs to be declared once and is valid globally:
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureAop(i => i.GlobalInterceptors.Add(new AddSixInterceptor()));
}
- Display interceptor must use
AbstractInterceptorAttribute
in all places need to display statement:
[AddTenInterceptor]
public interface IGenericTest<T, R> : IDisposable
{
T GetT();
}
So just use what the user thinks is convenient.
Interceptor Filter Mode
Norns.Urd
provides the following three filtering methods:
- Global filtering:
services.ConfigureAop(i => i.NonPredicates.AddNamespace("Norns")
.AddNamespace("Norns.*")
.AddNamespace("System")
.AddNamespace("System.*")
.AddNamespace("Microsoft.*")
.AddNamespace("Microsoft.Owin.*")
.AddMethod("Microsoft.*", "*"));
- According to filter:
[NonAspect]
public interface IGenericTest<T, R> : IDisposable
{
}
- The interceptor itself filters:
public class ParameterInjectInterceptor : AbstractInterceptor
{
public override bool CanAspect(MethodInfo method)
{
return method.GetReflector().Parameters.Any(i => i.IsDefined<InjectAttribute>());
}
}
Aop Limit
- When service type is class, only virtual and subclasses have access to methods that can be proxy intercepted
- When which type’s method has parameter is in
readonly struct
can’t proxy
The Default Implementation of Interface and Abstract Class
Norns.urd
implements the default subtype if you register with the DI framework no actual implementation of ‘Interface’ and ‘Abstract Class’.
Why is this feature available?
This is to provide some low-level implementation support for the idea of declarative coding, so that more students can customize some of their own declarative libraries and simplify the code, such as implementing a declarative HttpClient
.
Default Implementation Limit
- Property injection is not supported
- The default implementation generated by
Norns.urd
is the default value of the return type.
Demo
We will complete a simple httpClient
as an example. Here is a brief demo:
- If adding 10 was our logic like an HTTP call, we could put all the add 10 logic in the interceptor:
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);
}
}
- Define declarate client:
[AddTen]
public interface IAddTest
{
int AddTen();
public int NoAdd() => 3;
}
- Registered client:
services.AddTransient<IAddTest>();
services.ConfigureAop();
- Use it:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
IAddTest a;
public WeatherForecastController(IAddTest b)
{
a = b;
}
[HttpGet]
public int GetAddTen() => a.AddTen();
}
InjectAttribute
InjectAttribute
Is a functional complement to the default implementation of Interface and Abstract Class.
Especially when you’re doing declarative clients and things like that, and you’re providing custom Settings, like the interface default interface implementation.
The user may need to get an instance from DI, so there are two ways to supplement it.
ParameterInject
Method parameters can be set as InjectAttribute
:
- When the parameter is
null
, an attempt is made to get the instance from DI. - When the parameter is not
null
, the pass value will not be overridden and the pass parameter value will remain.
Example:
public interface IInjectTest
{
public ParameterInjectTest T([Inject] ParameterInjectTest t = null) => t;
}
PropertyInject
public interface IInjectTest
{
[Inject]
ParameterInjectInterceptorTest PT { get; set; }
}
FieldInject
According to industry coding conventions, FIELD
is not recommended to use without assignment, so this feature can lead to code review problems that need to be fixed.
public class ParameterInjectTest : IInjectTest
{
[Inject]
ParameterInjectInterceptorTest ft;
}
FallbackAttribute
public class DoFallbackTest
{
[Fallback(typeof(TestFallback))]
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 is .NET resilience and transient-fault-handling library.
Here, through Norns.urd
, Polly’s various functions are integrated into more user-friendly functions.
Use Norns.Urd + Polly, only need EnablePolly()
Example:
new ServiceCollection()
.AddTransient<DoTimeoutTest>()
.ConfigureAop(i => i.EnablePolly())
TimeoutAttribute
[Timeout(seconds: 1)]
double Wait(double seconds);
[Timeout(timeSpan: "00:00:00.100")]
async Task<double> WaitAsync(double seconds, CancellationToken cancellationToken = default);
[Timeout(timeSpan: "00:00:01")]
async Task<double> NoCancellationTokenWaitAsync(double seconds);
RetryAttribute
[Retry(retryCount: 2, ExceptionType = typeof(AccessViolationException))]
void Do()
CircuitBreakerAttribute
[CircuitBreaker(exceptionsAllowedBeforeBreaking: 3, durationOfBreak: "00:00:01")]
[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
itself does not provide any cache implementation for actual processing.
But based on Microsoft. Extensions. Caching. Memory. IMemoryCache
and Microsoft Extensions. Caching. Distributed. IDistributedCache
implements CacheAttribute
this call adapter.
Caching Strategies
Norns.urd
adapter three time strategy patterns:
AbsoluteExpiration
Absolute expiration, which means it expires at the set time:
[Cache(..., AbsoluteExpiration = "1991-05-30 00:00:00")]
void Do()
AbsoluteExpirationRelativeToNow
Expiration occurs when the current time is set more than once, meaning it expires when the cache is set to effective time (1991-05-30 00:00:00) + cache effective time (05:00:00) = (1991-05-30 05:00:00).
[Cache(..., AbsoluteExpirationRelativeToNow = "00:05:00")]
void Do()
Enable Memory Caching
IServiceCollection.ConfigureAop(i => i.EnableMemoryCache())
Enable DistributedCache
A serialization adapter for ‘system.text.json’ is currently provided by default:
IServiceCollection.ConfigureAop
(i => i.EnableDistributedCacheSystemTextJsonAdapter())
.AddDistributedMemoryCache()
SlidingExpiration
Sliding window expires, meaning that any access within the cache validity will push the window validity back, and the cache will be invalidated only if there is no access and the cache expires:
[Cache(..., SlidingExpiration = "00:00:05")]
void Do()
Use the Cache
A Single Cache
[Cache(cacheKey: "T", SlidingExpiration = "00:00:01")]
public virtual Task<int> DoAsync(int count);
Multistage Cache
[Cache(cacheKey: nameof(Do), AbsoluteExpirationRelativeToNow = "00:00:01",
Order = 1)]
[Cache(cacheKey: nameof(Do), cacheName:"json", AbsoluteExpirationRelativeToNow = "00:00:02",
Order = 2)]
public virtual int Do(int count);
Customize the Cache Configuration
Often, we need to get the cache configuration dynamically, and we can customize the configuration simply by inheriting ‘ICacheOptionGenerator
’.
For example:
public class ContextKeyFromCount : ICacheOptionGenerator
{
public CacheOptions Generate(AspectContext context)
{
return new CacheOptions()
{
CacheName = "json",
CacheKey = context.Parameters[0],
SlidingExpiration = TimeSpan.Parse("00:00:01")
};
}
}
Try and use:
[Cache(typeof(ContextKeyFromCount))]
public virtual Task<int> DoAsync(string key, int count);
How to Customize the New DistributedCache Serialization Adapter
Just simply inherit ISerializationAdapter
.
For example:
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);
}
}
Registered:
public static IAspectConfiguration EnableDistributedCacheSystemTextJsonAdapter
(this IAspectConfiguration configuration, string name = "json")
{
return configuration.EnableDistributedCacheSerializationAdapter
(i => new SystemTextJsonAdapter(name));
}
HttpClient
The HttpClient here is a encapsulation of the HttpClient under `System.Net.Http`, so that everyone can implement http calls only by simply defining the interface, which can reduce some repetitive code writing.
How to use HttpClient
1. add package Norns.Urd.HttpClient
dotnet add package Norns.Urd.HttpClient
2. enable HttpClient
new ServiceCollection()
.ConfigureAop(i => i.EnableHttpClient())
3. Define the HttpClient interface
Example:
[BaseAddress("http://localhost.:5000")]
public interface ITestClient
{
[Get("WeatherForecast/file")]
[AcceptOctetStream]
Task<Stream> DownloadAsync();
[Post("WeatherForecast/file")]
[OctetStreamContentType]
Task UpoladAsync([Body]Stream f);
}
4. add to ServiceCollection
new ServiceCollection()
.AddSingleton<ITestClient>()
.ConfigureAop(i => i.EnableHttpClient())
5. Just use it through DI, for example
[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's functions
How to set Url
BaseAddress
If some website domain names or basic api addresses are used by many interfaces, you can use `BaseAddressAttribute` on the interface
example:
[BaseAddress("http://localhost.:5000")]
public interface ITestClient
Use Http Method to set Url
Support Http Method:
- GetAttribute
- PostAttribute
- PutAttribute
- DeleteAttribute
- PatchAttribute
- OptionsAttribute
- HeadAttribute
(When the above method is not enough, you can inherit the custom implementation of `HttpMethodAttribute`)
All these Http Methods support Url configuration, and there are two ways to support:
Static configuration
[Post("http://localhost.:5000/money/getData/")]
public Data GetData()
Dynamic configuration
By default, it supports getting url configuration from `IConfiguration` through key
[Post("configKey", IsDynamicPath = true)]
public Data GetData()
If such a simple configuration form does not support your needs, you can implement the `IHttpRequestDynamicPathFactory` interface to replace the configuration implementation, and the implemented class only needs to be registered in the IOC container.
Implementation examples can refer to `ConfigurationDynamicPathFactory`
Routing parameter settings
If some url routing parameters need to be dynamically set, you can set it through `RouteAttribute`, such as
[Post("getData/{id}")]
public Data GetData([Route]string id)
If the parameter name does not match the setting in the url, it can be set by `Alias =`, such as
[Post("getData/{id}")]
public Data GetData([Route(Alias = "id")]string number)
How to set Query string
Query string parameters can be set in the method parameter list
[Post("getData")]
public Data GetData([Query]string id);
[Post("getData")]
public Data GetData([Query(Alias = "id")]string number);
The Url results are all `getData?id=xxx`,
The parameter type supports basic types and classes,
When it is class, the attributes of class will be taken as parameters,
So when the attribute name does not match the definition, you can use `[Query(Alias = "xxx")] on the attribute to specify
How to set Request body
Request body can specify parameters by setting `BodyAttribute` in the method parameter list,
Note that only the first parameter with `BodyAttribute` will take effect, for example
public void SetData([Body]Data data);
The serializer will be selected according to the set Request Content-Type to serialize the body
How to set Response body
To specify the response body type, you only need to write the required type in the return type of the method. The following are supported
- void (Ignore deserialization)
- Task (Ignore deserialization)
- ValueTask (Ignore deserialization)
- T
- Task<T>
- ValueTask<T>
- HttpResponseMessage
- Stream (Only effective when Content-Type is application/octet-stream)
example:
public Data GetData();
How to set Content-Type
Whether Request or Response Content-Type will affect the choice of serialization and deserialization,
The serialization and deserialization of json/xml are supported by default, which can be set as follows
- JsonContentTypeAttribute
- XmlContentTypeAttribute
- OctetStreamContentTypeAttribute
example:
[OctetStreamContentType]
public Data GetData([Body]Stream s);
The corresponding Accept is set to
- AcceptJsonAttribute
- AcceptXmlAttribute
- AcceptOctetStreamAttribute
example:
[AcceptOctetStream]
public Stream GetData();
The json serializer defaults to `System.Text.Json`
Change the json serializer to NewtonsoftJson
- 1. add package `Norns.Urd.HttpClient.NewtonsoftJson`
- 2. Registed in ioc, such as
new ServiceCollection().AddHttpClientNewtonsoftJosn()
Custom serializer
When the existing serializer is not enough to support the demand,
Just implement `IHttpContentSerializer` and register with the ioc container
Custom Header
In addition to the headers mentioned above, you can also add other headers
There are also the following two ways:
Use `HeaderAttribute` in interface or method static configuration
[Header("x-data", "money")]
public interface ITestClient {}
[Header("x-data", "money")]
public Data GetData();
Dynamic configuration of method parameters
public Data GetData([SetRequestHeader("x-data")]string header);
Custom HttpRequestMessageSettingsAttribute
When the existing `HttpRequestMessageSettingsAttribute` is not enough to support the demand,
Just inherit `HttpRequestMessageSettingsAttribute` to realize your own functions,
Just use it in the corresponding interface/method
Get through parameter setting Response Header
When sometimes we need to get the header returned by the response,
We can get the value of Response Header by out parameter + `OutResponseHeaderAttribute`
(Note that only the synchronization method, the out parameter can work)
example:
public Data GetData([OutResponseHeader("x-data")] out string header);
How to set HttpClient
MaxResponseContentBufferSize
[MaxResponseContentBufferSize(20480)]
public interface ITestClient {}
[MaxResponseContentBufferSize(20480)]
public Data GetData()
Timeout
[Timeout("00:03:00")]
public interface ITestClient {}
[Timeout("00:03:00")]
public Data GetData()
ClientName
When you need to combine HttpClientFactory to obtain a specially set HttpClient, you can specify it by `ClientNameAttribute`
example:
[ClientName("MyClient")]
public interface ITestClient {}
[ClientName("MyClient")]
public Data GetData()
You can get the HttpClient specified in this way
services.AddHttpClient("MyClient", i => i.MaxResponseContentBufferSize = 204800);
HttpCompletionOption
The CompletionOption parameter when calling HttpClient can also be set
HttpCompletionOption.ResponseHeadersRead is the default configuration
example:
[HttpCompletionOption(HttpCompletionOption.ResponseContentRead)]
public interface ITestClient {}
[HttpCompletionOption(HttpCompletionOption.ResponseContentRead)]
public Data GetData()
Global HttpRequestMessage and HttpResponseMessage handler
If you need to do some processing on HttpRequestMessage and HttpResponseMessage globally, such as:
- Link tracking id setting
- Customized handling of response exceptions
Can be used by implementing `IHttpClientHandler` and registering with the ioc container
For example, the default status code check, such as:
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;
}
}
Of course, if the StatusCode check processing is not needed, it can be cleared directly in the ioc container, such as:
services.RemoveAll<IHttpClientHandler>();
services.AddSingleton<IHttpClientHandler, xxx>();
Some Design of Norns.Urd
Implementation Premise of Norns.Urd
-
Support both sync / async method and user can choose sync or async.
-
The good thing about this is that it’s twice as much work. Sync and Async are completely split into two implementations.
-
The Interceptor
interface provided to the user needs to provide a solution that combines Sync and Async in one set of implementation code. After all, the user cannot be forced to implement two sets of code. Many scenario users do not need to implement two sets of code for the difference between Sync and Async.
-
No DI Implementation, but work fine with DI.
-
If the built-in DI container can make support generic scene is very simple, after all, from the DI container instantiation objects must have a definite type, but, now there are so many implementation libraries, I don’t want to realize many functions for some scenarios (am I really lazy, or the library can’t write that long).
-
But DI container does decoupling is very good, I often benefit and decrease a lot of code changes, so aop libraries must be considered based on the DI container support, in this case, DI support opens generic/custom instantiation method to support, DI and aop inside have to provide user call method, otherwise it doesn’t work out (so calculate down, am I really lazy? Am I digging a hole for myself?)
How to Resolve These Problems?
The current solution is not necessarily perfect, but it has solved the problem temporarily (please tell me if there is a better solution, I urgently need to learn).
What Interceptor Writing Patterns are Provided to the User?
I have encountered some other AOP implementation frameworks in the past, many of which require the intercepting code to be divided into method before/method after/with exceptions, etc. Personally, I think this form affects the code thinking of the interceptor implementation to some extent, and I always feel that it is not smooth enough.
But ASP.NET Core Middleware feels pretty good, as shown in the following figure and code:
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
The interceptor should also be able to do this, so the interceptor code should look like this:
public class ConsoleInterceptor
{
public async Task InvokeAsync(Context context, Delegate next)
{
Console.WriteLine("Hello, World!");
await next(context);
}
}
How do the Sync and Async methods split up? How can they be combined? How do users choose to implement sync or Async or both?
public delegate Task AsyncAspectDelegate(AspectContext context);
public delegate void AspectDelegate(AspectContext context);
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();
}
public abstract Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);
}
No DI and How to Support Other DI?
DI framework has registration type, we can use emit to generate proxy class, replace the original registration, can achieve compatibility.
Of course, each DI framework requires some custom implementation code to support (alas, workload again).
How to Support AddTransient<IMTest>(x => new NMTest())?
Due to the usage of this DI framework, it is not possible to get the actual type to be used through the Func
function. Instead, it can only generate the bridge proxy type through the emit according to the definition of IMTest
. The pseudo-code looks like the following:
interface IMTest
{
int Get(int i);
}
class IMTestProxy : IMTest
{
IMTest instance = (x => new NMTest())();
int Get(int i) => instance.Get(i);
}
How to Support .AddTransient(typeof(IGenericTest<,>), typeof(GenericTest<,>)) ?
The only difficulty is that it is not easy to generate method calls such as Get<T>()
, because IL needs to reflect the specific methods found, such as Get<int>()
,Get<bool>()
, etc., it cannot be ambiguous Get<T>()
.
The only way to solve this problem is to defer the actual invocation until the runtime invocation is regenerated into a specific invocation. The pseudo-code is roughly as follows:
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);
}
History
- 14th December, 2020: Initial version