I've been trying to fix this performance bottleneck in my web app for a while now.
Here is what the data looks like on my web page:
Here is how I get this data in the controller:
var allCategories = categoryRepository
.Categories
.Select(x => new
{
x.CategoryID,
x.FriendlyName,
x.RoutingName,
x.ParentID
})
.ToList();
var allListings = listingRepository
.Listings
.Where(x => x.Lister.Status != cancelledStatus)
.Select(x => new
{
x.SelectedCategoryOneID,
x.SelectedCategoryTwoID,
x.SelectedCategoryThreeID,
})
.ToList();
List<CategoryBrowseIndexViewModel> categories =
allCategories
.Where(x => x.ParentID == null)
.Select(a => new CategoryBrowseIndexViewModel
{
CategoryID = a.CategoryID,
FriendlyName = a.FriendlyName,
RoutingName = a.RoutingName,
ListingCount = allListings
.Where(x => x.SelectedCategoryOneID == a.CategoryID)
.Count(),
BrowseCategoriesLevelTwoViewModels =
allCategories
.Where(x => x.ParentID == a.CategoryID)
.Select(b => new BrowseCategoriesLevelTwoViewModel
{
CategoryID = b.CategoryID,
FriendlyName = b.FriendlyName,
RoutingName = b.RoutingName,
ParentRoutingName = a.RoutingName,
ListingCount = allListings
.Where(x => x.SelectedCategoryTwoID == b.CategoryID)
.Count(),
BrowseCategoriesLevelThreeViewModels =
allCategories
.Where(x => x.ParentID == b.CategoryID)
.Select(c => new BrowseCategoriesLevelThreeViewModel
{
CategoryID = c.CategoryID,
FriendlyName = c.FriendlyName,
RoutingName = c.RoutingName,
ParentRoutingName = b.RoutingName,
ParentParentID = a.CategoryID,
ParentParentRoutingName = a.RoutingName,
ListingCount = allListings
.Where(x => x.SelectedCategoryThreeID == c.CategoryID)
.Count()
})
.OrderBy(x => x.FriendlyName)
.ToList()
})
.OrderBy(x => x.FriendlyName)
.ToList()
})
.OrderBy(x => x.FriendlyName == jobVacanciesFriendlyName)
.ThenBy(x => x.FriendlyName == servicesLabourHireFriendlyName)
.ThenBy(x => x.FriendlyName == goodsEquipmentFriendlyName)
.ToList();
I make two database calls to get what I need into memory, and construct the data model in memory. I use nested foreach loops in the view to display the categories and the listing counts.
What I want to do is make one database call, have the database do all the work and send back the entire object model, in one go. Can this be done?
My repositories use IQueryable
, like this:
public class EFListingRepository : IListingRepository
{
private EFDbContext context = new EFDbContext();
IQueryable<Listing> Listings { get { return context.Listings; } }
}
To do one database call I guess I would have to use one dbContext, like this:
EFDbContext browseControllerDbContext = new EFDbContext();
var allCategories = browseControllerDbContext
.Categories
.Select(x => new
{
x.CategoryID,
x.FriendlyName,
x.RoutingName,
x.ParentID
});
var allListings = browseControllerDbContext
.Listings
.Where(x => x.Lister.Status != cancelledStatus)
.Select(x => new
{
x.SelectedCategoryOneID,
x.SelectedCategoryTwoID,
x.SelectedCategoryThreeID,
});
But I get stuck at this point. How do I make a single call to the database to populate the List<CategoryBrowseIndexViewModel> categories
variable, and have everything inside it loaded into memory in one go, ready to be looped through?
The Category entity looks like this:
public class Category
{
public long CategoryID { get; set; }
public string FriendlyKey { get; set; }
public string FriendlyName { get; set; }
public string RoutingKey { get; set; }
public string RoutingName { get; set; }
public long? ParentID { get; set; }
}
The Listing entity is similar in that it's just a (large) class with simple properties, as above. Category and Listing are not related by navigation properties or a foreign key.
Here are the viewmodels, which are nested. They're looped over with nested foreach's in the view:
The CategoryBrowseIndexViewModel looks like this:
public class CategoryBrowseIndexViewModel
{
public List<BrowseCategoriesLevelTwoViewModel> BrowseCategoriesLevelTwoViewModels { get; set; }
public long CategoryID { get; set; }
public string FriendlyName { get; set; }
public string RoutingName { get; set; }
public string ParentRoutingName { get; set; }
public long? ParentID { get; set; }
public long ListingCount { get; set; }
}
And BrowseCategoriesLevelTwoViewModel
public class BrowseCategoriesLevelTwoViewModel
{
public List<BrowseCategoriesLevelThreeViewModel> BrowseCategoriesLevelThreeViewModels { get; set; }
public long CategoryID { get; set; }
public string FriendlyName { get; set; }
public string RoutingName { get; set; }
public string ParentRoutingName { get; set; }
public long? ParentID { get; set; }
public long ListingCount { get; set; }
}
BrowseCategoriesLevelThreeViewModel
public class BrowseCategoriesLevelThreeViewModel
{
public long CategoryID { get; set; }
public string FriendlyName { get; set; }
public string RoutingName { get; set; }
public string ParentRoutingName { get; set; }
public long? ParentID { get; set; }
public long? ParentParentID { get; set; }
public string ParentParentRoutingName { get; set; }
public long ListingCount { get; set; }
}
EDIT: After testing how long everything was taking using Stopwatch
, from the time the controller was hit to the end of the view, the main source of slowness, unbelievably, was Html.ActionLink()
in a foreach
loop. Hardcoding the <a>
tag sped up this page by about 3-4 seconds. I couldn't believe how slow the Html
helpers are! So I guess the lesson is don't assume the database is the bottleneck! I've checked all the other loops in my codebase. In the controller loops I've been careful not to make repeated database calls, for example I was initially using IEnumerable
instead of IQueryable
without understanding how it worked. I understand immediate execution in linq code (Count
, Average
etc. ) and how it works in IQueryable
. Navigation properties can make repeated calls if you're not careful, even if a list of the main entity has been loaded into memory. Thank you everyone, I've learned alot from your suggestions.
IQueryable
versions ofallCategories
andallListings
in thecategories
query? That should be enough to get it all in one query (if you use one context instance!). Not that that will necessarily be faster. It's easy enough to generate queries that are too large to run efficiently. \$\endgroup\$DbContext.Database.Log
property. \$\endgroup\$