diff --git a/Wave/Components/Pages/ArticleView.razor b/Wave/Components/Pages/ArticleView.razor index 2363bd5..bb7770b 100644 --- a/Wave/Components/Pages/ArticleView.razor +++ b/Wave/Components/Pages/ArticleView.razor @@ -164,25 +164,22 @@ } protected override void OnInitialized() { + using var context = ContextFactory.CreateDbContext(); + var query = context.Set
() + .IgnoreQueryFilters().Where(a => !a.IsDeleted) + .Include(a => a.Author) + .Include(a => a.Reviewer) + .Include(a => a.Categories); + // We need blocking calls here, bc otherwise Blazor will execute Render in parallel, // running into a null pointer on the Article property and panicking if (Id is not null) { - using var context = ContextFactory.CreateDbContext(); - Article = context.Set
() - .IgnoreQueryFilters().Where(a => !a.IsDeleted) - .Include(a => a.Author) - .Include(a => a.Reviewer) - .Include(a => a.Categories) - .FirstOrDefault(a => a.Id == Id); + Article = query.AsSingleQuery().FirstOrDefault(a => a.Id == Id); } else if (Date is { } date && Title is { } title) { - using var context = ContextFactory.CreateDbContext(); string? slug = TitleEncoded == null ? null : Uri.EscapeDataString(TitleEncoded); - Article ??= context.Set
() - .IgnoreQueryFilters().Where(a => !a.IsDeleted) - .Include(a => a.Author) - .Include(a => a.Reviewer) - .Include(a => a.Categories) - .FirstOrDefault(a => a.PublishDate.Date == date.Date && (slug != null && a.Slug == slug || a.Title.ToLower() == title)); + Article = query.AsSingleQuery().FirstOrDefault(a => + a.PublishDate.Date == date.Date + && (slug != null && a.Slug == slug || a.Title.ToLower() == title)); } } diff --git a/Wave/Components/Pages/CategoryView.razor b/Wave/Components/Pages/CategoryView.razor index d150ab8..2e4a7ce 100644 --- a/Wave/Components/Pages/CategoryView.razor +++ b/Wave/Components/Pages/CategoryView.razor @@ -42,10 +42,22 @@ protected override async Task OnInitializedAsync() { await using var context = await ContextFactory.CreateDbContextAsync(); string category = WebUtility.UrlDecode(CategoryName); - await context.Set().LoadAsync(); + var now = DateTimeOffset.UtcNow; + // First load Category with simple chain and manual filters, as to minimize + // filter redundancy and query complexity (category -> Articles -> Author is linear) Category = await context.Set() - .Include(c => c.Articles).ThenInclude(a => a.Author) - .Include(c => c.Articles).ThenInclude(a => a.Categories) + .IgnoreAutoIncludes().IgnoreQueryFilters() + .Include(c => c.Articles.Where(a => !a.IsDeleted && a.PublishDate <= now)) + .ThenInclude(a => a.Author) .FirstOrDefaultAsync(c => c.Name == category); + // Load all the other categories missing on the articles, by loading all relevant + // articles ID with their categories, so EF can map them to the already loaded entries + // (again manual filter to minimize redundancy) + await context.Set
() + .IgnoreAutoIncludes().IgnoreQueryFilters() + .Where(a => !a.IsDeleted && a.PublishDate <= now && a.Categories.Contains(Category!)) + .Select(a => new { + a.Id, a.Categories + }).LoadAsync(); } } diff --git a/Wave/Components/Pages/Home.razor b/Wave/Components/Pages/Home.razor index 9976a33..6a2ca53 100644 --- a/Wave/Components/Pages/Home.razor +++ b/Wave/Components/Pages/Home.razor @@ -161,11 +161,14 @@ try { await using var context = await ContextFactory.CreateDbContextAsync(); - var query = context.Set
() - .Include(a => a.Author).Include(a => a.Categories) - .OrderByDescending(a => a.PublishDate).ThenBy(a => a.Id); + var query = context.Set
(); - Featured = await query.FirstOrDefaultAsync(); + Featured = await query + .Include(a => a.Author) + .Include(a => a.Categories) + .OrderByDescending(a => a.PublishDate).ThenBy(a => a.Id) + .AsSplitQuery() + .FirstOrDefaultAsync(); TotalPages = (int) Math.Max(Math.Ceiling((await query.CountAsync() - 1) / 10.0), 1); } catch { Message.ShowError(Localizer["Articles_Load_Error"]); @@ -178,6 +181,7 @@ return await context.Set
() .Include(a => a.Author).Include(a => a.Categories) .OrderByDescending(a => a.PublishDate).ThenBy(a => a.Id) + .AsSplitQuery() .Skip(page + 1).Take(count).ToListAsync(); } catch { Message.ShowError(Localizer["Articles_Load_Error"]); diff --git a/Wave/Components/Pages/UserView.razor b/Wave/Components/Pages/UserView.razor index a320fbf..bb9f15c 100644 --- a/Wave/Components/Pages/UserView.razor +++ b/Wave/Components/Pages/UserView.razor @@ -93,13 +93,20 @@ // Find user if (Id is not null) { - User = await context.Users.Include(u => u.Articles) - .ThenInclude(a => a.Categories) + var now = DateTimeOffset.UtcNow; + User = await context.Users + .IgnoreAutoIncludes().IgnoreQueryFilters() + .Include(u => u.Articles.Where(a => !a.IsDeleted && a.PublishDate <= now)) .FirstOrDefaultAsync(u => u.Id == Id.ToString()); + await context.Set
() + .Where(a => a.Author.Id == Id.ToString()) + .Select(a => new { + a.Id, a.Categories + }).LoadAsync(); } // Validate access to user - if (User is not null && User.Articles.Count > 0) { + if (User is not null && (User.Articles.Count > 0 || HttpContext.User.IsInRole("Admin"))) { } else if (User is not null && HttpContext.User.FindFirst("Id")?.Value == User.Id) { Message.ShowWarning(Localizer["ProfileNotPublic_Message"]); } else {