简体   繁体   中英

Returning IEnumerable straight from database or using ToListAsync before

How do the controllers work when giving them IEnumerable straight from the database? Which code is more correct and optimal? Let's assume that the database is very slow and there are other operations going on.

This example is very simple so there might not be enough difference in execution times but i am trying to learn the best practices.

#1

public Task<Application[]> Find(Expression<Func<Application, bool>> predicate)
{
    return DatabaseContext.Applications
        .Where(predicate)
        .ToArrayAsync();
}

...

public Task<Application[]> Find(...)
{
    return ApplicationService.Find(...);
}

#2

public Task<List<Application>> Find(Expression<Func<Application, bool>> predicate)
{
    return DatabaseContext.Applications
        .Where(predicate)
        .ToListAsync();
}

...

public async Task<IActionResult> Find(...)
{
    var applications = await ApplicationService.Find(...)
    return Ok(applications);
}

#3

public IEnumerable<Application> Find(Expression<Func<Application, bool>> predicate)
{
    return DatabaseContext.Applications;
}

...

public IActionResult<IEnumerable<Application>> Find(...)
{
    var applications = ApplicationService.Find(...);
    return Ok(applications);
}

How do the controllers work when giving them IEnumerable straight from the database?

(By IEnumerable I assume you mean directly returning an unexecuted IQueryable from your DbContext )

They don't and you shouldn't - this is because an unexecuted IQueryable does not represent loaded data - and when it is executed it can only load data from an open database connection - which requires an active and valid DbContext .

...so if the DbContext is disposed, then the IQueryable cannot be executed.

If you create the DbContext inside the controller action and render the IQueryable in your view or return it in an ObjectResponse (for Web API) then it will always fail:

public IActionResult GetPeople()
{
    // WARNING: NEVER DO THIS!
    using( MyDbContext db = new MyDbContext( GetConnectionString() ) )
    {
        return this.Ok( db.People.Where( p => p.Name == "John Smith" ) );

        // or:

        return this.View( model: db.People.Where( p => p.Name == "John Smith" ) );
    }
}

Remember that .Ok() and this.View() does not trigger evaluation of the View or send an object-response to the client - instead it causes the Controller action to end first and then pass the data on to the next step in the ASP.NET pipeline (ie the view). Remember: views are executed after the controller action is finished.

If you use Dependency Injection to have a ready instance of your DbContext in the Controller then the results are less predictable: the IQueryable can still be evaluated after the action method returns because the DbContext won't be disposed until after the Controller is disposed which is generally after the view is rendered, however you still shouldn't do this because your IQueryable could still be passed-on to some process that outlives the life of your Controller class which would then cause a failure. You should also avoid it because Views are designed to be rendered quickly and synchronously - having an external database or IO call breaks that design.

(And you shouldn't use Entity Framework entity objects as root ViewModels anyway, but that's another discussion).

You can avoid this habit if you always use the async operations on DbContext (eg ToListAsync() , ToDictionaryAsync , etc - because those return a Task<List<T>> or TaskDictionary<TKey,TValue>> respectively - which require an await which the compiler will prevent you from doing in a View or Object result by default (you can have await in views, but it's inadvisable and requires setting some settings somewhere).

In short, always do this:

public async Task<IActionResult> GetPeople()
{
    using( MyDbContext db = new MyDbContext( GetConnectionString() ) )
    {
        List<Person> list = await db.People
            .Where( p => p.Name == "John Smith" )
            .ToListAsync();

        // WebAPI:
        return this.Ok( list ); // returning an evaluated list, loaded into memory. (Make sure Lazy Navigation Properties are disabled too)

        // MVC:
        PeopleListViewModel vm = new PeopleListViewModel(); // in MVC always use a custom class for root view-models so you're not accepting nor returning Entity Framework entity types directly
        vm.List = list;

        return this.View( vm );
    }
}
  1. You are returning a task to be executed by the mvc framework;
  2. You are running (asynchronously) a task as soon as you await it, then getting the result and handing it to the mvc framework;
  3. You are returning an enumerator to be executed by the mvc framework.

I would go with option #2 because you know exactly when the database query will be executed. Since you are returning a Task and correctly using async and await keywords, the framework will keep as many threads as busy as it can, leveraging the throughput of the application.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM