Saving a file asynchronously in ASP.NET Core causing exceptions with DbContext

asp.net asp.net-core asynchronous c# entity-framework-6

Question

I have an application that saves document information for customers. I am trying to add functionality to allow users to upload PDFs and have them saved to disk. I am using the recommended code found here and here.

When the code works, everything looks good -- the document entity is saved to the database and the uploaded file is saved to disk. However, I am getting sporadic exceptions thrown -- things like System.ObjectDisposedException ("Cannot access a disposed object"), sometimes a DbUpdate exception saying a reference is null, etc... I can reproduce the issue, but not 100% every time and not with the same exception every time.

I suspect this is somehow being caused by using CopyToAsync when saving the file. I am not very experienced with async code and I may be doing something obviously wrong. If I change the method to use the synchronous CopyTo instead, it seems to fix the problem. I'm trying to understand why this is happening.

I am injecting my repo and DbContext using AutoFac. Here is the code for the service method which is called by the controller:

public async Task<int> SaveDocument(DocumentDetailDto dto, string user, string webRoot)
{
     documentToSave = new Document();

     // Code not shown that maps dto to new document and adds it to context

     if (dto.FileUpload != null && dto.FileUpload.Length > 0)
     {
          var fileName = $"{dto.CustomerId}-{dto.DocumentTypeId}-{DateTime.Now.Ticks}.pdf";

          var filePath = Path.Combine(webRoot, "uploads");

          using (var fileStream = new FileStream(Path.Combine(filePath, fileName), FileMode.Create))
          {
               await dto.FileUpload.CopyToAsync(fileStream);
          }
     }

     _repo.Save(user);

     return documentToSave.Id;
}

Do you see anything obviously wrong with this setup? Is there anything special I need to do when using async with a FileStream right before calling save on the context? I'm still trying to debug the errors, but it almost seems like the issue is some kind of conflict between the open FileStream and the DbContext trying to write to the database. Any ideas to try are most welcome!

Edited to add some additional code:

Here is how I am registering the DbContext in Startup.cs:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
     services.AddMvc();

     // Add DbContext
     var connection = Configuration.GetConnectionString("DefaultConnection");

     services.AddDbContext<DocContext>(
                options => options.UseSqlServer(connection, b => b.MigrationsAssembly("InfrastructureLayer")));

      // Add repository
      services.AddScoped<IRepository, EntityFrameworkRepository<DocContext>>();

      services.AddTransient<IResolveUserService, ResolveUserService>();

      // Autofac setup
      var containerBuilder = new ContainerBuilder();
      containerBuilder.RegisterModule<ServiceLayer.AutofacModule>();
      containerBuilder.Populate(services);
      var container = containerBuilder.Build();
      return new AutofacServiceProvider(container);
 }

Here is the Save method in the repo:

public virtual void Save(string user = "")
{
     var modifiedEntries = Context.ChangeTracker.Entries<IEntity>()
          .Where(x => x.State == EntityState.Modified)
          .Select(x => x.Entity)
          .ToList();

     foreach (var entity in modifiedEntries)
     {
          entity.ModifiedDate = DateTime.UtcNow;
          entity.ModifiedBy = user;
     }

     var newEntries = Context.ChangeTracker.Entries<IEntity>()
          .Where(x => x.State == EntityState.Added)
          .Select(x => x.Entity)
          .ToList();

     foreach (var entity in newEntries)
     {
          entity.CreatedDate = DateTime.UtcNow;
          entity.CreatedBy = user;
     }

     Context.SaveChanges();
}

And here is how the SaveDocument method is called from the controller:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Save(DocumentDetailDto dto, [FromServices]IResolveUserService userService, [FromServices]IHostingEnvironment environment)
{
     _service.SaveDocument(dto, userService.GetUser(), environment.WebRootPath);

     return RedirectToAction("Detail", "Customers", new { id = dto.CustomerId });
}

Thanks!

1
1
7/3/2017 10:18:59 PM

Accepted Answer

You should await for _service.SaveDocument to complete. Currently you start it and does not wait its results. Instead you send successful response to user, request is processed, used resources may be disposed at any moment.

Change your controller action to:

public async Task<IActionResult> Save(...
{
     await _service.SaveDocument(...

     return RedirectToAction(...
}
4
7/3/2017 11:16:50 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow