NullVoxPopuli
NullVoxPopuli

Reputation: 65113

How do I simplify the access of a has-many relationship with the entity framework?

Here is what I want to do:

var user = db.User.First(conditions);
user.Book.First();

Here is currently how I have to do that.

var user = db.User.Include("Book").First(conditionsForUser);
user.Book.First();

The reason why I want to simplify this, is because I don't want to have to specify what is included every time I want to access a relationship. Seems very cumbersome.

e.g.: I would like to just be able to do the following, given I have previously retrieved a user:

user.Book.First()
user.Blog.First()
user.SomeOtherHasManyRelationship.Where(conditions)

Here is what I have so far:

    public object RelationshipFor(string relationship)
    {
        using (var db = User.DbContext())
        {
            var relationshipType = TypeRepresentedBy(relationship); // unused for now, not sure if I need the type of the relationship
            var myTable = ((ICollection)db.Send(RelationshipName)); // RelationshipName is "User" in this instance.
            var meWithRelationship = myTable.Where(i => i.Send(IdColumn) == Id).Include(relationship);  // currently, myTable doesn't know about 'Where' for some reason.
            return meWithRelationship.Send(relationship);
        }
    }

And then how that would be used would be the following:

user.RelationshipFor("Book") // returns a list of books

I have some other logic in my code which abstracts that further which would allow me to do user.Book.First(). Hopefully I can get permission to open source a lot of this, as I'm modelling a lot of the api after ActiveRecord-style crud.

Note, that I'm using I set of extensions I made to help dealing with dynamicness less painful: https://github.com/NullVoxPopuli/csharp-extensions

UPDATE 1:

    public object RelationshipFor(string relationship)
    {
        using (var db = User.DbContext())
        {
            var myTable = (DbSet<DatabaseModels.User>)db.Send(RelationshipName);
            var myInclude = myTable.Include(i => i.Send(relationship));
            var meWithRelationship = myInclude.First(i => (long)i.Send(IdColumn) == Id);
            return meWithRelationship.Send(relationship);
        }
    }

For now, I've hard coded the cast of the user in an attempt to just get something working. My error now is:

Unable to cast object of type 'System.Linq.Expressions.MethodCallExpressionN' to type 'System.Linq.Expressions.MemberExpression'.

Upvotes: 4

Views: 963

Answers (1)

CodeCaster
CodeCaster

Reputation: 151586

This is not a trivial problem, and there's no "one size fits all" approach. What you actually seem to be after is lazy loading, which was not included in EF7 for many reasons.

I don't know what the code you show is supposed to do, but one option would be to introduce a repository pattern, where you specify the "entities to include" at the collection level:

public class UserRepository
{
    private readonly IQueryable<User> _dataSet;

    public UserRepository(IQueryable<User> userDataSet)
    {
        _dataSet = userDataSet;
    }

    public IQueryable<User> Include()
    {
        return _dataSet.Include(u => u.Book)
                       .Include(u => u.Blog);
    }
}

And you can move lots of the logic to a generic base class, leaving you with just the Include() method. You can for example work with strings as you show (or enums, or ...), to only select related entities to include:

public class GenericRepository
{
    // ...

    public IQueryable<User> Include(string includeGroup = null)
    {
        return IncludeGroup(includeGroup);
    }

    protected virtual IncludeGroup(string includeGroup)
    {
        return _dataSet;
    }
}

And then in UserRepository:

protected override IQueryable<User> IncludeGroup(string includeGroup)
{
    switch (includeGroup.ToUpperInvariant())
    {
        case "BOOK":
            return _dataSet.Include(u => u.Book)
                           .Include(u => u.Book.Author);
        case "BLOG":
            return _dataSet.Include(u => u.Blog);
        default:
            return base.Include(includeGroup);
    }
}

And then use it like this:

var userRepo = new UserRepository(db.User);

var userWithBooks = userRepo.Include("Book");

var firstUser = userWithBooks.FirstOrDefault(u => u.Name == "Foo");

var firstUserFirstBook = firstUser.Book.FirstOrDefault();

One alternative would be to always include all navigation properties (recursively), but that would be a horrible approach in terms of query efficiency, as every query will be one massive join to all related tables, whether that is necessary or not.

Upvotes: 1

Related Questions