切换为 中文文档
ps: the doc is generated by some language translator, so maybe some are difficult to understand, sorry, ;)
Contents
- Welcome to Norns.Urd
- Simple Benchmark
- Quick start
- Fundamentals
- Some design of Norns.Urd
Welcome to Norns.Urd
Norns.urd is a lightweight AOP framework based on emit which do dynamic proxy.
It base 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 can 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 useful to you
By the way, if you’re not familiar with AOP, check out these articles:
Simple Benchmark
Just 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 | 61.77 ns | 0.311 ns | 0.291 ns | 0.0178 | - | - | 112 B |
TransientInstanceCallSyncMethodWhenNornsUrd | 155.58 ns | 1.038 ns | 0.971 ns | 0.0548 | - | - | 344 B |
TransientInstanceCallSyncMethodWhenCastle | 213.94 ns | 1.213 ns | 1.076 ns | 0.0815 | - | - | 512 B |
TransientInstanceCallSyncMethodWhenAspectCore | 508.71 ns | 2.334 ns | 2.183 ns | 0.1030 | - | - | 648 B |
TransientInstanceCallAsyncMethodWhenNoAop | 92.58 ns | 0.793 ns | 0.619 ns | 0.0408 | - | - | 256 B |
TransientInstanceCallAsyncMethodWhenNornsUrd | 242.98 ns | 0.818 ns | 0.765 ns | 0.0892 | - | - | 560 B |
TransientInstanceCallAsyncMethodWhenCastle | 225.98 ns | 0.521 ns | 0.487 ns | 0.1044 | - | - | 656 B |
TransientInstanceCallAsyncMethodWhenAspectCore | 565.25 ns | 2.377 ns | 2.107 ns | 0.1373 | - | - | 864 B |
Quick start
This is simple demo whch to do global interceptor, full code fot the demo you can see 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 be 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(); }
-
add GlobalInterceptor to di
// 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())); }
-
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 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
{
// 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);
}
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;
// 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);
}
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
{
// or
//[AddTenInterceptor]
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.
Interceptor
Interceptor 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
{
// or
//[AddTenInterceptor]
T GetT();
}
So just use what the user thinks is convenient
Interceptor filter mode
Norns.Urd Provide 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 mehtod 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();
// 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;
}
- 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))] // 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 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)] // 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 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")] // Live for 5 minutes
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(/*You can specify your own Name*/))
.AddDistributedMemoryCache() // You can switch to any DistributedCache implementation
- 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")] // Does not specify a cache name CacheOptions.DefaultCacheName = "memory"
public virtual Task<int> DoAsync(int count);
Multistage cache
[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);
Customize the cache configuration
Often, we need to get the cache configuration dynamically, and we can customize the configuration simply by inheriting ‘ICacheOptionGenerator’
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 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
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
- add package Norns.Urd.HttpClient
dotnet add package Norns.Urd.HttpClient
- enable HttpClient
new ServiceCollection()
.ConfigureAop(i => i.EnableHttpClient())
- 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);
}
- add to 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())
- 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);
//or
[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
- ValueTask
- 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
- add package
Norns.Urd.HttpClient.NewtonsoftJson
- 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 {}
//or
[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 {}
//or
[MaxResponseContentBufferSize(20480)]
public Data GetData()
Timeout
[Timeout("00:03:00")]
public interface ITestClient {}
//or
[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 {}
//or
[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 {}
//or
[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>();
// Then add your own processing
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 by like
-
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 library, I don’t want to realize many functions for some scenarios (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 do a aop libraries must be considered based on the DI container do support, in this case, DI support open generic/custom instantiation method to do support, DI and aop inside have to provide user call method, otherwise it doesn’t work out (so calculate down, I really lazy? Am I digging a hole for myself?)
-
How to resolve these problem?
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 like ASP.NET Core Middlewarefeels 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);
// 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 awit 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
}
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);
}