Skip to content

Powered by Castle Core, the AdapterInterceptor enables the generation of efficient dynamic proxies for proxying or adapting invocation contracts without requiring virtual methods on the target.

License

akovac35/AdapterInterceptor

Repository files navigation

AdapterInterceptor

This library contains AdapterInterceptor. Powered by Castle Core, the AdapterInterceptor enables the generation of efficient dynamic proxies for proxying or adapting invocation contracts without requiring virtual methods on the target.

this

Status

PRODUCTION READY starting from version 1.0.0.

Samples

Samples utilizing library functionality are provided here: AdapterInterceptor.Samples

Summary

It is often the case that custom DTOs are used in different application layers for a specific service type, which involves method by method mappings between input and output types. Usually this is implemented by tedious and error prone custom code. This library provides a solution for removing the need for such code.

In addition, it is often the case that a type compiled by a third party must be proxied for reasons of testing it or for wrapping its methods with additional functionality. In .NET, this is typically achieved by generating a dynamic proxy, which for the popular dynamic proxy generator Castle Core requires that the target type contains virtual methods - which is rarely the case. This library provides a solution for removing the requirement for the methods to be virtual.

Contents

Enterprise applications typically rely on many backend systems, each of which defines its invocation contract and data transfer objects (DTOs). Integration with backend systems may rely in part on autogenerated code which commonly does not declare interfaces or virtual methods, which limits customization. An example of such integration is the following BlogService client and a corresponding Blog data transfer object (DTO) which would typically be used in several places of a particular application layer:

public class BlogService : IDisposable
{
    public BlogService(BlogContext context, ILogger<BlogService> logger = null)
    {
        // ...
    }

    public BlogContext Context { get; set; }

    public int Count
    {
        get
        {
            // ...
        }
    }

    public async Task<Blog> Add(string url = "https://defaulturl.com")
    {
        // ...
    }

    public async Task<Blog> Add(Blog blog)
    {
        // ...
    }

    public IEnumerable<Blog> Find(string term)
    {
        // ...
    }

    public bool TryGet(long blogId, out Blog result)
    {
        // ...
    }

    public void Dispose()
    {
        // ...
    }
}

public class Blog
{
    public long Id { get; set; }

    public string Url { get; set; }
}

There may be many other such services, each using custom DTOs. Directly integrating services in a particular application layer will result in many direct software dependencies and increased effort for developers, who will have to understand service-specific DTO details at all times. Suppose that instead of the described approach we extract the invocation contract of a generated service into an interface which uses a custom DTO (facade DTO, binding DTO, etc.) instead of the originally generated one, and also have automated means to translate the calls to this custom interface to target invocation contract and similarly translate the results back to the caller; this would remove direct software dependencies and also alleviate developers' efforts, who will now only have to understand the custom DTO - mappings between this custom DTO and service-specific DTO only have to be defined once, possibly by some other person or team.

The described approach simplifies development, enforces software development practices and increases software reliability by delegating data mapping to well-defined software components. AdapterInterceptor is designed for the described approach and enables using custom DTOs for the target invocation. To illustrate the idea, suppose that with the autogenerated code, or at some other place, we define the following custom interface for the BlogService example:

/// <summary>
/// This adapter interface is provided for the benefit of the service consumer without requiring that the BlogService implements it. An adapter can be readily generated based on this interface.
/// The BlogService implements the IDisposable interface so we make our interface inherit it as well.
/// </summary>
/// <typeparam name="T">The type of the blog data transfer object.</typeparam>
public interface IBlogServiceAdapter<T>: IDisposable
{
    BlogContext Context { get; set; }

    int Count { get; }

    // Also supported are Task, ValueTask and ValueTask<T> result types. Default adapter method parameters must be specified in the same position as they are in the target method
    Task<T> Add(string url = "https://defaulturl.com");

    Task<T> Add(T blog);

    IEnumerable<T> Find(string term);

    // out and ref parameter modifiers are supported as well
    bool TryGet(long blogId, out Blog result);
}

Furthermore, in a particular application layer there exists a custom DTO, for example, the following one which is the same as the original but has some required data binding attributes added:

public class BlogDto
{
    public long Id { get; set; }

    [Required]
    [StringLength(20, ErrorMessage = "Url is too long.")]
    public string Url { get; set; }
}

We should be able to invoke the BlogService with BlogDto via a dynamic proxy instance of IBlogServiceAdapter<BlogDto> as follows:

await blogService.Add(BlogDto)

The actual target invocation and input and return value mapping is taken care of by the AdapterInterceptor library and our custom type mapping code.

A variant of AdapterInterceptor, the ProxyImitatorInterceptor, can be used when there is only a need for proxying non-virtual methods. Because no mapping is involved, it is easier to set up the proxy imitator.

Usage

AdapterInterceptor must be used together with the Castle DynamicProxy. There are several constructor variants available but generally, the following variant should typically be used: AdapterInterceptor<TTarget, TSource1, TDestination1, ...>(TTarget target, IAdapterMapper adapterMapper, ILoggerFactory? loggerFactory = null)

TSource1 and TDestination1 are types to which type mapping applies. Support for reverse mapping as well as mappings and reverse mappings of common collection variants, e.g. TSource[] and IList<TDestination> is implicitly assumed. AdapterInterceptor constructor accepts an IAdapterMapper which will actually perform type mapping and which must support the assumptions if those are encountered during invocation. A DefaultAdapterMapper is provided and it uses AutoMapper:

using AutoMapper;
using System;

namespace com.github.akovac35.AdapterInterceptor
{
    /// <summary>
    /// Default type used to perform mapping from the source object to a new destination object using AutoMapper.
    /// </summary>
    public class DefaultAdapterMapper : IAdapterMapper
    {
        /// <summary>
        /// Initializes a new DefaultAdapterMapper instance.
        /// </summary>
        /// <param name="mapper">AutoMapper instance used for object mapping. Must support reverse mapping for method invocation result mapping.</param>
        public DefaultAdapterMapper(IMapper mapper) : this()
        {
            Mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
        }

        protected DefaultAdapterMapper()
        {
            Mapper = null!;
        }

        protected IMapper Mapper { get; set; }

        public virtual object? Map(object? source, Type sourceType, Type destinationType)
        {
            object? destination = null;
            if (sourceType == destinationType)
            {
                destination = source;
            }
            else
            {
                destination = Mapper.Map(source, sourceType.IsByRef ? sourceType.GetElementType() : sourceType, destinationType.IsByRef ? destinationType.GetElementType() : destinationType);
            }

            return destination;
        }
    }
}

A sample of AdapterInterceptor instantiation for ASP.NET Core dependency injection is provided below; first we define a handy extension method for the IServiceCollection:

using com.github.akovac35.AdapterInterceptor;
using com.github.akovac35.AdapterInterceptor.DependencyInjection;
// ...

public static class ServiceCollectionExtensions
{       
    public static IServiceCollection AddScopedBlogServiceAdapter<T>(this IServiceCollection services)
    {
        // Register dependencies
        // ...
        
        // Register the adapter
        // The IBlogServiceAdapter interface inherits the IDisposable interface. When the scope is closed, the adapter instance will be disposed of by the DI framework, which will also invoke the Dispose() method on the target through the AdapterInterceptor. Note we have to release the AdapterInterceptor to release the target, it is never released by the Dispose() method invocation
        services.AddAdapter<IBlogServiceAdapter<T>, BlogService>(targetFact =>
        {
            // Obtain the target - the adaptee
            var blogService = targetFact.GetService<BlogService>();
            return blogService;
        }, (serviceProvider, target) =>
        {
            // We can use the com.github.akovac35.AdapterInterceptor.DefaultAdapterMapper, which uses AutoMapper, or a custom class implementing com.github.akovac35.AdapterInterceptor.IAdapterMapper
            var mapperConfiguration = serviceProvider.GetService<MapperConfiguration>();
            var adapterMapper = new DefaultAdapterMapper(mapperConfiguration.CreateMapper());
            var adapterInterceptor = new CustomAdapterInterceptor<BlogService, Blog, T>(target, adapterMapper);
            return adapterInterceptor;
        }, ServiceLifetime.Scoped);

        return services;
    }
}

and use it in Startup.cs:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScopedBlogServiceAdapter<BlogDto>();
    }
}

An adapter can also be created as follows:

using com.github.akovac35.AdapterInterceptor;
using AutoMapper;

var service = new TestService();
var mapperConfig = new MapperConfiguration(cfg => cfg.CreateMap<CustomTestType, TestType>().ReverseMap());
var mapper = new DefaultAdapterMapper(mapperConfig.CreateMapper());
var adapter = service.GenerateAdapter<ICustomTestService<CustomTestType>, TestService>(target => new AdapterInterceptor<TestService, CustomTestType, TestType>(target, mapper));
var result = adapter.MethodUsingOneArgument(new CustomTestType());

Registering and using the ProxyImitatorInterceptor, which is a variant of AdapterInterceptor, is very similar to the above:

using com.github.akovac35.AdapterInterceptor.DependencyInjection;
// ...

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddScopedBlogServiceProxyImitator(this IServiceCollection services)
    {
        // Register dependencies
        // ...
            
        // Register the proxy imitator
        // The IBlogServiceProxyImitator interface inherits the IDisposable interface. When the scope is closed, the proxy imitator instance will be disposed of by the DI framework, which will also invoke the Dispose() method on the target through the ProxyImitatorInterceptor. Note we have to release the ProxyImitatorInterceptor to release the target, it is never released by the Dispose() method invocation
        services.AddProxyImitator<IBlogServiceProxyImitator, BlogService>(targetFact =>
        {
            // Obtain the target to be proxied
            var blogService = targetFact.GetService<BlogService>();
            return blogService;
        }, (serviceProvider, target) =>
        {
            var proxyImitatorInterceptor = new CustomProxyImitatorInterceptor<BlogService>(target);
            return proxyImitatorInterceptor;
        }, ServiceLifetime.Scoped);

        return services;
    }
}

A proxy imitator can also be generated as follows:

using com.github.akovac35.AdapterInterceptor;

var service = new TestService();
var proxyImitator = service.GenerateProxyImitator<ITestServiceProxyImitator, TestService>(target => new ProxyImitatorInterceptor<TestService>(target));
var result = proxyImitator.MethodUsingOneArgument(new TestType());

The IBlogServiceAdapter<T> and ITestServiceProxyImitator interfaces can be easily created in Visual Studio by navigating to the target type definition and either copying the screen contents, for types compiled by a third party, or extracting an interface, for types with the source code.

AdapterInterceptor must always be the last interceptor in the Castle DynamicProxy interceptor order. AdapterInterceptor instance is thread-safe and lightweight and supports singleton, scoped and transient instantiation.

The following is supported:

  • out and ref (currently replace only) parameters,
  • Task, generic Task, ValueTask, generic ValueTask,
  • default parameter values,
  • customization through extension,
  • adapter interface can extend another interface, for example IDisposable, if the target implements it. Note that for IDisposable the target is only released when the proxy instance is released - but not when it is disposed.

Extension example:

using Castle.DynamicProxy;
using com.github.akovac35.AdapterInterceptor;
using com.github.akovac35.AdapterInterceptor.Misc;
using com.github.akovac35.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Reflection;
using System.Threading.Tasks;

namespace WebApp.Blogs
{
    public class CustomAdapterInterceptor<TTarget, TSource1, TDestination1> : AdapterInterceptor<TTarget, TSource1, TDestination1>
        where TTarget : notnull
    {
        public CustomAdapterInterceptor(TTarget target, IAdapterMapper adapterMapper, ILoggerFactory? loggerFactory = null) : base(target, adapterMapper, loggerFactory)
        {
            _logger = (loggerFactory?.CreateLogger<CustomAdapterInterceptor<TTarget, TSource1, TDestination1>>()) ?? (ILogger)NullLogger.Instance;
        }

        private ILogger _logger;

        protected override object? InvokeTargetSync(MethodInfo adapterMethod, AdapterInvocationInformation adapterInvocationInformation, object?[] targetArguments, IInvocation invocation)
        {
            _logger.Here(l => l.LogInformation("Hello from a custom interceptor."));
            var result = base.InvokeTargetSync(adapterMethod, adapterInvocationInformation, targetArguments, invocation);
            return result;
        }

        protected override async Task<TAdapter> InvokeTargetGenericTaskAsync<TAdapter>(MethodInfo adapterMethod, AdapterInvocationInformation adapterInvocationInformation, object?[] targetArguments)
        {
            _logger.Here(l => l.LogInformation("Hello from a custom interceptor."));
            var result = await base.InvokeTargetGenericTaskAsync<TAdapter>(adapterMethod, adapterInvocationInformation, targetArguments);
            return result;
        }

        // And similarly for Task and (generic) ValueTask
    }
}

Logging

AdapterInterceptor supports invocation and return value logging when TRACE logger level is enabled for com.github.akovac35.AdapterInterceptor. This can be quite verbose but enables method invocation diagnostics. It is possible to completely disable logging by simply not providing a logger factory. To prepare the AdapterInterceptor for logging simply pass an instance of Microsoft.Extensions.Logging.ILoggerFactory to the constructor:

#if DEBUG
    var loggerFactory = fact.GetService<ILoggerFactory>();
    var adapterInterceptor = new AdapterInterceptor<BlogService, Blog, T>(blogService, adapterMapper, loggerFactory);
#else
    var adapterInterceptor = new AdapterInterceptor<BlogService, Blog, T>(blogService, adapterMapper);
#endif

Log example:

[2020-06-02 20:23:03.413 +02:00] TRA 10 6c95df29-6a8f-41b2-be9a-387443f3a40c <com.github.akovac35.AdapterInterceptor.AdapterInterceptor:Intercept:88> Entering: ["{Invocation: {MethodInfo: get_Count, Parameters: {ParameterInfo[0]: []}, ReturnType: {Type: System.Int32}, DeclaringType: {Type: Shared.Blogs.IBlogServiceAdapter`1[[WebApp.Blogs.BlogDto, WebApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]}}}",[],null]
[2020-06-02 20:23:03.431 +02:00] TRA 10 6c95df29-6a8f-41b2-be9a-387443f3a40c <Shared.Blogs.BlogService:Count:38> Entering
[2020-06-02 20:23:03.431 +02:00] TRA 10 6c95df29-6a8f-41b2-be9a-387443f3a40c <Shared.Blogs.BlogService:Count:42> Exiting: [2]
[2020-06-02 20:23:03.447 +02:00] TRA 10 6c95df29-6a8f-41b2-be9a-387443f3a40c <com.github.akovac35.AdapterInterceptor.AdapterInterceptor:InvokeTargetSync:149> Target method result: 2, adapter method result: 2
[2020-06-02 20:23:03.447 +02:00] TRA 10 6c95df29-6a8f-41b2-be9a-387443f3a40c <com.github.akovac35.AdapterInterceptor.AdapterInterceptor:Intercept:131> Exiting

Note that non-mappable invocation and return values may be enumerated by the logger implementation when logging is enabled.

Performance

The AdapterInterceptor library is designed for good performance within described usage patterns. The target method invocation overhead without accounting for the actual type mapping is in the order of microseconds, which is negligible. Below is a benchmark which includes complete dynamic proxy overhead with logging disabled:

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18362.778 (1903/May2019Update/19H1) Intel Core i7-2760QM CPU 2.40GHz (Sandy Bridge), 1 CPU, 4 logical and 4 physical cores .NET Core SDK=3.1.201 [Host] : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT DefaultJob : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT

Method Categories Mean [μs] Error [μs] StdDev [μs] Ratio RatioSD
Direct Five arguments, sync 0.0058 0.0002 0.0002 1.00 0.00
'Adapter, one mapping' Five arguments, sync 0.8740 0.0086 0.0081 149.05 6.29
'Adapter, several mappings' Five arguments, sync 0.8952 0.0172 0.0184 153.45 6.95
Direct Five arguments, async 0.0654 0.0014 0.0017 1.00 0.00
'Adapter, one mapping' Five arguments, async 3.1376 0.0281 0.0249 48.09 1.35
'Adapter, several mappings' Five arguments, async 3.0318 0.0234 0.0219 46.41 1.54
Direct Two arguments, sync 0.0053 0.0002 0.0002 1.00 0.00
'Adapter, one mapping' Two arguments, sync 0.6131 0.0121 0.0125 116.47 3.12
'Adapter, several mappings' Two arguments, sync 0.6091 0.0081 0.0076 115.57 3.55
Direct Two arguments, async 0.0627 0.0005 0.0004 1.00 0.00
'Adapter, one mapping' Two arguments, async 2.8512 0.0568 0.0531 45.32 0.91
'Adapter, several mappings' Two arguments, async 2.7860 0.0349 0.0326 44.37 0.67

Release history

  • 1.0.0 - Production ready.
  • 1.0.3 - updated dependencies, added extensions for object and IServiceCollection, added ProxyImitatorInterceptor and tests.

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please make sure to update tests as appropriate.

License

Apache-2.0

About

Powered by Castle Core, the AdapterInterceptor enables the generation of efficient dynamic proxies for proxying or adapting invocation contracts without requiring virtual methods on the target.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published