Kjell Rilbe
Kjell Rilbe

Reputation: 1509

How to modify already added singleton service in .NET host builder?

I'm using the new HostApplicationBuilder in .NET. I want to use IOptionsMonitor to monitor a section of appsettings.json for option changes, and apply those changes to Serilog LoggingLevelSwitch.MinimumLevel for such an object that's created during host build (using builder.Services.AddSerilog).

Something like this:

builder.Services.Configure<LoggingOptions>(
    builder.Configuration.GetSection(key: "Logging"));

LoggingLevelSwitch theSwitch = new LoggingLevelSwitch(); // Later added to a Serilog sink.

builder.Services.AddSingleton<Something>((serviceProvider) =>
{
    var monitor = serviceProvider.GetRequiredService<IOptionsMonitor<LoggingOptions>>();
    monitor.OnChange((options) => { 
        theSwitch.MinimumLevel = options.MinimumLevel;
    });
}

But what do I return from the AddSingleton call? I don't really want to add a service, but just add the OnChange handler to the singleton for IOptionsMonitor<LoggingOptions>.

Without a call such as builder.Services.AddXXX, how can I get an IServiceProvider instance that I can use to obtain a reference to the IOptionsMonitor<LoggingOptions> singleton?

This is supposed to be a helper in a utility package that we can use in multiple different apps to simplify and unify app code and reduce boilerplate code in apps, to allow apps based on .NET hosting to have logging set up and allow the logging level to be modified at runtime.

It would be sufficient to have the IOptionsMonitor to be hooked up to the LoggingLevelSwitch when the app worker starts and removed when the app worker terminates. But I would like to avoid a requirement on the app author to remember to make extra calls in the worker's start/stop code. So, preferably be able to set this up in the utility package's code that sets up the logging in general.

Upvotes: 0

Views: 367

Answers (2)

Kjell Rilbe
Kjell Rilbe

Reputation: 1509

OK, so the solution was kind-of staring me in the face right from the start.

// Apparently this is the new way of binding options, but same semantics as in my question.
builder.Services.AddOptions<LoggingOptions>().BindConfiguration("Logging");

builder.Services.AddSerilog((services, loggerConfiguration) =>
{
    // Create the switch, which will later be applied to a logging sink.
    var levelSwitch = new LoggingLevelSwitch();

    // Here inside AddSerilog we do have an IServiceProvider,
    // so we can request the IOptionsMonitor<LoggingOptions> singleton.
    // I assume that this is the point where it gets instantiated by the DI container.
    var monitor = services.GetRequiredService<IOptionsMonitor<LoggingOptions>>();

    // Here we hook the monitor up to the LoggingLevelSwitch.
    monitor.OnChange(options => levelSwitch.MinimumLevel = options.MinimumLevel);

    // Finally, we can add the sink using the switch,
    // which will be modified by the monitor at runtime.
    loggerConfiguration
        .WriteTo.ApplicationInsights(
            services.GetRequiredService<TelemetryConfiguration>(),
            TelemetryConverter.Traces,
            levelSwitch: levelSwitch);
});

I've tried it in a small console app and it works fine. When I change the logging level in appsettings.json, the sink starts receiving log events according to the new setting, without any code within the actual worker service.

Upvotes: 3

J.Memisevic
J.Memisevic

Reputation: 1454

I correct myself. I was wrong, you can achieve this with singleton.

I was able to make it work. I just returned the LoggingLevelSwitch as singleton I was able to se changes in MinimumLevel when appsetting value is changed.

Upvotes: 0

Related Questions