Entity Framework Lazy Loading with generic reposit

2019-06-01 06:24发布

My current project uses a generic repository interface, thus:

public interface IDataSource : IDisposable
{
    void Add<T>(T newItem) where T : EntityBase;

    T Get<T>(Guid id) where T : EntityBase;
    T Get<T>(Expression<Func<T, bool>> predicate) where T : EntityBase;
    IQueryable<T> GetAll<T>(Expression<Func<T, bool>> predicate = null) where T : EntityBase;
    int Count<T>(Expression<Func<T, bool>> predicate = null) where T : EntityBase;
    bool Any<T>(Expression<Func<T, bool>> predicate = null) where T : EntityBase;

    void Update<T>(T updated) where T : EntityBase;

    void Delete<T>(Guid id) where T : EntityBase;
    void Delete<T>(T item) where T : EntityBase;

    void Commit();
}

As an example, the Get method looks like this:

public T Get<T>(Expression<Func<T, bool>> predicate) where T : EntityBase
{
    return db.Set<T>().Single(predicate);
}

where db is an instance my data context, which extends Entity Framework's DbContext. The whole thing implements IDisposable so that I can use it in a scope block for unit-of-work pattern, waiting to the end before committing changes, or disposing the entire thing if something goes wrong before that.

This interface is used by a logic layer to handle more complex queries, to keep business logic entirely separated from data access. So, a query to that layer might go like this:

public List<Product> ItemsBoughtByCustomer(Guid customerID)
{
    using (var db = DataAccess.GetContext())
    {
        List<Purchase> purchaseHistory = db.GetAll<Purchase>(p => p.CustomerID == customerID);
        List<int> IDs = purchaseHistory.Select(p => p.ProductID);
        return db.GetAll<Product>(p => IDs.Contains(p.ID));
    }
}

(Yes, I realise that can be condensed; it is in the app, but for an example, this is clearer.)

My problem is that sometimes I return a set of objects, and then later I might want to get to some of the things it references. For example, when I get a Product to display, the Display might want to do this:

@foreach (Comment comment in Product.Comments)
{
    <div class="comment">
        <span>@Html.UserDisplay(comment.Author)</span>
        <span>@comment.Body</span>
    </div>
}

(ignore the quality of the HTML; again, it's a quick example)

The problem is that this throws errors when Entity Framework's lazy loading leaves these properties null when returning entities from my queries. Now, I'm aware of the Include() method, but if my repository is generic then it's difficult to apply those. I could turn it off entirely, but then EF will start retrieving enormous linked collections of things when I don't need them - the structure of my model and the links that things have out to the audit logs mean a lot of links for EF to follow.

Is there a way that I can lazy-load in a slightly smarter manner? Is there a method like .Single() and .Where() that I can call on the DbSet that will bring child objects as well, so that I can specifically ask for child objects to be included for a certain query?

1条回答
Lonely孤独者°
2楼-- · 2019-06-01 07:26

add an optional parameter for the include path then invoke Include(str) on the DbSet. Example with your Get method:

public T Get<T>(Expression<Func<T, bool>> predicate, string includePath = null) where T : EntityBase
{
    var query = db.Set<T>();

    if( !string.IsNullorWhitespace( includePath ) )
    {
        query = query.Include( includePath );
    }

    return query.Single(predicate);
}
查看更多
登录 后发表回答