如何使用 Entity Framework 6 访问中间件中的数据库

How to access the database in Middleware using Entity Framework 6

我写了一些中间件来记录数据库中的请求路径和查询。我有两个独立的模型。一种用于日志记录,一种用于商业模式。在尝试了一些事情之后,我想到了这个:

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>();
    }
}

模特:

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

如您所见,没什么特别的。它有效,但不是我想要的那样。问题可能出在 EF6,不是线程安全的。

我在 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 需要是瞬态的才能让它为中间件工作。但是这个方法马上就出问题了:

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.

我可以向你保证,我确实在所有地方都添加了 await。但这并没有解决这个问题。如果我删除异步部分,则会收到此错误:

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.

这就是我想出上面代码的原因。我本来想为模型使用依赖注入。但我无法让它发挥作用。我也找不到从中间件访问数据库的示例。所以我觉得我可能在错误的地方这样做。

我的问题:有没有办法使用依赖注入来完成这项工作,或者我不应该访问中间件中的数据库?我想知道,使用 EFCore 会有什么不同吗?

-- 更新--

我尝试将代码移动到单独的 class 并注入:

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();
    }
}

并且在启动中:

services.AddTransient<RequestLog>();

在中间件中:

public LogMiddleware(RequestDelegate next, RequestLog requestLog)

但这与原来的做法没有区别,同样的错误。唯一似乎有效的方法(除了非 DI 解决方案)是:

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

但我不明白为什么会有所不同。

考虑抽象化服务背后的数据库上下文,或者为数据库上下文本身创建一个并由中间件使用。

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

    //...other needed members.
}

并从抽象派生实现。

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

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

    //...
}

服务配置似乎已正确完成。根据我的上述建议,它需要更新数据库上下文的注册方式。

services.AddTransient<IMyLoggingModel, MyLoggingModel>();

中间件可以通过构造函数注入抽象,也可以直接注入 Invoke 方法。

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();
        }
    }
}

如果其他所有方法都失败,请考虑从请求的服务中获取上下文,将其用作服务定位器。

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();
        }
    }
}