在ASP.NET Core WebApi

时间:2016-11-16 10:10:45

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

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

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

不使用async工作,我唯一需要解决的解决方法是通过获取EventArgs的SQLConnectionString传递DbContext来创建另一个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个控制器 一个接收JSON并在反序列化之后,我将JSON返回给调用者,另一个控制器获取一个JSON,该JSON封装了我以异步方式迭代的对象列表,返回Ok()状态。

控制器声明为async Task<IActionResult>,两者都以async执行2种类似的方法。

第一个返回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()期间始终在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而不是重新使用旧版本,我看到了访问的巨大改进。 不知何故,当我使用第一个控制器并返回信息时,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作为我的自定义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,
                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,我会试着看看它的行为是否相似。

1 个答案:

答案 0 :(得分:6)

关于您为什么会收到错误

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

除此之外,由于 DbContext 没有明确的生命周期管理,当垃圾收集器认为合适时,DbContext将被处理掉。

根据您的上下文判断,并提及请求作用域的DbContext 我想你在你的控制器的构造函数中DI你的DbContext。 由于您的DbContext是请求作用域,因此只要您的请求结束就会被处理,

但是因为您已经解雇并忘记了OnRequestCompleted事件,所以无法保证您的DbContext不会被处理掉。

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

您可以做的是从

更改活动的返回类型
async void

async Task<T>

通过这种方式,您可以等待控制器中的 RequestCompleted 任务完成,这样可以保证在RequestCompleted任务完成之前,您的Controller / DbContext不会被处理

关于 正确处理DbContexts

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

  1. 一个建议是“尽快处理DbContexts” 因为有一个DbContext Alive占用像db这样的宝贵资源 连接等....
  2. 其他声明每个请求一个DbContext很高 reccomended
  3. 那些相互矛盾的原因是因为如果你的请求与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;
             }
    

    他们只是获取他们的数据并尽快处理上下文。 这被 MANY 人认为是可接受的做法。 虽然这样可以在最短的时间内占用您的数据库资源,但它明显牺牲了EF提供的所有 UnitOfWork “缓存”糖果。

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

    在很多情况下,我相信你的情况也不是这样。 我认为 Logging 是一个单独的UnitOfWork,因此为你的Post-Request Logging提供一个新的DbContext是完全可以接受的(这也是我也使用的做法)。

    我的项目中的一个示例我在3个工作单元的请求中有3个DbContexts。

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