Let's say we have four entities in data model: Categories, Books, Authors and BookPages. Also assume Categories-Books, Books-Authors and Books-BookPages relationships are one-to-many.
If a category entity instance is retrieved from database - including "Books", "Books.BookPages" and "Books.Authors" - this will become a serious performance issue. Moreover, not including them will result in "Object reference is not set to an instance of an object" exception.
What is the best practice for using multiple Include method calls?
EDIT: By second option I meant something like this:
public static Category GetCategoryById(ModelEntities db, int categoryId, params string[] includeFields)
{
var categories = db.Categories;
foreach (string includeField in includeFields)
{
categories = categories.Include(includeField);
}
return categories.SingleOrDefault(i => i.CategoryId == categoryId);
}
When calling we need a code like this:
Category theCategory1 = CategoryHelper.GetCategoryById(db, 5, "Books");
Category theCategory2 = CategoryHelper.GetCategoryById(db, 5, "Books", "Books.Pages");
Category theCategory3 = CategoryHelper.GetCategoryById(db, 5, "Books", "Books.Authors");
Category theCategory4 = CategoryHelper.GetCategoryById(db, 5, "Books", "Books.Pages", "Books.Authors");
Are there any distinct disadvantages of this approach?
Write a single method GetCategoryById and send a list of relationships to include (maybe, but still seems not elegant enough)
Write methods like GetCategoryByIdWithBooks, GetCategoryByIdWithBooksAndBooksPages and GetCategoryByIdWithBooksAndAuthors (not practical)
A combination of these two is currently my approach. I know what properties I want to include for each context, so I rather hand-code them (as you said yourself, lazy-loading isn't always an option, and if it is, you'll repeat the same repetitive Include()
-like syntax when mapping from data models to DTO's).
This separation causes you to think harder about what datasets you want to expose, given data-access-code like this is usually hidden beneath a service.
By utilizing a base class containing a virtual method you can override to run the required Include()
s:
using System.Data.Entity;
public class DataAccessBase<T>
{
// For example redirect this to a DbContext.Set<T>().
public IQueryable<T> DataSet { get; private set; }
public IQueryable<T> Include(Func<IQueryable<T>, IQueryable<T>> include = null)
{
if (include == null)
{
// If omitted, apply the default Include() method
// (will call overridden Include() when it exists)
include = Include;
}
return include(DataSet);
}
public virtual IQueryable<T> Include(IQueryable<T> entities)
{
// provide optional entities.Include(f => f.Foo) that must be included for all entities
return entities;
}
}
You can then instantiate and use this class as-is, or extend it:
using System.Data.Entity;
public class BookAccess : DataAccessBase<Book>
{
// Overridden to specify Include()s to be run for each book
public override IQueryable<Book> Include(IQueryable<Book> entities)
{
return base.Include(entities)
.Include(e => e.Author);
}
// A separate Include()-method
private IQueryable<Book> IncludePages(IQueryable<Book> entities)
{
return entities.Include(e => e.Pages);
}
// Access this method from the outside to retrieve all pages from each book
public IEnumerable<Book> GetBooksWithPages()
{
var books = Include(IncludePages);
}
}
Now you can instantiate a BookAccess
and call methods on it:
var bookAccess = new BookAccess();
var allBooksWithoutNavigationProperties = bookAccess.DataSet;
var allBooksWithAuthors = bookAccess.Include();
var allBooksWithAuthorsAndPages = bookAccess.GetBooksWithPages();
In your case, you might want to create separate IncludePages
and GetBooksWithPages
-alike method pairs for each view of your collection. Or just write it as one method, the IncludePages
method exists for reusability.
You can chain these methods all the way you like, since each of them (as well as Entity Framework's Include()
extension method) returns yet another IQueryable<T>
.