Search code examples
dependency-injectionentity-framework-coreextension-methods

EF Core explicitly loading related data in a scoped service using fluent extension methods


Lets say I have the following entities:

public class EntityA 
{
    //....
    public EntityB Child { get; set; }
}

public class EntityB
{
    //....
}

And I have a scoped service where I return these entities:

public class MyScopedService
{
    public Task<IQueryable<EntityA>> GetById(int id)
    {
        var entity = _dbContext.EntityA.First(e => e.Id == id);
        return await Task.FromResult(entity);
    }
}

Now I want a way to explicitly load the related data, something like this:

public class MyScopedService
{
    public async Task<IQueryable<EntityA>> GetById(int id)
    {
        var entity = _dbContext.EntityA.First(e => e.Id == id);
        return await Task.FromResult(entity);
    }

    public static async Task<EntityA> WithEntityB(this EntityA entity)
    {
        return await _dbContext.Entry(entity).Reference(e => e.EntityB).LoadAsync();
    }
}

So in pages or controllers or wherever, I can do this:

var entity = await _myScopedService.GetById(id).WithEntityB();

Obviously, this will not work as you can't have extension methods inside a non-static class. So what is the best way of achieving something similar inside a scoped service?

I could pass the DbContext as a parameter into the extension method like so:

var entity = await _myScopedService.GetById(id).WithEntityB(_myScopedService.DbContext);

But then I would have to expose my context from inside my scoped service. I am looking for something which does not require this.


Solution

  • Your methods are useless especially if you care about performance. What you can do additionally - it is just correct GetById method to return IQueryable

    public class MyScopedService
    {
        public IQueryable<EntityA> GetById(int id)
        {
            var query = _dbContext.EntityA.Where(e => e.Id == id);
            return query;
        }
    }
    

    WithEntityB - it is strange replacement of Include.

    var entity = await _myScopedService.GetById(id).Include(e => e.EntityB).FirstAsync();
    

    Anyway, you can add Include in WithEntityB and return IQueryable again:

    public static IQueryable<EntityA> WithEntityB(this IQueryable<EntityA> query)
    {
        return query.Include(e => e.EntityB);
    }
    
    var entity = await _myScopedService.GetById(id).WithEntityB().FirstAsync();
    

    Main idea that you have to play with IQueryable as long as possible. At the end you have to use one of the available materialization methods (First, Single, ToArray, ToList, Count, Any, ect.)

    Also, do not create async methods if it is not needed. It is another performance gap.