Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP .NET Entity Framework Core Cannot access a disposed object

Wanting to get into .NET Core, I created a WEB API that takes a file upload and then saves the transactions in the file into a DB table. I'm using .NET Core 2 with Entity Framework Core. I created my context using the example from here.

My problem is that I get the error "System.ObjectDisposedException Cannot access a disposed object" when it tries to save to the context object in my repository. It's a simple stack, so I'm hoping someone can help me out.

My container setup looks like this:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        services.AddDbContext<SyncFinContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
        services.AddScoped<ITransactionProcessor, TransactionProcessor>();
        services.AddScoped<ITransactionRepository, TransactionRepository>();
    }

My DBInitializer which I also got from the link above:

public static class DbInitializer
{
    public static async Task Initialize(SyncFinContext context)
    {
        await context.Database.EnsureCreatedAsync();

        // Look for any students.
        if (context.Transactions.Any())
        {
            return;   // DB has been seeded
        }

        var ts = new Transaction[]
        {
            // insert test data here
        };

        await context.SaveChangesAsync();
    }
}   

My DB Context:

public class SyncFinContext : DbContext
{
    public SyncFinContext(DbContextOptions<SyncFinContext> options) : base(options)
    {
    }

    public DbSet<Transaction> Transactions { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Transaction>().ToTable("Transactions");
    }
}

My Controller looks like this:

[Produces("application/json")]
public class TransactionController : Controller
{
    ITransactionRepository _transactionRepository { get; set; }
    ITransactionProcessor _transactionProcessor { get; set; }

    public TransactionController(ITransactionRepository m, ITransactionProcessor p) : base()
    {
        _transactionRepository = m;
        _transactionProcessor = p;
    }

    // POST: transaction/import
    [HttpPost]
    public async void Import(List<IFormFile> files)
    {
        if (files == null || files.Count == 0)
        {
            throw new FileNotFoundException("No file was received.");
        }

        // copy file to temp location so that it can be processed
        var filepath = Path.GetTempFileName();
        using (var stream = new FileStream(filepath, FileMode.Create))
        {
            await files[0].CopyToAsync(stream);
        }

        ImportTransactionRequest input = new ImportTransactionRequest
        {
            FilePath = filepath
        };

        var transactions = await _transactionProcessor.ReadDocument(filepath);

        await _transactionRepository.AddBulk(transactions);            
    }
}

And my repository looks like this:

public class TransactionRepository : ITransactionRepository
{

    // TODO: move the context
    private SyncFinContext _context;
    public TransactionRepository(SyncFinContext context)
    {
        _context = context;
    }

    public async Task AddBulk(List<Transaction> transactions)
    {
        foreach(var t in transactions)
        {
            await _context.Transactions.AddAsync(t);
        }

        _context.SaveChanges();

    }
}

For full transparency, the transaction Processor just gets a list of rows from a csv:

    public async Task<List<Transaction>> ReadDocument(string filepath)
    {
        try
        {
            var ret = new List<Transaction>();
            var lines = await File.ReadAllLinesAsync(filepath);

            foreach (var line in lines)
            {
                var parts = line.Split(',');

                var tx = new Transaction
                {
                    PostedDate = DateTime.Parse(parts[0]),
                    TransactionDate = DateTime.Parse(parts[1]),
                    Description = parts[2],
                    Deposit = ParseDecimal(parts[3]),
                    Withdrawal = ParseDecimal(parts[4]),
                    Balance = ParseDecimal(parts[5])
                };

                ret.Add(tx);
            }

            return ret;
        }
        catch(Exception e)
        {
            throw;
        }
    }

I've read where the whole stack must be async in order for the db context instance to be available, and, unless I'm doing it wrong, I seem to be doing that, as you can see above.

My expectations are that AddDbContext() will indeed properly scope the context to be available throughout the stack unless I explicitly dispose of it. I have not found anything to make me think otherwise.

I've tried hard-coding data in my DB Initializer also, as I read that may be a factor, but that does not solve the problem. Not sure what else to try. If someone can give me some ideas I would appreciate it.

like image 278
Ben Avatar asked Dec 14 '22 17:12

Ben


2 Answers

The Import() action method needs to have a return type of Task. MVC will await execute an action method with a return type of Task.

Also, probably best to get in the habit of returning an IActionResult on your action methods. The task based equivalent is Task<IActionResult>. This makes your controllers easier to test.

like image 147
Brad Avatar answered Dec 29 '22 10:12

Brad


Since the AddBulk(List<Transaction> transactions) method is public async Task, the DbContext will be disposed if any part returns void (not awaited) at any point.

Try changing _context.SaveChanges();

To await _context.SaveChangesAsync();

This would ensure a Task is being returned and not void.


https://stackoverflow.com/a/46308661/3062956

https://learn.microsoft.com/en-us/ef/core/saving/async

like image 33
JG Oliver Avatar answered Dec 29 '22 12:12

JG Oliver