c# - 在 ASP.NET Core Web Api 中正确处理 DbContexts

标签 c# asp.net entity-framework asp.net-core

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

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

不使用 async有效并且我设法推出的唯一解决方法是通过获取传递给 DbContext 的 EventArgs 的 SQLConnectionString 创建 DbContext 的另一个实例。

在发布之前,我确实根据我的问题做了一个小的研究
Entity Framework disposing with async controllers in 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 个 Controller
一个接收 JSON 并在对其进行反序列化后,我将一个 JSON 返回给调用者,另一个 Controller 获取一个 JSON,该 JSON 封装了我以异步方式迭代的对象列表,返回一个 Ok()地位。

Controller 声明为 async Task<IActionResult>并且都带有 async执行2个类似的方法。

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

第二个(触发此错误的那个)
foreach (string t in requestFiscalBulkView.FiscalNoList)
       await ProcessFiscalNo(t, dbContext);

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

或通过插入行
dbContext.FiscalNumbers.Add(partnerFiscalNumber);

最后我执行了一个 await dbContext.SaveChangesAsync();
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();

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

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

这就是我将 dbContext 传播到所有请求它的 Controller 的方式。
 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();
        }

在自定义 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)));
        }

在我的 Controller 的 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;
        }

在代码的更下方,有 ProcessFiscalNo我使用 dbContext 的方法
 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 对象(因为我正在使用等待?)
如果没有重新创建对象,当我想使用它时,我会遇到很大的延迟。

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

最佳答案

关于为什么会出现错误

正如@set-fu 的评论中指出的那样数据库上下文 不是线程安全的 .

除此之外,因为还有没有明确您的终身管理数据库上下文 当垃圾收集器认为合适时,您的 DbContext 将被处理。

从你的上下文来看,你提到了 请求范围的 DbContext
我想你在 Controller 的构造函数中 DI 你的 DbContext 。
由于您的 DbContext 是请求范围的,因此一旦您的请求结束,它就会被处理掉,

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

从那时起,我认为我们的一种方法成功而另一种方法失败的事实是先见者“运气”。
一种方法可能比另一种更快并完成 之前 垃圾收集器处理 DbContext。

你可以做的是改变你的事件的返回类型

async void


async Task<T>

这样你就可以等你的请求完成 任务 在您的 Controller 中完成,这将保证您的 Controller/DbContext 在您的 RequestCompleted 任务完成之前不会被处理。

关于 正确处理 DbContexts

微软这里有两个相互矛盾的建议,许多人以完全不同的方式使用 DbContexts。
  • 一个建议是 “尽快处理 DbContexts”
    因为拥有 DbContext Alive 会占用宝贵的资源,例如 db
    连接等....
  • 另一个声明 每个请求一个 DbContext 是高度
    推荐

  • 这些相互矛盾,因为如果您的 Request 做了很多与 Db 无关的事情,那么您的 DbContext 就会无缘无故地保留下来。
    因此,当您的请求只是在等待随机的事情完成时,让您的 DbContext 保持事件状态是浪费...

    这么多人关注规则 1 在他们的 中有他们的 DbContexts “存储库模式”并创建 每个数据库查询一个新实例
            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 个请求内这一事实。

    但是在很多情况下,我相信你的情况也是如此。
    我考虑 日志记录 一个单独的 UnitOfWork 因此为您的请求后日志记录提供一个新的 DbContext 是完全可以接受的(这也是我使用的做法)。

    我的项目中的一个示例我在 3 个工作单元的 1 个请求中有 3 个 DbContext。
  • 做工作
  • 写日志
  • 向管理员发送电子邮件。
  • 关于c# - 在 ASP.NET Core Web Api 中正确处理 DbContexts,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40629327/

    相关文章:

    C# System.Diagnostics.Process 如何处理多个批处理文件

    c# - LinqDataSource 因未知原因抛出 ChangeConflictException

    c# - 使用带有 Entity Framework 的 LINQ 语句以编程方式构建查询

    sql-server - Sql Server 数据库连接字符串中的初始目录是什么意思?

    c# - 同时执行两个独立的方法

    c# - 识别 Visual Studio 中的一次性对象?

    javascript - 如何通过单击另一个页面的链接按钮调用按钮的单击事件而无需在 c# ASP.NET 中回发

    javascript - 在同一行的复选框选中/未选中时启用 GridViewRow 中的 DropDownList(使用任何 c#、jquery、javascript)

    entity-framework - 是否有 Entity Framework 的内存提供程序?

    c# - 系统.InvalidCastException : Specified cast is not valid