简体   繁体   English

在ASP.NET Core WebApi中正确处理DbContexts

[英]Properly handling DbContexts in ASP.NET Core WebApi

Got a small confusion here. 这里有一点混乱。 I'm not sure if I am handling my DbContext throughout the WebApi properly. 我不确定我是否正确地在整个WebApi中处理我的DbContext。 I do have some controllers that do some operations on my DB (Inserts/Updates with EF) and after doing these actions I do trigger an event. 我确实有一些控制器在我的数据库上执行某些操作(使用EF插入/更新),在执行这些操作后,我确实触发了一个事件。 In my EventArgs (I have a custom class which inherits from EventArgs ) I pass my DbContext and I use it in the event handler to log these operations (basically I just log authenticated user API requests). 在我的EventArgs中(我有一个继承自EventArgs的自定义类)我传递了我的DbContext ,我在事件处理程序中使用它来记录这些操作(基本上我只记录经过身份验证的用户API请求)。

In the event handler when I am trying to commit my changes ( await SaveChangesAsync ) I get an error : "Using a disposed object...etc" basically noticing me that at the first time I use await in my async void (fire and forget) I notify the caller to dispose the Dbcontext object. 在事件处理程序中,当我尝试提交我的更改( await SaveChangesAsync )时,我收到一个错误:“使用已处置的对象...等”基本上注意到我第一次在async void使用await (火灾和遗忘) )我通知调用者处理Dbcontext对象。

Not using async works and the only workaround that I've mangaged to put out is by creating another instance of DbContext by getting the SQLConnectionString of the EventArgs passed DbContext. 不使用async工作,并且我已经解决的唯一解决方法是通过获取EventArgs的SQLConnectionString传递DbContext来创建另一个DbContext实例。

Before posting I did made a small research based on my issue Entity Framework disposing with async controllers in Web api/MVC 在发布之前,我做了一个基于我的问题的小型研究, 实体框架在Web api / MVC中使用异步控制器进行处理

This is how I pass parameters to my OnRequestCompletedEvent 这是我将参数传递给OnRequestCompletedEvent

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

This is the OnRequestCompleted() declaration 这是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
          });
        }

And this is how I process and use my dbContext 这就是我处理和使用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();

By using SaveChanges instead of SaveChangesAsync everything works. 通过使用SaveChanges而不是SaveChangesAsync一切正常。 My only idea is to create another dbContext by passing the previous DbContext's SQL connection string 我唯一的想法是通过传递前一个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();
    }

The latter code excerpt is just a small test to check my supposition, basically I should pass the SQL connection string instead of the DbContext object. 后面的代码摘录只是一个小测试来检查我的假设,基本上我应该传递SQL连接字符串而不是DbContext对象。

I am not sure (in terms of best practice) if I should pass a connection string and create a new dbContext object (and dispose it by using a using clause) or if I should use/have another mindset for this issue. 我不确定(就最佳实践而言)是否应该传递一个连接字符串并创建一个新的dbContext对象(并使用using子句来处置它),或者我是否应该使用/拥有另一种思维模式来解决这个问题。

From what I know, using a DbContext should be done for a limited set of operations and not for multiple purposes. 据我所知,使用DbContext应该针对一组有限的操作而不是出于多种目的。

EDIT 01 编辑01

I'm going to detail more thorough what I've been doing down below. 我将详细介绍下面我一直在做的事情。

I think I got an idea of why this error happens. 我想我知道为什么会发生这种错误。

I have 2 controllers One that receives a JSON and after de-serializing it I return a JSON to the caller and another controller that gets a JSON that encapsulates a list of objects that I iterate in an async way, returning an Ok() status. 我有2个控制器一个接收JSON并且在反序列化之后我将JSON返回给调用者,另一个控制器获取JSON,该JSON封装了我以异步方式迭代的对象列表,返回Ok()状态。

The controllers are declared as async Task<IActionResult> and both feature an async execution of 2 similar methods. 控制器被声明为async Task<IActionResult>并且都具有2个类似方法的async执行。

The first one that returns a JSON executes this method 第一个返回JSON的方法执行此方法

await ProcessFiscalNo(requestFiscalView.FiscalNo, dbContext);

The second one (the one that triggers this error) 第二个(触发此错误的那个)

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

Both methods (the ones defined previously) start an event OnOperationComplete() Within that method I execute the code from my post's beginning. 两种方法(前面定义的方法)都会启动一个事件OnOperationComplete()在该方法中,我从帖子的开头执行代码。 Within the ProcessFiscalNo method I DO NOT use any using contexts nor do I dispose the dbContext variable. ProcessFiscalNo方法中,我不使用任何使用上下文,也不处理dbContext变量。 Within this method I only commit 2 major actions either updating an existing sql row or inserting it. 在这个方法中,我只提交了2个主要操作,要么更新现有的sql行,要么插入它。 For edit contexts I select the row and tag the row with the modified label by doing this 对于编辑上下文,我选择行并使用修改后的标签标记行

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

or by inserting the row 或插入行

dbContext.FiscalNumbers.Add(partnerFiscalNumber);

and finally I execute an await dbContext.SaveChangesAsync(); 最后我执行await dbContext.SaveChangesAsync();

The error always gets triggered within the EventHandler ( the one detailed @ the beginning of the thread) during the await dbContext.SaveChangedAsync() which is pretty weird since 2 lines before that I do await reads on my DB with EF. await dbContext.SaveChangedAsync()期间,错误总是在EventHandler(一个详细的@开头的线程)中被触发,这是非常奇怪的,因为之前的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();

For some reason calling await within the Event Handler notifies the caller to dispose the DbContext object. 出于某种原因,在事件处理程序中调用await会通知调用者处理DbContext对象。 Also by re-creating the DbContext and not re-using the old one I see a huge improvement on access. 此外,通过重新创建DbContext而不是重新使用旧的DbContext ,我看到了访问的巨大改进。 Somehow when I use the first controller and return the info the DbContext object appears to get flagged by the CLR for disposal but for some unknown reason it still functions. 不知何故,当我使用第一个控制器并返回信息时, DbContext对象似乎被CLR标记为处理但由于某些未知原因它仍然起作用。

EDIT 02 Sorry for the bulk-ish content that follows, but I've placed all of the areas where I do use dbContext. 编辑02对于随后的批量内容很抱歉,但我已经放置了所有使用dbContext的区域。

This is how I'm propagating my dbContext to all my controllers that request it. 这就是我将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>();
    }

In Configure I'm using the dbContext for my custom MiddleWare 在配置中我使用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();
        }

In the custom MiddleWare I'm only using it for a query. 在自定义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)));
        }

In my controller's CTOR I'm passing the dbContext 在我的控制器的CTOR中,我传递了dbContext

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

This is my Post 这是我的帖子

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

Within the ProcessFiscalNoSingle method I have the following usage, If that partner exists then I'll grab him, if not, create and return him. 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;
        }

Further down in the code, there's the ProcessFiscalNo method where I use the dbContext 在代码的下面,有我使用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();

Also I've got an Event called OnRequestCompleted() where I pass my actual dbContext (after it ends up with SaveChangesAsync() if I update/create it) 此外,我有一个名为OnRequestCompleted()的事件,我传递了我的实际dbContext(如果我更新/创建它,最后会使用SaveChangesAsync())

The way I initiate the event args. 我发起事件的方式是args。

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

This is the notifier class (where the error occurs) 这是通知程序类(发生错误的地方)

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

Somehow when the dbContext reaches the Event Handler CLR gets notified to dispose the dbContext object (because I'm using await?) Without recreating the object I was having huge lag when I wanted to use it. 不知何故,当dbContext到达事件处理程序时,CLR会收到处理dbContext对象的通知(因为我正在使用await?)而没有重新创建对象,当我想使用它时,我有很大的滞后。

While writing this I have an idea, I did upgrade my solution to 1.1.0 and I'm gonna try to see if it behaves similarly. 在写这篇文章时我有一个想法,我确实将我的解决方案升级到1.1.0,我将尝试查看它是否表现相似。

Concerning Why you get the error 关于你为何得到错误

As pointed out at the Comments by @set-fu DbContext is not thread safe . 正如@ set-fu DbContext的评论所指出的那样,它不是线程安全的

In addition to that, since there is no explicit lifetime management of your DbContext your DbContext is going to get disposed when the garbage collector sees fit. 除此之外,由于DbContext 没有明确的生命周期管理, 因此当垃圾收集器认为合适时, DbContext将被释放。

Judging from your context, and your mention about Request scoped DbContext I suppose you DI your DbContext in your controller's constructor. 从您的上下文来看,以及您对Request scoped DbContext的提及,我想您在控制器的构造函数中DI你的DbContext。 And since your DbContext is request scoped it is going to be disposed as soon as your Request is over, 因为您的DbContext是请求范围,所以一旦您的请求结束,它就会被处理掉,

BUT since you have already fired and forgot your OnRequestCompleted events there is no guarantee that your DbContext won't be disposed. 由于您已经解雇并忘记了OnRequestCompleted事件,因此无法保证不会丢弃您的DbContext。

From there on , the fact that one of our methods succeeds and the other fails i think is seer " Luck ". 从那以后,我们的一个方法成功而另一个失败的事实我认为是先见“ 运气 ”。 One method might be faster than the other and completes before the Garbage collector disposes the DbContext. 一种方法可能比另一种方法更快,并垃圾收集器处理DbContext 之前完成。

What you can do about this is to change the return type of your Events from 您可以做的是更改事件的返回类型

async void

To

async Task<T>

This way you can wait your RequestCompleted Task within your controller to finish and that will guarantee you that your Controller/DbContext will not get Disposed until your RequestCompleted task is finished. 这样,您可以在控制器中等待你的RequestCompleted 任务来完成,这将保证你,直到你的RequestCompleted任务完成后你的控制器/的DbContext不会得到弃置。

Concerning Properly handling DbContexts 关于 正确处理DbContexts

There are two contradicting recommendations here by microsoft and many people use DbContexts in a completely divergent manner. 微软有两个相互矛盾的建议,许多人以完全不同的方式使用DbContexts。

  1. One recommendation is to "Dispose DbContexts as soon as posible" because having a DbContext Alive occupies valuable resources like db connections etc.... 一个建议是“尽快处理DbContexts”,因为DbContext Alive占用了像db连接等宝贵的资源....
  2. The other states that One DbContext per request is highly reccomended 另一个说明每个请求的一个DbContext被高度推荐

Those contradict to each other because if your Request is doing a lot of unrelated to the Db stuff , then your DbContext is kept for no reason. 那些相互矛盾,因为如果你的请求与Db的东西做了很多无关,那么你的DbContext就没有任何理由了。 Thus it is waste to keep your DbContext alive while your request is just waiting for random stuff to get done... 因此,当您的请求只是等待随机的东西完成时,保持您的DbContext活着是浪费...

So many people who follow rule 1 have their DbContexts inside their "Repository pattern" and create a new Instance per Database Query 许多遵循规则1的人在他们的“存储库模式”中都有他们的DbContexts,并为每个数据库查询创建一个新的实例

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

They just get their data and dispose the context ASAP. 他们只是尽快获取数据并处理上下文。 This is considered by MANY people an acceptable practice. 许多人认为这是可以接受的做法。 While this has the benefits of occupying your db resources for the minimum time it clearly sacrifices all the UnitOfWork and "Caching" candy EF has to offer. 虽然这有利于在最短的时间内占用您的数据库资源,但它明显牺牲了EF提供的所有UnitOfWork“缓存”糖果。

So Microsoft's recommendation about using 1 Db Context per request it's clearly based on the fact that your UnitOfWork is scoped within 1 request. 因此,微软建议每个请求使用1 Db Context,这显然是基于UnitOfWork在1个请求范围内的事实。

But in many cases and i believe your case also this is not true. 在许多情况下,我相信你的情况也不是这样。 I consider Logging a separate UnitOfWork thus having a new DbContext for your Post-Request Logging is completely acceptable (And that's the practice i also use). 我认为记录一个单独的UnitOfWork,因此为你的Post-Request Logging提供一个新的DbContext是完全可以接受的(这也是我也使用的做法)。

An Example from my project i have 3 DbContexts in 1 Request for 3 Units Of Work. 我的项目中的一个示例我在3个工作单元的请求中有3个DbContexts。

  1. Do Work 做工作
  2. Write Logs 写日志
  3. Send Emails to administrators. 将电子邮件发送给管理员。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM