vendredi 8 septembre 2017

Design pattern for explicit loading with Entity Framework

Consider the following, typical repository implementation using Entity Framework

interface IRepository<TModel>
{
    TModel GetById(int id);
    IEnumerable<TModel> GetAll();

    void Add(TModel item);
    void AddRange(IEnumerable<TModel> items);

    void Remove(TModel item);
    void RemoveRange(IEnumerable<TModel> items);
}

class Repository<TModel>
{
    public Repository(DbContext context)
    {
        Context = context;
    }

    protected DbContext Context { get; }

    public TModel GetById(int it)
    {
        return Context.Set<TModel>().Find(id);
    }

    ...
}

This works fine where TModel doesn't have any navigation properties, but where it does, I'd have to create concrete repositories for specific TModel types and override methods where I require navigation properties to be loaded.

class FooRepository : Repository<Foo>
{
    override TModel GetById(int id)
    {
        return Context
            .Set<Foo>()
            .Include(o => o.Bar)
            .ThenInclude(o => o.Baz)
            .Find(id);
    }
}

While this approach respects the open/closed principle, I want a pattern that allows me to determine on the generic repository, which navigation properties can be used.

I'm not sure if there's an existing pattern for this, but here's what I've come up with:

abstract class Loader<TModel>
{
    public static readonly Loader<TModel> Default = new Loader<TModel>();

    public virtual IQueryable<TModel> Load(IQueryable<TModel> queryable)
    {
        return queryable;
    }
}

This requires a change to the repository methods...

TModel GetById(int id, Loader<TModel> loader = Loader<TModel>.Default)
{
    return loader.Load(Context.Set<TModel>()).Find(id);
}

I could then create a custom Loader<TModel> type...

class FooLoader : Loader<Foo>
{
    override IQueryable<Foo> Load(IQueryable<Foo> queryable)
    {
        return queryable
            .Include(o => o.Bar)
            .ThenInclude(o => o.Baz);
    }
}

And use it like so...

IRepository<Foo> repo = new Repository<Foo>();
Foo foo = repo.GetById(123, new FooLoader());

  • Does this look/feel like a natural and appropriate solution?
  • Are there any other patterns that solve this?
  • Does my proposed pattern break any of the SOLID or DRY principles?

Aucun commentaire:

Enregistrer un commentaire