代码之家  ›  专栏  ›  技术社区  ›  user4864425

如何使用实体框架6访问中间件中的数据库

  •  4
  • user4864425  · 技术社区  · 7 年前

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

    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中,它不是线程安全的。

    我在启动时就这样开始了:

    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 需要是暂时的,以便让它为中间件工作。但这种方法会立即引发问题:

    系统NotSupportedException:对此启动了第二个操作 上一个异步操作完成之前的上下文。使用 “等待”以确保任何异步操作都已完成 在此上下文上调用其他方法之前。任何实例成员 不保证线程安全。

    我可以向你保证,我确实补充了 await 处处但这并没有解决这个问题。如果删除异步部分,则会出现以下错误:

    系统InvalidOperationException:对数据库的更改为 已成功提交,但更新时出错 对象上下文。ObjectContext可能处于不一致的状态。 内部异常消息:保存或接受更改失败,因为 “MyLoggingModel”类型的多个实体。“日志”具有相同的 主键值。确保显式设置的主键值为 唯一的确保配置了数据库生成的主键 在数据库和实体框架模型中正确。使用 数据库优先/模型优先配置的实体设计器。使用 'HasDatabaseGeneratedOption“fluent API或DatabaseGeneratedAttribute' 对于代码优先配置。

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

    我的问题是:有没有一种方法可以使用依赖注入来实现这一点,或者我不应该访问中间件中的数据库?我想知道,使用EFCore会有什么不同吗?

    --更新--

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

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

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

    1 回复  |  直到 7 年前
        1
  •  3
  •   Nkosi    7 年前

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

    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; }
    
        //...
    }
    

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

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