Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Norns.Urd Lightweight AOP Framework

0.00/5 (No votes)
3 Jan 2021 1  
Norns.Urd is lightweight AOP framework based on emit which does dynamic proxy
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

build GitHub

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.

  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 as 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 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. 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
{
    // 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 theAttribute 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.

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
    {
        // or
        //[AddTenInterceptor]
        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:

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

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>()  // 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. 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<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. 1. add package `Norns.Urd.HttpClient.NewtonsoftJson`
  2. 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 {}
//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

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

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

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

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here