Blog
Monitoring a .NET app using Scrutor
Scrutor is a NuGet package which extend the ASP.NET built-in dependency injection container with assembly scanning and decoration extensions for Microsoft.Extensions.DependencyInjection.
The library adds two extension methods to IServiceCollection:
- Scan, which is the entry point to set up your assembly scanning.
- Decorate, which is used to decorate already registered services.
Assembly scanning is not what we are interested in here so I won't detail it here, just know that the Scan method allow you to browse some assemblies and to register some implementations of services in the lifetime you want. The Scrutor package has limited functionnality but it's very powerful, for example the assembly scanning is that great design that you can specify options like the registration strategy you want to apply which is how you want handling dupllicates services or implementation, anyway if you don't know it you should take a moment to discover it (there is a great and very detailled blog post from Andrew Lock about it: Using Scrutor to automatically register your services with the ASP.NET Core DI container.
The method we want to use here is the Decorate method which allows to decorate already registered services. You register your decorated service, than you can decorate it with a decorator service, and you can still add some decorator's decorators if you need to.
What we want to do here is basic need : we want to know how much time some methods takes to complete.
Assume we have the default .NET 6 Api application.
The first thing we do is to create a service interface and his implementation to get the get forecast logic out of the controller.
We have this :
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();
}
}
We clean the controller to resolve the service and call the method he exposes:
[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();
}
}
And in the program class we register our service and his implementation, like this:
builder.Services.AddSingleton<IWeatherService, WeatherService>();
How can we now add some monitoring to our app? We could add some measures in our service but it has nothing to do there, and if we do this we should add a mechanism to allow us to disable it if we want so it becomes complex and invasive. This is where Scrutor comes: this package allows us to add a decorator to our WeatherForecastService which will take the responsability of the measuring stuff and only this, so our service won't have to change, it won't even know that he's measured!
How to do this? It's pretty simple.
We add another service, let's call it MeasuredWeatherService which implements the same interface that our WeatherService and takes the IWeatherService in constructor's parameter. Then you implement the interface by assuming the monitoring logic and calling the decorated service for the core logic:
public class MeasuredWeatherService : IWeatherService
{
private readonly ILogger<MeasuredWeatherService> _logger;
private readonly IWeatherService _weatherService;
public MeasuredWeatherService(ILogger<MeasuredWeatherService> logger, IWeatherService weatherService)
{
_logger = logger;
_weatherService = weatherService;
}
public WeatherForecast[] GetForecast()
{
var sw = Stopwatch.StartNew();
try
{
return _weatherService.GetForecast();
}
finally
{
sw.Stop();
_logger.LogInformation("{MethodName} took {Duration}ms to complete", nameof(GetForecast), sw.ElapsedMilliseconds);
}
}
}
We don't even have to change the controller, the only thing we have to do is to register ou decorator:
builder.Services.Decorate<IWeatherService, MeasuredWeatherService>();
At the runtime the Dependency Injection container will resolve the decorator service which references the decorated so at the execution we will find our measures in the traces:
You can check the source code on my GitHub: @AdrienTorris