Which types should my Entity Framework repository

2019-01-16 18:00发布

问题:

I have a concrete repository implementation that returns a IQueryable of the entity:

public class Repository
{
    private AppDbContext context;

    public Repository()
    {
        context = new AppDbContext();
    }


    public IQueryable<Post> GetPosts()
    {
        return context.Posts;
    }
}

My service layer can then perform LINQ as needed for other methods (where, paging, etc)

Right now my service layer is setup to return IEnumerable:

public IEnumerable<Post> GetPageOfPosts(int pageNumber, int pageSize)
{ 
    Repository postRepo = new Repository();

    var posts = (from p in postRepo.GetPosts()  //this is IQueryable
                orderby p.PostDate descending
                select p)
                .Skip((pageNumber - 1) * pageSize)
                .Take(pageSize);

    return posts;
}

This means in my codebehind I have to do a ToList() if I want to bind to a repeater or other control.

Is this the best way to handle the return types or do I need to be converting to list before I return from my service layer methods?

回答1:

Both approaches are possible and it is only matter of choice.

Once you use IQueryable you have simple repository which will work in the most cases but it is worse testable because queries defined on IQueryable are linq-to-entities. If you mock repository they are linq-to-objects in unit tests = you don't test your real implementation. You need integration tests to test your query logic.

Once you use IEnumerable you will have very complex public interfaces of your repositories - you will need special repository type for every entity which needs special query exposed on the repository. This kind of repositories was more common with stored procedures - each method on the repository was mapped to single stored procedure. This type of repository provides better separation of concerns and less leaky abstraction but in the same time it removes a lot of ORM and Linq flexibility.

For the last you can have combined approach where you have methods returning IEnumerable for most common scenarios (queries used more often) and one method exposing IQueryable for rare or complex dynamically build queries.

Edit:

As noted in comments using IQueryable has some side effects. When you expose IQueryable you must keep your context alive until you execute the query - IQueryable uses deferred execution in the same way as IEnumerable so unless you call ToList, First or other functions executing your query you still need your context alive.

The simplest way to achieve that is using disposable pattern in the repository - create context in its constructor and dispose it when repository disposes. Then you can use using blocks and execute queries inside them. This approach is for very simple scenarios where you are happy with single context per repository. More complex (and common) scenarios require context to be shared among multiple repositories. In such case you can use something like context provider / factory (disposable) and inject the factory to repository constructor (or allow provider to create repositories). This leads to DAL layer factory and custom unit of work.



回答2:

The other word for your question seems to need to determine when the AppDbContext is disposed or where it is.

If you don't dispose it, meaning it's disposed when a application exits, it is no problem to return IEnumerable/IQueryable, no having actual data. However, you would need to return the type as IList, having actual data, before the AppDbContext is disposed.

UPDATE: I think you would need to catch the following code meaning though you already know.

//outside of this code is refered to your code.

//Returning IEnumerable could be used outside this scope if AppDbContext is ensured no disposing
public IEnumerable<Post> GetIEnumerableWithoutActualData()
{
    return context.Posts;
}

//Even if AppDbContext is disposed, IEnumerable could be used.
public IEnumerable<Post> GetIEnumerableWithActualData()
{
    return context.Posts.ToList();
}


回答3:

Your returns types should always be as high up on the inheritance hierarchy as possible (or maybe I should write that as low, if the base is towards the bottom). If all your methods require IQueryable<T>, then all the return values should surrender that type.

That said, IEnumerable<T> has a method (AsQueryable()) you can call to achieve (what I believe to be) the desired result.