Proper DbContexts handling in ASP.NET Core WebApi

There was a little confusion here. I am not sure if I am handling my DbContext correctly throughout WebApi. I have some controllers that perform some operations on my database (Insert / Update with EF), and after completing these steps I fire an event. In my EventArgs (I have a custom class that inherits from EventArgs ), I pass in my DbContext , and I use it in an event handler to log these operations (basically, I just log authenticated user API requests).

In the event handler, when I try to commit my changes ( await SaveChangesAsync ), I get an error: "Using a hosted object ... etc." basically notice that the first time I use await in my async void (fire and forget) I notify the caller of the removal of the Dbcontext object.

Not using async works, and the only workaround I created is to create another instance of DbContext by getting the SQLConnectionString from EventArgs passed to the DbContext.

Before publishing, I did a little research based on my problem Entity Framework with asynchronous controllers in Web api / MVC

This is how I pass parameters to my OnRequestCompletedEvent

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

This is an 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

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

Using SaveChanges instead of SaveChangesAsync , everything works. My only idea is to create another dbContext by passing the previous dbContext SQL connection string

 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 last piece of code is just a small test to test my assumption, basically I have to pass the SQL connection string instead of the DbContext object.

I'm not sure (from a best practice point of view) if I need to pass the connection string and create a new dbContext object (and delete it using the using clause), or if I have to use / have a different mindset for this problem.

From what I know, using DbContext should be done for a limited set of operations, and not for several purposes.

EDIT 01

I will talk in detail about what I do below.

I think I understand why this error occurs.

I have 2 controllers One that receives JSON and after de-serialization, I return JSON to the caller and another controller that receives JSON, which encapsulates the list of objects that I repeat asynchronously, returning the status of Ok() .

The controllers are declared as async Task<IActionResult> , and both have async executing two similar methods.

The first that returns JSON executes this method

 await ProcessFiscalNo(requestFiscalView.FiscalNo, dbContext); 

The second (the one that causes this error)

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

Both methods (those defined earlier) fire the OnOperationComplete() event OnOperationComplete() Within this method, I fire the code from my message. As part of the ProcessFiscalNo method, I DO NOT use any usage contexts, and I do not use the dbContext variable. As part of this method, I perform only 2 main actions, either updating the existing sql row or inserting it. For editing contexts, I select a line and mark the line with the changed label, doing this

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

or by inserting a line

 dbContext.FiscalNumbers.Add(partnerFiscalNumber); 

and finally I execute await dbContext.SaveChangesAsync();

The error always fires in the EventHandler (one of the @ details at the beginning of the stream) during await dbContext.SaveChangedAsync() which is rather strange, since I wait 2 lines before that to read in my DB using 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, a wait call in the event handler notifies the caller of the removal of the DbContext object. Also, by re-creating the DbContext , rather than reusing the old, I see a huge improvement in access. Somehow, when I use the first controller and return the information, the DbContext object appears to be marked with the CLR for deletion, but for some unknown reason, it still functions.

EDIT 02 Sorry for the contents of the bulk content that follows, but I placed all the areas where I use dbContext.

This is how I distribute my dbContext to all my controllers that request it.

  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 am using dbContext for my custom 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 custom MiddleWare, I use it for query only.

 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 CTOR controller, I pass 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); } 

As part of the ProcessFiscalNoSingle method, I have the following use. If this partner exists, I will take it; if not, create and return it.

 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 in the code there is a ProcessFiscalNo method where I use 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(); 

I also have an event called OnRequestCompleted () where I pass in my actual dbContext (after it ends with SaveChangesAsync () if I update / create it)

The way I trigger event arguments.

  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 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 dbContext reaches the event handler, the CLR receives a notification about the disposal of the dbContext object (because I use the wait?). Without recreating the object, I had a huge lag when I wanted to use it.

While writing this, I had an idea, I updated my solution to version 1.1.0, and I will try to see if it works the same way.

+6
source share
1 answer

About why you get an error message

As pointed out in the comments of @ set-fu, DbContext is unsafe .

In addition to this, since there is no explicit lifecycle management of your DbContext , your DbContext will be deleted when the garbage collector deems fit.

Judging by your context and your mention of Request scoped DbContext, I assume that you made your DbContext in your controller constructor. And since your DbContext is the request area, it will be deleted as soon as your request is completed,

BUT , since you have already fired and forgot your OnRequestCompleted events, there is no guarantee that your DbContext will not be deleted.

From there, the fact that one of our methods succeeds and the other does not work, I think this is Luck seer. One method may be faster than another, and completes before the garbage collector that provides the DbContext.

What you can do is change the return type of events from

 async void 

To

 async Task<T> 

That way you can wait for the RequestCompleted Task in your controller to complete, and this ensures that your Controller / DbContext will not be deleted until your RequestCompleted task is complete.

Relatively Proper DbContexts Processing

There are two conflicting recommendations here from Microsoft, and many people use DbContexts in a completely divergent manner.

  • One recommendation is to "Dispose of DbContexts as soon as possible" because having a DbContext Alive takes up valuable resources, such as db connections, etc.
  • Other indicates that One DbContext for request Recommended

This contradicts each other, because if your request does a lot of non-material Db, then your DbContext is saved for no reason. Thus, it is a waste to keep your DbContext alive while your request is just waiting for random things to be done ...

So many people who follow rule 1 have their own DbContexts inside the repository template and create a new instance to query the database

  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 have context as soon as possible. This is considered by MANY people to be an acceptable practice. Although it has the advantage of taking up your resources in a minimal amount of time, it clearly sacrifices all the benefits of UnitOfWork and Caching . EF can offer.

So, Microsoft's recommendation about using 1 Db Context for a request is clearly based on the fact that your UnitOfWork is covered within 1 request.

But in many cases, and I believe that your case is also wrong. I consider Logging a separate UnitOfWork, so having a new DbContext for your registration after the request is completely acceptable (and this practice, which I also use).

An example from my project I have 3 DbContexts in 1 request for 3 units of work.

  • Do work
  • Log Recording
  • Send emails to administrators.
+3
source

Source: https://habr.com/ru/post/1012367/


All Articles