No, it doesn't take 10 records before paged list. The way your code is shown, AutoMapper
will cause a deferred execution of the query, before it reaches paged list, which means it will return all data (let's suppose, 1000 records). Then PagedList
will properly retrieve 10 of the already materialized List
, and recognize the total amount of record was 1000.
I think you want to filter 10 in database, which has better performance, so you should use PagedList
in the IQueryable
of your database entities like this:
List<Product> filteredProducts = dbContext.Products.OrderBy(p => p.ProductId).ToPagedList(pageNumber, 10);
return Mapper.Map<List<Product>, List<ProductViewModel>>(filteredProducts);
The OrderBy
is mandatory for PagedList
.
BE CAREFUL WITH AUTOMAPPER
Consider the following scenario. What if your Product
entity had a child relationship with ProductReview
(a ICollection<ProductReview>
) like this:
public class ProductReview
{
public int ProductId { get; set; }
public string Description { get; set; }
public int ReviewerId { get; set; }
public double Score { get; set; }
}
public class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public virtual ICollection<ProductReview> Reviews { get; set; }
}
...and your ProductViewModel
had an int
property ReviewsCount
to show in your view?
When Automapper
would map and transform your entities into view model, it would access the Reviews
property of each Product in the List (let's suppose, 10 in your case), one by one, and get the Reviews.Count()
to fill ReviewsCount
in your ProductViewModel
.
Considering my example, where I never eager loaded Reviews
of products, if Lazy Load was on, AutoMapper
would execute ten queries (one per product) to count Reviews
. Count is a fast operation and ten products are just a few. but if instead of count you were actually mapping the ProductReview
to a ProductReviewViewModel
, this would be kinda heavy. If Lazy Load was turned off, we would get an exception, since Reviews
would be null
.
One possible solution, is to eager load all child you might need during mapping, like this:
List<Product> filteredProducts = dbContext.Products.Include("Reviews").OrderBy(p => p.ProductId).ToPagedList(pageNumber, 10);
return Mapper.Map<List<Product>, List<ProductViewModel>>(filteredProducts);
...so 10 products and their reviews would be retrieved in just one query, and no other queries would be executed by AutoMapper.
But.......I just need a count, do I really need to retrieve ALL Reviews
just to avoid multiple queries?
Isn't it also heavy to load all reviews and all their expensive fields like Description
which may have thousands of characters???
Yes, absolutely. Avoid mixing PagedList
with AutoMapper
for these scenarios.
Just do a projection like this:
List<Product> filteredProducts = dbContext.Products
.Select(p => new ProductViewModel
{
ProductId = p.ProductId,
ProductName = p.Name,
ProductDescription = p.Description,
ReviewsCount = p.Reviews.Count(),
ScoreAverage = p.Reviews.Select(r => r.Score).DefaultIfEmpty().Average()
})
.OrderBy(p => p.ProductId).ToPagedList(pageNumber, 10);
Now you are loading your 10 products, projecting them into ProductViewModel
, calculating the Reviews
count and score average, without retrieving all Reviews
from database.
Of course there are scenarios where you might really need all child entities loaded/materialized, but other than that, projection ftw.
You can also put the Select()
part inside an extension class, and encapsulate all your projections in extension methods, so you can reuse them like you would to with AutoMapper
.
I'm not saying AutoMapper
is evil and you shouldn't use it, I use it myself in some situations, you just need to use it when it's appropriate.
EDIT: AUTOMAPPER DOES SUPPORT PROJECTION
I found this question where @GertArnold explains the following about AutoMapper
:
...the code base which added support for projections that get translated
into expressions and, finally, SQL
So be happy, just follow his suggestion.