"Cannot resolve scoped service from root provider" with custom EF Core SeriLog Sink

asp.net-core-webapi c# entity-framework-core serilog

Question

I'm trying to create a custom SeriLog sink that ties to EntityFrameworkCore. I found an existing one called Serilog.Sinks.EntityFrameworkCore but it used its own DbContext and I need to be able to use an existing DbContext.

So, I basically created my own version of the code that works with my DbContext. However, every time the Emit method gets called and it tries to load the DbContext, I get the following error:

Cannot resolve scoped service ... from root provider

I have seen other posts regarding this issue that involve scoped services and middleware. However, I don't believe that what I've got is middleware.

In a nutshell, here are the core pieces of my code (again, most of which is copied from the previously mentioned Git Repo).

startup.cs

public void ConfigureServices(IServiceCollection services)
{
   services.AddDbContext<EligibilityDbContext>(opts => opts.UseSqlServer(Configuration.GetConnectionString("EligibilityDbConnection")));
}

public void Configure(IApplicationBuilder app,
                      IHostingEnvironment env, 
                      SystemModelBuilder modelBuilder, 
                      ILoggerFactory loggerFactory)
{
   Log.Logger = new LoggerConfiguration()
       .WriteTo.EntityFrameworkSink(app.ApplicationServices.GetService<EligibilityDbContext>)
.CreateLogger();

loggerFactory.AddSeriLog();
}

EntityFrameworkSinkExtensions.cs

public static class EntityFrameworkSinkExtensions
{
    public static LoggerConfiguration EntityFrameworkSink(
        this LoggerSinkConfiguration loggerConfiguration,
        Func<EligibilityDbContext> dbContextProvider,
        IFormatProvider formatProvider = null) 
    {
        return loggerConfiguration.Sink(new EntityFrameworkSink(dbContextProvider, formatProvider));
    }
}

EntityFrameworkSink.cs

public class EntityFrameworkSink : ILogEventSink
{
    private readonly IFormatProvider _formatProvider;
    private readonly Func<EligibilityDbContext> _dbContextProvider;
    private readonly JsonFormatter _jsonFormatter;
    static readonly object _lock = new object();

    public EntityFrameworkSink(Func<EligibilityDbContext> dbContextProvider, IFormatProvider formatProvider)
    {
        _formatProvider = formatProvider;
        _dbContextProvider = dbContextProvider ?? throw new ArgumentNullException(nameof(dbContextProvider));
        _jsonFormatter = new JsonFormatter(formatProvider: formatProvider);
    }

    public void Emit(LogEvent logEvent)
    {
        lock (_lock)
        {
            if (logEvent == null)
            {
                return;
            }

            try
            {
                var record = ConvertLogEventToLogRecord(logEvent);

                //! This is the line causing the problems!
                DbContext context = _dbContextProvider.Invoke();

                if (context != null)
                {
                    context.Set<LogRecord>().Add(this.ConvertLogEventToLogRecord(logEvent));

                    context.SaveChanges();
                }
            }
            catch(Exception ex)
            {
                // ignored
            }
        }
    }

    private LogRecord ConvertLogEventToLogRecord(LogEvent logEvent)
    {
        if (logEvent == null)
            return null;

        string json = this.ConvertLogEventToJson(logEvent);

        JObject jObject = JObject.Parse(json);
        JToken properties = jObject["Properties"];

        return new LogRecord
        {
            Exception = logEvent.Exception?.ToString(),
            Level = logEvent.Level.ToString(),
            LogEvent = json,
            Message = logEvent.RenderMessage(this._formatProvider),
            MessageTemplate = logEvent.MessageTemplate?.ToString(),
            TimeStamp = logEvent.Timestamp.DateTime.ToUniversalTime(),
            EventId = (int?)properties["EventId"]?["Id"],
            SourceContext = (string)properties["SourceContext"],
            ActionId = (string)properties["ActionId"],
            ActionName = (string)properties["ActionName"],
            RequestId = (string)properties["RequestId"],
            RequestPath = (string)properties["RequestPath"]
        };
    }

    private string ConvertLogEventToJson(LogEvent logEvent)
    {
        if (logEvent == null)
        {
            return null;
        }

        StringBuilder sb = new StringBuilder();
        using (StringWriter writer = new StringWriter(sb))
        {
            this._jsonFormatter.Format(logEvent, writer);
        }

        return sb.ToString();
    }
}

The error occurs in EntityFrameworkSink.cs on the line DbContext context = _dbContextProvider.Invoke();

Any thoughts on why this is throwing an error and how to get this working?

Update

Based on Eric's comments, I updated my startup.cs code as follows:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, SystemModelBuilder modelBuilder, ILoggerFactory loggerFactory, IServiceProvider provider)
{
   Log.Logger = new LoggerConfiguration()
      .WriteTo.EntityFrameworkSink(provider.GetService<EligibilityDbContext>)
      .CreateLogger();
}

Now I get the error: Cannot access a disposed object. Object name: IServiceProvider

Caveat To Answer

So I marked Tao Zhou's answer as the answer. However, it was not what he said but the code he provided that actually provided the answer. I don't believe that EmitBatchAsync will actually resolve what my issue was -- however, I've run across a couple other comments, etc. elsewhere that indicate that it may help improve performance.

What actually resolved the problem was following his code sample. In startup, he is passing app.ApplicationServices. Then, in the actual Sink implementation, he created a scope for resolving an instance of the dbContext:

using(var context = service.CreateScope().ServiceProvider.GetRequiredService<EligibilityDbContext>())
{
}

This actually resolved all the errors I was getting and got this working the way I had expected. Thanks

1
2
10/1/2018 11:10:33 AM

Accepted Answer

For using Serilog with EF Core, you may need to implement PeriodicBatchingSink instead of ILogEventSink.

Follow steps below:

  1. Install package Serilog.Sinks.PeriodicBatching
  2. EntityFrameworkCoreSinkExtensions

    public static class EntityFrameworkCoreSinkExtensions
    {
    public static LoggerConfiguration EntityFrameworkCoreSink(
              this LoggerSinkConfiguration loggerConfiguration,
              IServiceProvider serviceProvider,
              IFormatProvider formatProvider = null)
    {
        return loggerConfiguration.Sink(new EntityFrameworkCoreSink(serviceProvider, formatProvider, 10 , TimeSpan.FromSeconds(10)));
    }
    }
    
  3. EntityFrameworkCoreSink

       public class EntityFrameworkCoreSink : PeriodicBatchingSink
       {
    private readonly IFormatProvider _formatProvider;
    private readonly IServiceProvider _serviceProvider;
    private readonly JsonFormatter _jsonFormatter;
    static readonly object _lock = new object();
    
    public EntityFrameworkCoreSink(IServiceProvider serviceProvider, IFormatProvider formatProvider, int batchSizeLimit, TimeSpan period):base(batchSizeLimit, period)
    {
        this._formatProvider = formatProvider;
        this._serviceProvider = serviceProvider;
        this._jsonFormatter = new JsonFormatter(formatProvider: formatProvider);
    }
    
    protected override async Task EmitBatchAsync(IEnumerable<LogEvent> events)
    {
        using (var context = _serviceProvider.CreateScope().ServiceProvider.GetRequiredService<ApplicationDbContext>())
        {
            if (context != null)
            {
                foreach (var logEvent in events)
                {
                    var log = this.ConvertLogEventToLogRecord(logEvent);
                    await context.AddAsync(log);
                }
                await context.SaveChangesAsync();
            }
        }
    }
    
    private LogRecord ConvertLogEventToLogRecord(LogEvent logEvent)
    {
        if (logEvent == null)
        {
            return null;
        }
    
        string json = this.ConvertLogEventToJson(logEvent);
    
        JObject jObject = JObject.Parse(json);
        JToken properties = jObject["Properties"];
    
        return new LogRecord
        {
            Exception = logEvent.Exception?.ToString(),
            Level = logEvent.Level.ToString(),
            LogEvent = json,
            Message = this._formatProvider == null ? null : logEvent.RenderMessage(this._formatProvider),
            MessageTemplate = logEvent.MessageTemplate?.ToString(),
            TimeStamp = logEvent.Timestamp.DateTime.ToUniversalTime(),
            EventId = (int?)properties["EventId"]?["Id"],
            SourceContext = (string)properties["SourceContext"],
            ActionId = (string)properties["ActionId"],
            ActionName = (string)properties["ActionName"],
            RequestId = (string)properties["RequestId"],
            RequestPath = (string)properties["RequestPath"]
        };
    }
    
    private string ConvertLogEventToJson(LogEvent logEvent)
    {
        if (logEvent == null)
        {
            return null;
        }
    
        StringBuilder sb = new StringBuilder();
        using (StringWriter writer = new StringWriter(sb))
        {
            this._jsonFormatter.Format(logEvent, writer);
        }
    
        return sb.ToString();
    }
    }
    
  4. Startup

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        Log.Logger = new LoggerConfiguration()
                            .WriteTo.EntityFrameworkCoreSink(app.ApplicationServices)
                            .CreateLogger();
        loggerFactory.AddSerilog();
    

    Source Code:StartupEFCore

0
9/27/2018 4:13:14 AM

Popular Answer

When you call app.ApplicationServices.GetService<EligibilityDbContext>, you're directly resolving a scoped service from the application container which isn't allowed. If you add EligibilityDbContext as a parameter to the Configure method, it will generate a scope and inject the context into your method.

public void Configure(IApplicationBuilder app, ..., EligibilityDbContext context)
{
  // ... use context
}


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow