c# – 在ASP.NET Core WebApi中正确处理DbContexts
这里有一点混乱.
我不确定我是否正确地在整个WebApi中处理我的DbContext. 我确实有一些控制器在我的数据库上执行某些操作(使用EF插入/更新),在执行这些操作后,我会触发一个事件. 在我的EventArgs中(我有一个继承自EventArgs的自定义类)我传递了我的DbContext,我在事件处理程序中使用它来记录这些操作(基本上我只记录经过身份验证的用户API请求). 在事件处理程序中,当我尝试提交我的更改(等待SaveChangesAsync)时,我收到一个错误:“使用已处置的对象……等等”基本上注意到我第一次在异步void中使用await(fire and forget )我通知调用者处理Dbcontext对象. 不使用异步工作,并且我已经解决的唯一解决方法是通过获取EventArgs的SQLConnectionString传递DbContext来创建另一个DbContext实例. 在发布之前,我根据我的问题进行了一项小型研究 这是我将参数传递给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,RequestType = requestType }; miningResultEventArgs.DbContext.ApiUserRequests.Add(apiRequest); await miningResultEventArgs.DbContext.SaveChangesAsync(); 通过使用SaveChanges而不是SaveChangesAsync一切正常. 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,RequestType = requestType }; dbContext.ApiUserRequests.Add(apiRequest); await dbContext.SaveChangesAsync(); } 后面的代码摘录只是一个检查我的假设的小测试,基本上我应该传递SQL连接字符串而不是DbContext对象. 我不确定(在最佳实践方面)我是否应该传递一个连接字符串并创建一个新的dbContext对象(并使用using子句来处置它),或者我是否应该使用/拥有另一个心态来解决这个问题. 据我所知,使用DbContext应该针对一组有限的操作而不是出于多种目的. 编辑01 我将详细介绍下面我一直在做的事情. 我想我知道为什么会发生这种错误. 我有2个控制器 控制器被声明为异步任务< IActionResult>并且两者都具有2个类似方法的异步执行. 第一个返回JSON的方法执行此方法 await ProcessFiscalNo(requestFiscalView.FiscalNo,dbContext); 第二个(触发此错误的那个) foreach (string t in requestFiscalBulkView.FiscalNoList) await ProcessFiscalNo(t,dbContext); 两种方法(先前定义的方法)都会启动一个事件OnOperationComplete() dbContext.Entry(partnerFiscalNumber).State = EntityState.Modified; 或插入行 dbContext.FiscalNumbers.Add(partnerFiscalNumber); 最后我执行await dbContext.SaveChangesAsync(); 在await dbContext.SaveChangedAsync()期间,错误总是在EventHandler(一个详细的@开头的线程)中被触发 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,RequestType = requestType }); //this throws the error await dbContext.SaveChangesAsync(); 由于某种原因,在事件处理程序中调用await通知调用者处置DbContext对象. 编辑02 这就是我将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作为我的自定义MiddleWare 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(); } 在自定义MiddleWare中,我只将其用于查询. 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中,我传递了dbContext 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()结束) 我发起事件的方式args. RequestCompleted?.Invoke(this,new MiningResultEventArgs() { TypeOfQuery = typeOfQuery,DbContextConnStr = dbContextConnString,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,RequestType = requestType }; dbContext.ApiUserRequests.Add(apiRequest); await dbContext.SaveChangesAsync(); } } } 不知何故,当dbContext到达事件处理程序时,CLR会收到处理dbContext对象的通知(因为我正在使用等待?) 在写这篇文章时我有一个想法,我确实将我的解决方案升级到了1.1.0,我将尝试查看它是否表现相似. 解决方法
关于你为何得到错误
正如@ set-fu DbContext的评论所指出的那样,它不是线程安全的. 除此之外,由于DbContext没有明确的生命周期管理,因此当垃圾收集器认为合适时,DbContext将被处理掉. 从您的上下文来看,以及您对Request scoped DbContext的提及 但由于您已经解雇并忘记了OnRequestCompleted事件,因此无法保证不会丢弃您的DbContext. 从那以后,我们的一个方法成功而另一个失败的事实我认为是先见“运气”. 您可以做的是更改事件的返回类型 async void 至 async Task<T> 这样,您可以等待控制器中的RequestCompleted Task完成,这将保证您的Controller / DbContext在RequestCompleted任务完成之前不会被Disposed. 关于正确处理DbContexts 微软有两个相互矛盾的建议,许多人以完全不同的方式使用DbContexts. >一个建议是“尽快处理DbContexts” 那些相互矛盾,因为如果你的请求与Db的东西有很多不相关的东西,那么你的DbContext就会无缘无故地保留下来. 那么多遵循规则1的人在他们的“存储库模式”中都有他们的DbContexts并为每个数据库查询创建一个新的实例 public User GetUser(int id) { User usr = null; using (Context db = new Context()) { usr = db.Users.Find(id); } return usr; } 他们只是尽快获取数据并处理上下文. 所以微软建议每个请求使用1 Db Context,这显然是基于你的UnitOfWork在1个请求范围内的事实. 但在许多情况下,我相信你的情况也不是这样. 我的项目中的一个示例我在3个工作单元的请求中有3个DbContexts. >做好工作>写日志>向管理员发送电子邮件. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |