简体   繁体   中英

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

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 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

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
}

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM