正确处理核心 WebAPI ASP.NET 中的 DbContexts

本文关键字:中的 DbContexts NET ASP 核心 WebAPI 正确处理 | 更新日期: 2023-09-27 18:18:22

这里有一个小困惑。我不确定我是否在整个WebApi中正确处理了我的DbContext。我确实有一些控制器在我的数据库上执行一些操作(使用 EF 插入/更新(,执行这些操作后,我确实触发了一个事件。在我的 EventArgs 中(我有一个继承自 EventArgs 的自定义类(,我传递我的DbContext并在事件处理程序中使用它来记录这些操作(基本上我只是记录经过身份验证的用户 API 请求(。

在事件处理程序中,当我尝试提交更改(await SaveChangesAsync(时,我收到一个错误:"使用已释放的对象...等等"基本上注意到我第一次在async void中使用await(即发即弃(时,我通知调用方释放 Dbcontext 对象。

不使用async有效,我管理的唯一解决方法是通过获取传递的 DbContext 的 EventArgs 的 SQLConnectionString 来创建另一个 DbContext 实例。

在发布之前,我确实根据我的问题做了一个小研究使用 Web api/MVC 中的异步控制器释放实体框架

这就是我将参数传递给OnRequestCompletedEvent的方式

OnRequestCompleted(dbContext: dbContext,requestJson: JsonConvert.SerializeObject);

这是OnRequestCompleted()声明

 protected virtual void OnRequestCompleted(int typeOfQuery,PartnerFiscalNumberContext dbContext,string requestJson,string appId)
        {
       RequestCompleted?.Invoke(this,new MiningResultEventArgs()
          {
            TypeOfQuery = typeOfQuery,
            DbContext   = dbContext,
            RequestJson = requestJson,
            AppId = appId
          });
        }

这就是我处理和使用dbContext的方式

var appId = miningResultEventArgs.AppId;
var requestJson = miningResultEventArgs.RequestJson;
var typeOfQuery = miningResultEventArgs.TypeOfQuery;
var requestType =  miningResultEventArgs.DbContext.RequestType.FirstAsync(x => x.Id == typeOfQuery).Result;
var apiUserRequester =  miningResultEventArgs.DbContext.ApiUsers.FirstAsync(x => x.AppId == appId).Result;
var apiRequest = new ApiUserRequest()
{
    ApiUser = apiUserRequester,
    RequestJson = requestJson,
    RequestType = requestType
};
miningResultEventArgs.DbContext.ApiUserRequests.Add(apiRequest);
await miningResultEventArgs.DbContext.SaveChangesAsync();

通过使用SaveChanges而不是SaveChangesAsync一切正常。我唯一的想法是通过传递前一个 DbContext 的 SQL 连接字符串来创建另一个 dbContext。

var dbOptions = new DbContextOptionsBuilder<PartnerFiscalNumberContext>();
dbOptions.UseSqlServer(miningResultEventArgs.DbContext.Database.GetDbConnection().ConnectionString);
    using (var dbContext = new PartnerFiscalNumberContext(dbOptions.Options))
    {
        var appId = miningResultEventArgs.AppId;
        var requestJson = miningResultEventArgs.RequestJson;
        var typeOfQuery = miningResultEventArgs.TypeOfQuery;

        var requestType = await dbContext.RequestType.FirstAsync(x => x.Id == typeOfQuery);
        var apiUserRequester = await dbContext.ApiUsers.FirstAsync(x => x.AppId == appId);
        var apiRequest = new ApiUserRequest()
        {
            ApiUser = apiUserRequester,
            RequestJson = requestJson,
            RequestType = requestType
        };
        dbContext.ApiUserRequests.Add(apiRequest);
        await dbContext.SaveChangesAsync();
    }

后面的代码摘录只是一个小测试来检查我的假设,基本上我应该传递SQL连接字符串而不是DbContext对象。

我不确定(就最佳实践而言(我是否应该传递连接字符串并创建一个新的 dbContext 对象(并使用 using 子句处理它(,或者我是否应该对此问题使用/有另一种心态。

据我所知,使用 DbContext 应该用于一组有限的操作,而不是用于多种目的。

编辑 01

我将在下面更详细地详细介绍我一直在做的事情。

我想我知道为什么会发生此错误。

我有 2 个控制器一个接收 JSON,在反序列化后,我将一个 JSON 返回给调用方,另一个控制器获取一个 JSON,该 JSON 封装了我以异步方式迭代的对象列表,返回Ok()状态。

控制器声明为 async Task<IActionResult>,并且都具有 2 个类似方法的async执行。

返回 JSON

的第一个执行此方法
await ProcessFiscalNo(requestFiscalView.FiscalNo, dbContext);

第二个(触发此错误的那个(

foreach (string t in requestFiscalBulkView.FiscalNoList)
       await ProcessFiscalNo(t, dbContext);

这两种方法(前面定义的方法(都会启动事件OnOperationComplete()在该方法中,我从帖子的开头执行代码。在ProcessFiscalNo方法中,我不使用任何使用上下文,也不处理dbContext变量。在这种方法中,我只提交 2 个主要操作,要么更新现有的 sql 行,要么插入它。对于编辑上下文,我选择行并使用修改后的标签标记行

dbContext.Entry(partnerFiscalNumber).State = EntityState.Modified;

或通过插入行

dbContext.FiscalNumbers.Add(partnerFiscalNumber);

最后我执行了一个await dbContext.SaveChangesAsync();

await dbContext.SaveChangedAsync()期间,该错误总是在事件处理程序(详细@线程的开头(中触发这很奇怪,因为在此之前有 2 行我确实在使用 EF 等待我的数据库上的读取。

 var requestType = await dbContext.RequestType.FirstAsync(x => x.Id == typeOfQuery);
 var apiUserRequester = await dbContext.ApiUsers.FirstAsync(x => x.AppId == appId);
 dbContext.ApiUserRequests.Add(new ApiUserRequest() { ApiUser = apiUserRequester, RequestJson = requestJson, RequestType = requestType });
  //this throws the error
 await dbContext.SaveChangesAsync();

出于某种原因,事件处理程序中的调用 await 通知调用方释放DbContext对象。此外,通过重新创建DbContext而不是重复使用旧,我看到了访问的巨大改进。不知何故,当我使用第一个控制器并返回信息时,DbContext对象似乎被 CLR 标记为处置,但由于某种未知原因它仍然起作用。

编辑 02很抱歉下面的大量内容,但我已经放置了我使用 dbContext 的所有区域。

这就是我将 dbContext 传播到所有请求它的控制器的方式。

 public void ConfigureServices(IServiceCollection services)
        {
         // Add framework services.
        services.AddMemoryCache();
        // Add framework services.
        services.AddOptions();
        var connection = @"Server=.;Database=CrawlerSbDb;Trusted_Connection=True;";
        services.AddDbContext<PartnerFiscalNumberContext>(options => options.UseSqlServer(connection));
        services.AddMvc();
        services.AddAuthorization(options =>
        {
            options.AddPolicy("PowerUser",
                              policy => policy.Requirements.Add(new UserRequirement(isPowerUser: true)));
        });
        services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        services.AddSingleton<IAuthorizationHandler, UserTypeHandler>();
    }

在"配置"中,我正在为我的自定义中间件使用 dbContext

 public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();
            var context = app.ApplicationServices.GetService<PartnerFiscalNumberContext>();
            app.UseHmacAuthentication(new HmacOptions(),context);
            app.UseMvc();
        }

在自定义中间件中,我只将其用于查询。

public HmacHandler(IHttpContextAccessor httpContextAccessor, IMemoryCache memoryCache, PartnerFiscalNumberContext partnerFiscalNumberContext)
        {
            _httpContextAccessor = httpContextAccessor;
            _memoryCache = memoryCache;
            _partnerFiscalNumberContext = partnerFiscalNumberContext;
            AllowedApps.AddRange(
                    _partnerFiscalNumberContext.ApiUsers
                        .Where(x => x.Blocked == false)
                        .Where(x => !AllowedApps.ContainsKey(x.AppId))
                        .Select(x => new KeyValuePair<string, string>(x.AppId, x.ApiHash)));
        }

在我的控制器的 CTOR 中,我正在传递数据库上下文

public FiscalNumberController(PartnerFiscalNumberContext partnerContext)
        {
            _partnerContext = partnerContext;
        }

这是我的帖子

        [HttpPost]
        [Produces("application/json", Type = typeof(PartnerFiscalNumber))]
        [Consumes("application/json")]
        public async Task<IActionResult> Post([FromBody]RequestFiscalView value)
        {
            if (!ModelState.IsValid)
                return BadRequest(ModelState);
            var partnerFiscalNo = await _fiscalNoProcessor.ProcessFiscalNoSingle(value, _partnerContext);
        }

ProcessFiscalNoSingle方法中,我有以下用法,如果该合作伙伴存在,那么我将抓住他,如果没有,则创建并返回他。

internal async Task<PartnerFiscalNumber> ProcessFiscalNoSingle(RequestFiscalView requestFiscalView, PartnerFiscalNumberContext dbContext)
        {
            var queriedFiscalNumber =  await dbContext.FiscalNumbers.FirstOrDefaultAsync(x => x.FiscalNo == requestFiscalView.FiscalNo && requestFiscalView.ForceRefresh == false) ??
                                       await ProcessFiscalNo(requestFiscalView.FiscalNo, dbContext, TypeOfQuery.Single);
            OnRequestCompleted(typeOfQuery: (int)TypeOfQuery.Single, dbContextConnString: dbContext.Database.GetDbConnection().ConnectionString, requestJson: JsonConvert.SerializeObject(requestFiscalView), appId: requestFiscalView.RequesterAppId);
            return queriedFiscalNumber;
        }

在代码的更下方,我使用 dbContext 的ProcessFiscalNo方法

 var existingItem =
        dbContext.FiscalNumbers.FirstOrDefault(x => x.FiscalNo == partnerFiscalNumber.FiscalNo);
    if (existingItem != null)
    {
        var existingGuid = existingItem.Id;
        partnerFiscalNumber = existingItem;
        partnerFiscalNumber.Id = existingGuid;
        partnerFiscalNumber.ChangeDate = DateTime.Now;
        dbContext.Entry(partnerFiscalNumber).State = EntityState.Modified;
    }
    else
        dbContext.FiscalNumbers.Add(partnerFiscalNumber);
    //this gets always executed at the end of this method
    await dbContext.SaveChangesAsync();

此外,我还有一个名为OnRequestCompleted((的事件,我在其中传递了实际的dbContext(如果我更新/创建它,则以SaveChangesAsync((结束(

我发起事件的方式参数。

 RequestCompleted?.Invoke(this, new MiningResultEventArgs()
            {
                TypeOfQuery = typeOfQuery,
                DbContextConnStr = dbContextConnString,
                RequestJson = requestJson,
                AppId = appId
            });

这是通告程序类(发生错误的位置(

internal class RequestNotifier : ISbMineCompletionNotify
    {
        public async void UploadRequestStatus(object source, MiningResultEventArgs miningResultArgs)
        {
            await RequestUploader(miningResultArgs);
        }
        /// <summary>
        /// API Request Results to DB
        /// </summary>
        /// <param name="miningResultEventArgs">EventArgs type of a class that contains requester info (check MiningResultEventArgs class)</param>
        /// <returns></returns>
        private async Task RequestUploader(MiningResultEventArgs miningResultEventArgs)
        {
            //ToDo - fix the following bug : Not being able to re-use the initial DbContext (that's being used in the pipeline middleware and controller area), 
            //ToDo - basically I am forced by the bug to re-create the DbContext object
            var dbOptions = new DbContextOptionsBuilder<PartnerFiscalNumberContext>();
            dbOptions.UseSqlServer(miningResultEventArgs.DbContextConnStr);
            using (var dbContext = new PartnerFiscalNumberContext(dbOptions.Options))
            {
                var appId = miningResultEventArgs.AppId;
                var requestJson = miningResultEventArgs.RequestJson;
                var typeOfQuery = miningResultEventArgs.TypeOfQuery;
                var requestType = await dbContext.RequestType.FirstAsync(x => x.Id == typeOfQuery);
                var apiUserRequester = await dbContext.ApiUsers.FirstAsync(x => x.AppId == appId);
                var apiRequest = new ApiUserRequest()
                {
                    ApiUser = apiUserRequester,
                    RequestJson = requestJson,
                    RequestType = requestType
                };
                dbContext.ApiUserRequests.Add(apiRequest);
                await dbContext.SaveChangesAsync();
            }
        }
    }

不知何故,当 dbContext 到达事件处理程序时,CLR 会收到释放 dbContext 对象的通知(因为我正在使用 await?如果没有重新创建对象,当我想使用它时,我遇到了巨大的滞后。

在写这篇文章时,我有一个想法,我确实将我的解决方案升级到 1.1.0,我将尝试看看它的行为是否相似。

正确处理核心 WebAPI ASP.NET 中的 DbContexts

关于为什么你得到错误

正如 @set-fu 的评论中所指出的,DbContext不是线程安全的

除此之外,由于没有显式的 DbContext 生存期管理,因此当垃圾回收器认为合适时,您的 DbContext 将被释放。

从您的上下文以及您提到的请求范围的 DbContext 来看我想你在控制器的构造函数中DI你的DbContext。由于您的 DbContext 是请求范围的,因此一旦您的请求结束,它就会被释放,

但是,由于您已经触发并忘记了 OnRequestCompleted 事件,因此无法保证您的 DbContext 不会被释放。

从那以后,我们的一种方法成功而另一种方法失败的事实我认为是先知">运气"。一种方法可能比另一种方法更快,并且在垃圾回收器释放 DbContext 之前完成。

您可以做的是将事件的返回类型从

async void

async Task<T>

这样,您可以在控制器中等待 RequestDone 任务完成,这将保证在 RequestDone任务完成之前不会释放控制器/DbContext。

关于正确处理数据库上下文

微软有两个相互矛盾的建议,许多人以完全不同的方式使用DbContexts。

  1. 一个建议是"尽快处置 DbContexts">因为拥有一个活跃的 DbContext 会占用像 db 这样的宝贵资源。连接等....
  2. 另一个声明每个请求一个 DbContext 是高度的推荐

这些是相互矛盾的,因为如果您的请求正在做很多与 Db 内容无关的事情,那么您的 DbContext 就会无缘无故地保留。因此,当您的请求只是等待随机内容完成时,保持 DbContext 处于活动状态是浪费......

许多遵循规则 1 的人将他们的 DbContext 放在他们的"存储库模式">中,并为每个数据库查询创建一个新实例

        public User GetUser(int id)
        {
          User usr = null;
          using (Context db = new Context())
          {
              usr = db.Users.Find(id);
          }
          return usr;
         }

他们只是获取数据并尽快处理上下文。许多人认为这是一种可以接受的做法。虽然这样做的好处是在最短时间内占用数据库资源,但它显然牺牲了 EF 必须提供的所有工作单元"缓存">糖果。

因此,Microsoft关于每个请求使用 1 Db 上下文的建议显然是基于您的 UnitOfWork 的范围在 1 个请求内的事实。

在许多情况下,我相信你的情况也不是真的。我认为日志记录是一个单独的工作单元,因此为您的请求后日志记录提供新的 DbContext 是完全可以接受的(这也是我也使用的做法(。

我的项目中的一个例子,我在 3 个工作单元的请求中有 3 个 DbContext。

  1. 做工作
  2. 写入日志
  3. 向管理员发送电子邮件。