0

I have these layers:

  • Repositories
  • Services
  • BlazorServer

In my BlazorServer, I have dependency injected IDbContextFactory, my repositories and services like below:

builder.Services.AddDbContextFactory<EstateDbContext>(OptionsHelper.ContextOptions(builder));
builder.Services.AddScoped<IRepository<Dealer>, EFRepository<Dealer, EstateDbContext>>();
builder.Services.AddScoped<IDealerService, DealerService>();

My repository example with GetTableAsync-method:

public sealed class EFRepository<T, TDbContext> : IRepository<T> where T : BaseEntity where TDbContext : DbContext
{
    private readonly IDbContextFactory<TDbContext> contextFactory;

    public EFRepository(IDbContextFactory<TDbContext> contextFactory)
    {
        this.contextFactory = contextFactory;
    }

    public async Task<IQueryable<T>> GetTableAsync(CancellationToken cancellationToken = default)
    {
        using TDbContext context = await contextFactory.CreateDbContextAsync(cancellationToken);
        DbSet<T> table = context.Set<T>();

        return table;
    }
}

And my Services-layer, example with Get a list of 'Dealer'-entity:

public sealed class DealerService : IDealerService
{
    private readonly IRepository<Dealer> dealerRepository;

    public DealerService(IRepository<Dealer> dealerRepository)
    {
        this.dealerRepository = dealerRepository;
    }

    public async Task<List<Dealer>?> GetDealersAsync(CancellationToken cancellationToken = default)
    {
        IQueryable<Dealer> dealerQuery = await dealerRepository.GetTableAsync(cancellationToken);
        List<Dealer> dealers = await dealerQuery.ToListAsync(cancellationToken);

        return dealers;
    }
}

Lastly, my Blazor Component:

@page "/dealers"
@inject IDealerService DealerService

<h1>Dealers</h1>

<div class="container bg-light p-3 mb-4 border">
    @if (dealers is not null)
    {
        foreach (var dealer in dealers)
        {
            <p>@dealer.Name</p>
        }
    }
    else
    {
        <p>Dealer is null</p>
    }
</div>

@code {
    private List<Dealer>? dealers;

    protected override async Task OnInitializedAsync()
    {
        dealers = await DealerService.GetDealersAsync();
    }
}

I have awaited everything all the way to the application layer, but I still get exception, how do I fix this one:

Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances. Object name: 'EstateDbContext'. |

Note: I have narrowed down my logic in the code above to be as simple as possible.

EDIT: I found the issue, but don't know how to solve it: in Repository, the using-statement is used on GetTableAsync()-method, and after the method is finished, the context is disposed, and then it can't be used in Service-layer... which causes the exception. How would I go about sending a IQueryable to the service layer without it being disposed?

Zecret
  • 360
  • 1
  • 4
  • 19
  • the problem may occur in asynchronous tasks. check if the methods that call DealerService return a task in the Blazor component – Laaziv Mar 05 '23 at 22:32
  • I triple checked, all methods that call DealerService return async Tasks. @Laaziv – Zecret Mar 05 '23 at 22:33
  • you can delete builder.Services.AddDbContextFactory //AddService builder.Services.AddTransient(); – Laaziv Mar 05 '23 at 22:47
  • AddDbContextFactory shouldn't be deleted since it holds my connectionstring to my database.. @Laaziv – Zecret Mar 05 '23 at 23:14
  • you can use a Service.AddDbContext the same way. and for the Service used transient AddTransient();. @Zecret – Laaziv Mar 05 '23 at 23:33
  • Doing so will cause other problems which I have encountered before, because I am using Blazor Server and when loading multiple stuff from Database on the same context it will cause exceptions too. That's why I want to Dispose at method levels using DbContextFactory. @Laaziv – Zecret Mar 06 '23 at 00:04
  • This is a big headache for Blazor Server with it's long-lived scopes. I dealt with it by creating my own Scope from `IServiceScopeFactory` and getting the `DbContext` via DI. – Valuator Mar 06 '23 at 22:02

1 Answers1

1

Your problem, as you've identified, is in your usage of IQueryable.

You solve the problem like this:

    public async ValueTask<IEnumerable<T>> GetTableAsync(CancellationToken cancellationToken = default)
    {
        using TDbContext context = await contextFactory.CreateDbContextAsync(cancellationToken);
        // can turn off tracking as you are only querying
        TDbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

        // get the IQueryable object
        IQueryable<T> query =  context.Set<T>();
        // At this point you can apply any filtering, sorting, paging,....

        // Materialize the query into a memory based IEnumerable object
        // this is an async operation 
        var table = await query.ToListAsync();

        // return the object   
        return table;
    }

Basically, you don't don't materialize your query before the DBContext goes out of scope. An IQueryable object isn't a list in itself: it's a class that defines how to get a list from it's data source. It's a unexecuted SQL query, So when the DbContext goes out of scope at the end of the method, you sever the query from it's source.

In the above code, the query is materialized [run] by calling ToListAsync which gets the data from the database and stores in it a memory based object. You then return it as an IEnumerable which is the memory based equivalent of IQueryable. Lists implement IEnumerable. It won't go out of scope and the GC destroy it until all references to it are released.

I've also changed the Task type to a ValueTask which is normally a little less "expensive" in resource usage.

If you want to know more about materialization and the differences between IQueryable and IEnumerable see this - What is the difference between IQueryable<T> and IEnumerable<T>? and many other articles.

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • I currently use just one generic repository (EFRepository.cs) which holds 4 methods: GetTable, Create, Update & Delete. I usually pass IQueryable with GetTable-method to the service layer which is responsible to forming the Queryable with includes, paging, etc. for each Get methods. Does that mean I have to switch up my way of using repositories and instead create a repository for each domain entity I have which has methods for each use-case i.e. GetBookWithAuthor()? – Zecret Mar 06 '23 at 23:33
  • No, you can stay generic. You just need to move your paging, filtering and sorting operations down to the repository service. Paging and sorting is relatively simple, but filtering has it's challenges if you are writing a WASM application with API calls. It looks like you are building a Blazor Server application. If so, see this article that describes how to builds a generic IRepository data pipeline - https://www.codeproject.com/Articles/5350000/A-Different-Repository-Pattern-Implementation – MrC aka Shaun Curtis Mar 07 '23 at 08:15