danielwertheim

danielwertheim


notes from a passionate developer

Share


Sections


Tags


Disclaimer

This is a personal blog. The opinions expressed here represent my own and not those of my employer, nor current or previous. All content is published "as is", without warranty of any kind and I don't take any responsibility and can't be liable for any claims, damages or other liabilities that might be caused by the content.

ASP.NET, registration of a proxy-decorator service in ServiceCollection

I recently found myself, having the need of intercepting calls to a service from an external NuGet package. The Service was registered in the Service Collection via extension methods and I wanted it to be "intact", seen to configuration and dependencies etc. I needed to register a proxy/decorator, with the same lifetime, so that I could intercept the calls. Hence why services.Replace failed for me.
What I wanted was to be able to do something like the following (please guide me if there's already built in support for this):

Given:

services.AddExternalService();

//Simulated via:
// services.AddScoped<IExternalService, ExternalService>();

I wanted to be able to do:

services.Proxy<IExternalService>(r => new InterceptingService(
    r.ServiceProvider.GetService<ILogger<InterceptingService>>(),
    r.Proxied));

Solution

ServiceDescriptor to the rescue. All I needed to do was something in the lines of below (sample code quality, adapt to your needs). It might look a bit intimidating, but that's just because there are a bunch of overloads, you can of course reduce it if you want.

public static class ServiceProxyExtensions
{
    public static IServiceCollection Proxy(
        this IServiceCollection services,
        ServiceDescriptor toProxy,
        Func<(IServiceProvider ServiceProvider, Type ProxiedType), object> proxyServiceFactory)
    {
        var proxiedType = toProxy.ImplementationType ?? throw new Exception(
            $"Can not register proxy service, as no implementation type was found for {toProxy.ServiceType}");

        services.Remove(toProxy);
        services.Add(ServiceDescriptor.Describe(proxiedType, proxiedType, toProxy.Lifetime));
        services.Add(ServiceDescriptor.Describe(toProxy.ServiceType, sp => proxyServiceFactory((sp, proxiedType)),
            toProxy.Lifetime));

        return services;
    }

    public static IServiceCollection Proxy(
        this IServiceCollection services,
        Type serviceType,
        Type proxiedType,
        Func<(IServiceProvider ServiceProvider, Type ProxiedType), object> proxyServiceFactory)
    {
        var toProxy = services.Single(sd =>
            sd.ServiceType == serviceType &&
            sd.ImplementationType == proxiedType);

        return services.Proxy(toProxy, proxyServiceFactory);
    }

    public static IServiceCollection Proxy(
        this IServiceCollection services,
        Type serviceType,
        Func<(IServiceProvider ServiceProvider, Type ProxiedType), object> proxyServiceFactory)
    {
        var toProxy = services.Single(sd =>
            sd.ServiceType == serviceType);

        return services.Proxy(toProxy, proxyServiceFactory);
    }

    public static IServiceCollection Proxy<TService, TProxied>(
        this IServiceCollection services,
        Func<(IServiceProvider ServiceProvider, TService Proxied), TService> proxyServiceFactory)
        => services.Proxy(typeof(TService), typeof(TProxied), r =>
        {
            var proxied = (TService) r.ServiceProvider.GetService(r.ProxiedType);

            return proxyServiceFactory((r.ServiceProvider, proxied));
        });

    public static IServiceCollection Proxy<TService>(
        this IServiceCollection services,
        Func<(IServiceProvider ServiceProvider, TService Proxied), TService> proxyServiceFactory)
        => services.Proxy(typeof(TService), r =>
        {
            var proxied = (TService) r.ServiceProvider.GetService(r.ProxiedType);

            return proxyServiceFactory((r.ServiceProvider, proxied));
        });
}

This allows me to do:

Option 1:

services.Proxy(
    typeof(IExternalService),
    typeof(ExternalService),
    r => new InterceptingService(
        r.ServiceProvider.GetService<ILogger<InterceptingService>>(),
        (IExternalService) r.ServiceProvider.GetService(r.ProxiedType)));

Option 2:

services.Proxy(
    typeof(IExternalService),
    r => new InterceptingService(
        r.ServiceProvider.GetService<ILogger<InterceptingService>>(),
        (IExternalService) r.ServiceProvider.GetService(r.ProxiedType)));

Option 3:

services.Proxy<IExternalService, ExternalService>(r => new InterceptingService(
    r.ServiceProvider.GetService<ILogger<InterceptingService>>(),
    r.Proxied));

Option 4:

services.Proxy<IExternalService>(r => new InterceptingService(
    r.ServiceProvider.GetService<ILogger<InterceptingService>>(),
    r.Proxied));

So if we has something like these services:

public interface IExternalService
{
    void Execute<T>(T input);
}

public class ExternalService : IExternalService
{
    private readonly ILogger<ExternalService> _logger;

    public ExternalService(ILogger<ExternalService> logger)
    {
        _logger = logger;
    }
    public void Execute<T>(T input)
    {
        _logger.LogInformation("In external service doing normal stuff");
    }
}

public class InterceptingService : IExternalService
{
    private readonly ILogger<InterceptingService> _logger;
    private readonly IExternalService _innerExternalService;
    
    public InterceptingService(
        ILogger<InterceptingService> logger,
        IExternalService innerExternalService)
    {
        _logger = logger;
        _innerExternalService = innerExternalService;
    }

    public void Execute<T>(T input)
    {
        _logger.LogInformation("In intercepting service");
        _innerExternalService.Execute(input);
    }
}

We could use it in e.g an Controller:

[ApiController]
[Route("[controller]")]
public class MyController : ControllerBase
{
    private readonly ILogger<MyController> _logger;
    private readonly IExternalService _externalService;

    public MyController(ILogger<MyController> logger, IExternalService externalService)
    {
        _logger = logger;
        _externalService = externalService;
    }

    [HttpGet]
    public void Get()
    {
        _logger.LogInformation("Controller invoking service.");
        _externalService.Execute(42);
    }
}

And the output would be:

info: DiProxy.Controllers.MyController[0]
      Controller invoking service.
info: DiProxy.Services.InterceptingService[0]
      In intercepting service
info: DiProxy.Services.ExternalService[0]
      In external service doing normal stuff

Hope you can use it to something. Please, feel free to guide me if it's already built-in.

//Daniel

View Comments