1

I'm wondering how IServiceCollection.AddDbContext() adds my ApplicationDbContext. My guess is that it is not added as a singleton, and that a new instance is created with each request.

I'm implementing a custom ILoggerProvider, which requires an instance of ApplicationDbContext.

My question is: what happens if my ILoggerProvider is configured as a singleton, but it has a dependency on an ApplicationDbContext that is not a singleton?

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
Jonathan Wood
  • 65,341
  • 71
  • 269
  • 466
  • 1
    As an aside, having your logger relying on a `DbContext` is likely a bad idea anyway, you should be very careful there. It would be safer to use a proper logging library that has a SQL provider built in. – DavidG Oct 21 '21 at 15:55
  • 1
    To expand @DavidG's comment - you should really consider Serilog. But honestly, you should really consider whether using a relational database is a good idea at all. Look at Serilog sinks to get an idea: https://www.nuget.org/packages?q=serilog – Camilo Terevinto Oct 21 '21 at 15:59
  • In general logging should be separated as much as possible from the application itself, and you are trying to tie it with your database (which might be the thing which breaks and that event you might need to log). – Evk Oct 21 '21 at 16:01
  • @JonathanWood what's your actual use case? Logging to a database *is* useful if you want to easily track operations, if you have multiple active jobs or services, or if you want to collect and correlate events from multiple processes. It can also be used for distributed tracing up to a point, with multiple independent services writing sufficiently important messages to the same logging database. – Panagiotis Kanavos Oct 21 '21 at 16:17
  • @PanagiotisKanavos: I want the option of logging to a database for anything my application might decide is noteworthy. – Jonathan Wood Oct 21 '21 at 16:22
  • @CamiloTerevinto: What do you mean to consider whether I want to use a relational database? I have a big application that uses SQL Server. Why would I want to revisit that decision just because I want to do a little logging? – Jonathan Wood Oct 21 '21 at 16:23
  • @JonathanWood you can do that with the proper log sink, not by using a DbContext in the logger. Serilog provides a database sink and allows you to customize columns, batch sizes and periods, indexing. Logging to a database is more expensive than logging to a local file but allows easier querying up to a point. After all, a log entry contains a few tags and a lot of text. It's common to log to *Elastic* and use its text processing features to crunch and query the logs themselves. Iff you use .NET Core's logging properly your code won't have to know anything about the log providers – Panagiotis Kanavos Oct 21 '21 at 16:43
  • @JonathanWood on the other hand, if you have lots of services you should look into distributed tracing through the cross-platform OpenTelemetry protocol. [ASP.NET Core 5 and later support this](https://github.com/open-telemetry/opentelemetry-dotnet), again allowing you to configure where the events end up through configuration (Prometheus, Jaeger, Zipkin etc) – Panagiotis Kanavos Oct 21 '21 at 16:45
  • @JonathanWood finally, a logger provider only writes specific events to its sink, in this case the database. This means that using an ORM doesn't help at all. There aren't multiple objects to read or map, only specific columns to write. The only thing that's really needed is the connection string and ADO.NET to execute INSERT statements, either for individual entries or batches. You could use Dapper to reduce the boilerplate code and write the records as quickly as possible. An ORM's overhead would seriously harm the logger's performance – Panagiotis Kanavos Oct 21 '21 at 16:53
  • I meant what @PanagiotisKanavos commented, but in many less words :) Thanks Panagiotis for the helpful comments, as usual – Camilo Terevinto Oct 21 '21 at 17:33

1 Answers1

4

Yes, but using a DbContext from an ILogger isn't the best idea. Loggers and log sinks are separate entities.

Using scoped services from a singleton isn't unusual. This is the case with Background services, which are registered as singletons. Consuming scoped services is explained in the section Consuming a scoped service in a background task.

To use a scoped (or transient) service, the singleton needs access to the IServiceProvider. This is typically passed as a constructor dependency.

public class MyHostedService : BackgroundService
{
    private readonly ILogger<MyHostedService> _logger;

    public MyHostedService(IServiceProvider services, 
        ILogger<MyHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }
}

This class can be used to retrieve transient instances using IServiceProvider.GetService or GetRequiredService:

var service = Services.GetRequiredService<MyTransientService>();

To use a scoped service the singleton needs to explicitly create the scope and retrieve the services from that scope. When the scope is disposed, any scoped instances will be disposed as well

using (var scope = Services.CreateScope())
{
    var context = 
        scope.ServiceProvider
            .GetRequiredService<MyDbContext>();

    ...
    context.SaveChanges();
}

Loggers and databases

An ILogger doesn't need to depend on a DbContext. Logging libraries use separate interfaces to publish and store log entries. An ILogger instance is used to publish a log entry, not store it. Storing the log event is the job of a log sink or log provider - different log libraries use different names for this.

.NET Core offers few log sinks intentionally. There are very good logging libraries like Serilog, and the .NET Core team had no intention of duplicating their work. They do offer a few sink implementations for Microsoft-specific services like Azure Application Insights.

One could write a custom log sink that writes to a database but it's probably better to use an existing library that already offers this functionality and takes care of buffering, async operations etc. Writing to a database is always more expensive than writing to a local file, so database sinks need to buffer messages to reduce the impact to the application.

It's easy to integrate one of the existing libraries like Serilog using eg Serilog.AspNetCore.

public static int Main(string[] args)
{
    Log.Logger = new LoggerConfiguration()
        .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
        .Enrich.FromLogContext()
        .WriteTo.Console()
        .CreateLogger();
    ....
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseSerilog() // <-- Add this line
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });

That library in turn offers database sinks like Serilog.Sinks.SqlServer.

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .WriteTo.MSSqlServer(
        connectionString: "Server=localhost;Database=LogDb;Integrated Security=SSPI;",
        sinkOptions: new MSSqlServerSinkOptions { TableName = "LogEvents" })
    .CreateLogger();

The SQL Server sink will create the log table and start writing log entries to it. The UseSerilog() call will redirect all ILogger calls to Serilog and eventually the database table.

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Actually transient services do not need custom scope and can be resolved from root one, so it can be injected as is without any need for `IServiceProvider`. – Guru Stron Oct 21 '21 at 15:58
  • @GuruStron But EF is injected as Scoped by default. Although I've preferred to inject it as Transient, personally – Camilo Terevinto Oct 21 '21 at 16:00
  • @CamiloTerevinto I'm refering to particular statement in the answer (_"To use a scoped (or transient) service, the singleton needs access to the IServiceProvider"_) which is not true, in my opinion. – Guru Stron Oct 21 '21 at 16:01
  • @DavidG Am I missing [something](https://dotnetfiddle.net/wXhcH3)? – Guru Stron Oct 21 '21 at 16:08
  • @DavidG but singletone one will be also alive forever. Also it is more about "should not do", than "can't do". – Guru Stron Oct 21 '21 at 16:10
  • @DavidG it does not make the statement in aswer correct though. Also, I assume, that is exactly why `DbContext` is registered as registered as scoped by default. – Guru Stron Oct 21 '21 at 16:28
  • @GuruStron `which is not true, in my opinion` it's true, simply because `IServiceProvider` is the only way you can get a service from .NET Core's DI. `IServiceProvider` is the root of .NET Core's DI – Panagiotis Kanavos Oct 21 '21 at 16:49
  • @PanagiotisKanavos my point is that singleton itself does not need access to it to resolve transient and singleton dependencies. But agreed - my wording is not perferct =) – Guru Stron Oct 21 '21 at 16:52
  • @GuruStron how else would it get them, without actually calling their constructors or calling a hard-coded factory? With DI you don't know or care how the services were constructed, or how many dependencies they have – Panagiotis Kanavos Oct 21 '21 at 16:54
  • @PanagiotisKanavos using contructor injection as usual. As `MyHostedService` in your example does not need access to `IServiceProvider` to resolve `ILogger`. – Guru Stron Oct 21 '21 at 16:55
  • @PanagiotisKanavos: Thanks. I'm reading up on all this new information to build an understanding. Can I ask if you see any problems with me implementing my own `ILoggerProvider` and `ILogger` classes for logging to non-database targets? And are the concerns expresses above strictly related to database? – Jonathan Wood Oct 21 '21 at 16:59
  • @GuruStron I changed the example. Besides who said the class needs the same instance every time? In a queued background service, the worker loop would typically require new instances of transient services for every message, probably disposing them after each loop. – Panagiotis Kanavos Oct 21 '21 at 17:00
  • @JonathanWood IO is always expensive, which is why most loggers implement some kind of buffering and batching. For files, most file providers offer rolling logs and cleanup as well. You should probably check how other log providers are already implemented. – Panagiotis Kanavos Oct 21 '21 at 17:02
  • @PanagiotisKanavos: Thanks, but I'm not sure you answered my question. The problem with buffering file logging is that the application could crash with important data not yet flushed to the file. So are you saying you don't think I should implement my own non-database loggers either? – Jonathan Wood Oct 21 '21 at 17:07
  • @JonathanWood that's a tradeoff. If you write everything immediately performance will suffer. If you batch for too long, you risk losing entries. Logging libraries solve this in various ways, but you'll have to check their code to see how this works. It's not trivial, I don't know how all of them work. I do know that this can be configured even at the file level, so errors can be sent to one file with no batching and normal entries sent to another file with normal batching. – Panagiotis Kanavos Oct 21 '21 at 17:10