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