Skip to the content.

切换为 中文文档

ps: the doc is generated by some language translator, so maybe some are difficult to understand, sorry, ;)

Contents

Welcome to Norns.Urd

build GitHub GitHub Repo stars

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:

Hopefully, this library will be of some useful to you

By the way, if you’re not familiar with AOP, check out these articles:

Aspect-oriented programming

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
MethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
TransientInstanceCallSyncMethodWhenNoAop61.77 ns0.311 ns0.291 ns0.0178--112 B
TransientInstanceCallSyncMethodWhenNornsUrd155.58 ns1.038 ns0.971 ns0.0548--344 B
TransientInstanceCallSyncMethodWhenCastle213.94 ns1.213 ns1.076 ns0.0815--512 B
TransientInstanceCallSyncMethodWhenAspectCore508.71 ns2.334 ns2.183 ns0.1030--648 B
TransientInstanceCallAsyncMethodWhenNoAop92.58 ns0.793 ns0.619 ns0.0408--256 B
TransientInstanceCallAsyncMethodWhenNornsUrd242.98 ns0.818 ns0.765 ns0.0892--560 B
TransientInstanceCallAsyncMethodWhenCastle225.98 ns0.521 ns0.487 ns0.1044--656 B
TransientInstanceCallAsyncMethodWhenAspectCore565.25 ns2.377 ns2.107 ns0.1373--864 B

Quick start

This is simple demo whch to do global interceptor, full code fot the demo you can see Examples.WebApi

  1. 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);
             }
         }
     }
    
  2. set WeatherForecastController’s method be 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. 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()));
     }
    
  5. 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
[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

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

public void ConfigureServices(IServiceCollection services)
{
    services.ConfigureAop(i => i.GlobalInterceptors.Add(new AddSixInterceptor()));
}
[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

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 limit

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

demo

We will complete a simple httpClient as an example. Here is a brief demo

  1. 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);
    }
}
  1. 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;
}
  1. Registered client
services.AddTransient<IAddTest>();
services.ConfigureAop();
  1. 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:

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

Absolute expiration, which means it expires at the set time

[Cache(..., AbsoluteExpiration = "1991-05-30 00:00:00")]
void Do()

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

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

  1. add package Norns.Urd.HttpClient
    dotnet add package Norns.Urd.HttpClient
    
  2. enable HttpClient
new ServiceCollection()
    .ConfigureAop(i => i.EnableHttpClient())
  1. 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);
}
  1. 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())
  1. 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:

(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:

[Post("http://localhost.:5000/money/getData/")]
public Data GetData()

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

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

example:

[OctetStreamContentType]
public Data GetData([Body]Stream s);

The corresponding Accept is set to

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:

[Header("x-data", "money")]
public interface ITestClient {}
//or
[Header("x-data", "money")]
public Data GetData();
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:

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

  1. 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

  2. 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:

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!");
});

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);
}