Monitoring a .NET app using Castle Core

We saw on a previous post Monitoring a .NET app using Scrutor and the decorator pattern. This is a great but it comes with a big limitation: this is not scalable. Indeed, as the decorators 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 the elegant methods are really good. This is where comes Castle, his dynamic proxies and the interceptor pattern.

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

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 API .NET 6 application (like in the post Monitoring a .NET app using Scrutor) and we do the same, which is 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 a diagnostic monitoring to log how much time the method took to execute.

That 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 it in the clean way we will create an extension method which can handle as many as interceptor class registering 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 execute our web application now, we can see that our WeatherService is intercepted by our DurationInterceptor so we can find traces about his 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