简体   繁体   中英

How to access the database in Middleware using Entity Framework 6

I wrote some middleware to log the request path and query in the database. I have two seperate models. One for logging and one business model. After trying a few things I came up with this:

public class LogMiddleware
{
    private readonly RequestDelegate _next;
    private readonly DbConnectionInfo _dbConnectionInfo;

    public LogMiddleware(RequestDelegate next, DbConnectionInfo dbConnectionInfo)
    {
        _next = next;
        _dbConnectionInfo = dbConnectionInfo;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        httpContext.Response.OnStarting( async () =>
        {
            await WriteRequestToLog(httpContext);
        });
        await _next.Invoke(httpContext);
    }

    private async Task WriteRequestToLog(HttpContext httpContext)
    {
        using (var context = new MyLoggingModel(_dbConnectionInfo))
        {
            context.Log.Add(new Log
            {
                Path = request.Path,
                Query = request.QueryString.Value
            });
            await context.SaveChangesAsync();
        }
    }
}

public static class LogExtensions
{
    public static IApplicationBuilder UseLog(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<LogMiddleware>();
    }
}

The Model:

public class MyLoggingModel : DbContext
{
    public MyLoggingModel(DbConnectionInfo connection)
        : base(connection.ConnectionString)
    {
    }
    public virtual DbSet<Log> Log { get; set; }
}

As you can see nothing special. It works, but not quite the way I would have wanted it to. The problem lies probably in EF6, not being threadsafe.

I started with this in Startup:

public class Startup
{
    private IConfigurationRoot _configuration { get; }

    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: false, reloadOnChange: true)
            .AddEnvironmentVariables();
        _configuration = builder.Build();
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOptions();
        services.Configure<ApplicationSettings>(_configuration.GetSection("ApplicationSettings"));
        services.AddSingleton<ApplicationSettings>();

        services.AddSingleton(provider => new DbConnectionInfo { ConnectionString = provider.GetRequiredService<ApplicationSettings>().ConnectionString });
        services.AddTransient<MyLoggingModel>();
        services.AddScoped<MyModel>();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseLog();
        app.UseStaticFiles();
        app.UseMvc();
    }
}

MyLoggingModel needs to be transient in order to let it work for the middleware. But this method immediately causes problems:

System.NotSupportedException: A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.

I can assure you that I did add await everywhere. But that did not resolve this. If I remove the async part then I get this error:

System.InvalidOperationException: The changes to the database were committed successfully, but an error occurred while updating the object context. The ObjectContext might be in an inconsistent state. Inner exception message: Saving or accepting changes failed because more than one entity of type 'MyLoggingModel.Log' have the same primary key value. Ensure that explicitly set primary key values are unique. Ensure that database-generated primary keys are configured correctly in the database and in the Entity Framework model. Use the Entity Designer for Database First/Model First configuration. Use the 'HasDatabaseGeneratedOption" fluent API or DatabaseGeneratedAttribute' for Code First configuration.

That's why I came up with above code. I would have wanted to use dependency injection for the model. But I cannot make this to work. I also cannot find examples on accessing the database from middleware. So I get the feeling that I may be doing this in the wrong place.

My question: is there a way to make this work using dependency injection or am I not supposed to access the database in the middleware? And I wonder, would using EFCore make a difference?

-- update --

I tried moving the code to a seperate class and inject that:

public class RequestLog
{
    private readonly MyLoggingModel _context;

    public RequestLog(MyLoggingModel context)
    {
        _context = context;
    }

    public async Task WriteRequestToLog(HttpContext httpContext)
    {
        _context.EventRequest.Add(new EventRequest
        {
            Path = request.Path,
            Query = request.QueryString.Value
        });
        await _context.SaveChangesAsync();
    }
}

And in Startup:

services.AddTransient<RequestLog>();

And in the middelware:

public LogMiddleware(RequestDelegate next, RequestLog requestLog)

But this doesn't make a difference with the original approach, same errors. The only thing that seems to work (besides the non-DI solution) is:

private async Task WriteRequestToLog(HttpContext httpContext)
{
    var context = (MyLoggingModel)httpContext.RequestServices.GetService(typeof(MyLoggingModel));

But I do not understand why this would be different.

Consider abstracting the db context behind a service or create one for the db context itself and used by the middleware.

public interface IMyLoggingModel : IDisposable {
    DbSet<Log> Log { get; set; }
    Task<int> SaveChangesAsync();

    //...other needed members.
}

and have the implementation derived from the abstraction.

public class MyLoggingModel : DbContext, IMyLoggingModel {
    public MyLoggingModel(DbConnectionInfo connection)
        : base(connection.ConnectionString) {
    }

    public virtual DbSet<Log> Log { get; set; }

    //...
}

The service configurations appear to be done correctly. With my above suggestion it would need to update how the db context is registered.

services.AddTransient<IMyLoggingModel, MyLoggingModel>();

the middleware can either have the abstraction injected via constructor or directly injected into the Invoke method.

public class LogMiddleware {
    private readonly RequestDelegate _next;

    public LogMiddleware(RequestDelegate next) {
        _next = next;
    }

    public async Task Invoke(HttpContext context, IMyLoggingModel db) {
        await WriteRequestToLog(context.Request, db);
        await _next.Invoke(context);
    }

    private async Task WriteRequestToLog(HttpRequest request, IMyLoggingModel db) {
        using (db) {
            db.Log.Add(new Log {
                Path = request.Path,
                Query = request.QueryString.Value
            });
            await db.SaveChangesAsync();
        }
    }
}

If all else fails consider getting the context from the request's services, using it as a service locator.

public class LogMiddleware {
    private readonly RequestDelegate _next;

    public LogMiddleware(RequestDelegate next) {
        _next = next;
    }

    public async Task Invoke(HttpContext context) {
        await WriteRequestToLog(context);
        await _next.Invoke(context);
    }

    private async Task WriteRequestToLog(HttpContext context) {
        var request = context.Request;
        using (var db = context.RequestServices.GetService<IMyLoggingModel>()) {
            db.Log.Add(new Log {
                Path = request.Path,
                Query = request.QueryString.Value
            });
            await db.SaveChangesAsync();
        }
    }
}

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