Monitoring a .NET app using Castle Core

We saw in a previous post, Monitoring a .NET app using Scrutor, the decorator pattern. It's great but it comes with a big limitation: it is not scalable. Indeed, as the decorator classes have to implement the interface they decorate, each time you add a method you will have to update all the decorators. It's not a really big issue but it's clearly not elegant, and only elegant methods are really good. This is where Castle, its dynamic proxies and the interceptor pattern come in.

Castle DynamicProxy is a library for generating lightweight .NET proxies on the fly at runtime. They allow us to intercept calls to the original class and to overwrite their behavior. As you can see below, it's a common practice in software development. For example, NHibernate uses them to provide lazy loading of data and several mocking frameworks use them to intercept method/property calls, like this:

Castle DynamicProxy used by the mocking library Moq

Castle DynamicProxy allows you to proxy a class to add additional features without modifying it like logging or security checking. Let's see how to use it to add diagnostics monitoring traces.

We start with an empty .NET 6 API application (like in the post Monitoring a .NET app using Scrutor) and we do the same thing, which is to create a WeatherService called in the controller.


public interface IWeatherService
{
    WeatherForecast[] GetForecast();
}

public class WeatherService : IWeatherService
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecast[] GetForecast()
    {
        Thread.Sleep(Random.Shared.Next(10, 50));

        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly ILogger<WeatherForecastController> _logger;
    private readonly IWeatherService _weatherService;

    public WeatherForecastController(ILogger<WeatherForecastController> logger, IWeatherService weatherService)
    {
        _logger = logger;
        _weatherService = weatherService;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        return _weatherService.GetForecast();
    }
}

Now we add the Castle.Core NuGet package to our application, and create our diagnostic proxy:


public class DurationInterceptor : IInterceptor
{
    private readonly ILogger<DurationInterceptor> _logger;

    public DurationInterceptor(ILogger<DurationInterceptor> logger)
    {
        _logger = logger;
    }

    public void Intercept(IInvocation invocation)
    {
        var sw = Stopwatch.StartNew();
        try
        {
            invocation.Proceed();
        }
        finally
        {
            sw.Stop();
            _logger.LogInformation("{MethodName} method took {Duration}ms", invocation.Method.Name, sw.ElapsedMilliseconds);
        }
    }
}

As you can see, it's a basic behavior: we just decorate the original method processing with diagnostic monitoring to log how much time the method took to execute.

The last thing to do is to register our proxy in the Dependency Injection container. We could do it directly in the main program class but, to do it the clean way, we will create an extension method which can handle registering as many interceptor classes as we need.


using Castle.DynamicProxy;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace DynamicProxyLab.CastleCore.Api.Extensions
{
    internal static class InterceptionExtensions
    {
        internal static void AddInterceptedSingleton<TInterface, TImplementation, TInterceptor>(this IServiceCollection services)
            where TInterface : class
            where TImplementation : class, TInterface
            where TInterceptor : class, IInterceptor
        {
            services.TryAddSingleton<IProxyGenerator, ProxyGenerator>();
            services.AddSingleton<TImplementation>();
            services.TryAddTransient<TInterceptor>(); // We use a Transient lifetime to not register multiple times the same interceptor
            services.AddSingleton(provider =>
            {
                var proxyGenerator = provider.GetRequiredService<IProxyGenerator>();
                var implementation = provider.GetRequiredService<TImplementation>();
                var interceptor = provider.GetRequiredService<TInterceptor>();
                return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(implementation, interceptor);
            });
        }
    }
}

Finally we use our extension method to register our interceptor:


builder.Services.AddInterceptedSingleton<IWeatherService, WeatherService, DurationInterceptor>();

If we run our web application now, we can see that our WeatherService is intercepted by our DurationInterceptor, so we can find traces about its completion time:

You can check the source code on my GitHub: @AdrienTorris

June 13, 2022
  • .NET
  • ASP.NET Core
  • .NET 6
  • Castle Core
  • Dependency Injection
  • Inversion Of Control