For some reason when bringing up the page CampaignPage.razor, OnInitializedAsync()
is being called twice.
In addition, the property ReturnUrl
is null each time (should be the first time) and is null again when HandleValidSubmitAsync()
is called.
The first time it hits it, this is the call stack:
LouisHowe.web.dll!LouisHowe.web.Pages.Manager.CampaignPage.OnInitializedAsync() Line 143 C#
[External Code]
LouisHowe.web.dll!LouisHowe.web.Pages.Pages__Host.ExecuteAsync.AnonymousMethod__23_1() Line 40 C#
[External Code]
And the second time, this is it:
LouisHowe.web.dll!LouisHowe.web.Pages.Manager.CampaignPage.OnInitializedAsync() Line 143 C#
[External Code]
In addition it's 5+ seconds between the two hits and there's very little the system is doing to initialize the page. It hits the database but that's Sql Server Express sitting on my computer doing nothing else.
This only happens for case of mode == read or update. And in that case, in OnInitializedAsync()
it is calling the following code. So it's something in this code that is causing the issue.
Note that it calls the same dbContext to get Countries.All()
for the case of mode == create and that does not have the problem. So it's something in the call to Campaigns.FindByNames()
that's doing it - but how?
await using (var dbContext = await NoTrackingDbFactory.CreateDbContextAsync())
{
// 1. if no uniqueId then create mode (because what would we Read/Update)?
if (string.IsNullOrEmpty(UniqueId))
ModeState = Mode.Create;
else
{
// 2. if have a uniqueId then it can't be create.
if (ModeState == Mode.Create)
ModeState = Mode.Read;
// 3. Read it in from the database.
_campaign = await dbContext.Campaigns.FindByNames(string.Empty,
UniqueId, CampaignIncludes.All).FirstOrDefaultAsync();
if (_campaign == null)
{
_badUniqueId = true;
ModeState = Mode.Read;
return;
}
Model.Campaign = _campaign;
}
var list = new List<IOrganization>();
list.AddRange(await dbContext.Countries.All().ToListAsync());
list.Add(Country.Dashes);
list.AddRange(await dbContext.States.All().ToListAsync());
AllRegions = list;
}
And here's FindByNames
public static IQueryable<Campaign> FindByNames(this IQueryable<Campaign> source, string searchName,
string searchUniqueId, params Expression<Func<Campaign, object?>>[] includes)
{
var query = source;
if (searchName.Length != 0)
query = query.Where(campaign => campaign.Name.Contains(searchName));
if (searchUniqueId.Length != 0)
query = query.Where(campaign => campaign.UniqueId.Contains(searchUniqueId));
foreach (var include in includes)
query = query.Include(include);
return query.OrderBy(i => i.Name);
}
So, what would cause the long delay and the multiple calls?
So I did as @MrCakaShaunCurtis suggested and put a debug entering and leaving OnInitializedAsync()
and here's what I get.
Only once case (mode == create):
enter OnInitializedAsync() CampaignPage Instance a69f0a2d-e36a-4297-b8a1-d8d9cd12bbd1
... 2 expected DB calls
leave OnInitializedAsync() CampaignPage Instance a69f0a2d-e36a-4297-b8a1-d8d9cd12bbd1
enter OnAfterRenderAsync() CampaignPage Instance a69f0a2d-e36a-4297-b8a1-d8d9cd12bbd1
leave OnAfterRenderAsync() CampaignPage Instance a69f0a2d-e36a-4297-b8a1-d8d9cd12bbd1
enter OnAfterRenderAsync() CampaignPage Instance a69f0a2d-e36a-4297-b8a1-d8d9cd12bbd1
leave OnAfterRenderAsync() CampaignPage Instance a69f0a2d-e36a-4297-b8a1-d8d9cd12bbd1
And for the calls twice case:
enter OnInitializedAsync() CampaignPage Instance 427cd0b9-bf07-41d7-924f-0355da16b441
... 4 (FindByNames query becomes 2 calls) expected DB calls
leave OnInitializedAsync() CampaignPage Instance 427cd0b9-bf07-41d7-924f-0355da16b441
Microsoft.EntityFrameworkCore.Database.Command: Information: Executed DbCommand (2ms) [Parameters=[@__userid_0='?' (Size = 450)], CommandType='Text', CommandTimeout='30']
SELECT TOP(1) [a].[Id], [a].[AccessFailedCount], [a].[ConcurrencyStamp], [a].[Email], [a].[EmailConfirmed], [a].[LockoutEnabled], [a].[LockoutEnd], [a].[NormalizedEmail], [a].[NormalizedUserName], [a].[PasswordHash], [a].[PhoneNumber], [a].[PhoneNumberConfirmed], [a].[SecurityStamp], [a].[TwoFactorEnabled], [a].[UserName]
FROM [AspNetUsers] AS [a]
WHERE [a].[Id] = @__userid_0
enter OnInitializedAsync() CampaignPage Instance 2b5a7537-85cc-4bca-9b29-403f8cdba668
... 4 calls again
leave OnInitializedAsync() CampaignPage Instance 2b5a7537-85cc-4bca-9b29-403f8cdba668
'LouisHowe.web.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.5\System.Web.HttpUtility.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
enter OnAfterRenderAsync() CampaignPage Instance 2b5a7537-85cc-4bca-9b29-403f8cdba668
leave OnAfterRenderAsync() CampaignPage Instance 2b5a7537-85cc-4bca-9b29-403f8cdba668
'LouisHowe.web.exe' (CoreCLR: clrhost): Loaded 'C:\git\LouisHowe\LouisHowe.web\bin\Debug\net7.0\Newtonsoft.Json.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
enter OnAfterRenderAsync() CampaignPage Instance 2b5a7537-85cc-4bca-9b29-403f8cdba668
leave OnAfterRenderAsync() CampaignPage Instance 2b5a7537-85cc-4bca-9b29-403f8cdba668
enter OnAfterRenderAsync() CampaignPage Instance 2b5a7537-85cc-4bca-9b29-403f8cdba668
leave OnAfterRenderAsync() CampaignPage Instance 2b5a7537-85cc-4bca-9b29-403f8cdba668
So it's calling the Identity DB to get the AspNetUsers
user record. It generally does that before each call to OnInitializedAsync()
, I assume to initialize the value in the Task<AuthenticationState>
cascading parameter.
So it's re-initializing, before ever getting to OnAfterRenderAsync()
.
I made the change @MarvinKlein suggested (2 places) - that fixed it. I now have:
@page "/"
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using LouisHowe.web
@namespace LouisHowe.web.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Copyright (c) 2023 by David Thielen - ALL RIGHTS RESERVED -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<base href="~/"/>
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css"/>
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet"
asp-append-version="true"/>
<link href="css/site.css" rel="stylesheet" asp-append-version="true"/>
<link href="css/menu.css" rel="stylesheet" asp-append-version="true"/>
<link href="css/header.css" rel="stylesheet" asp-append-version="true"/>
<link rel="stylesheet" href="css/dx-demo.css" asp-append-version="true">
<link rel="stylesheet" href="css/dx-demo-pages.css" asp-append-version="true">
<link href="LouisHowe.web.styles.css" rel="stylesheet"/>
<link rel="icon" type="image/png" href="favicon.png"/>
<script src="~/js/Interop.js"></script>
<component type="typeof(HeadOutlet)" render-mode="Server"/>
</head>
<body>
@{
var initialTokenState = new Services.InitialApplicationState
{
XsrfToken = Xsrf.GetAndStoreTokens(HttpContext).RequestToken,
Cookie = HttpContext.Request.Cookies[".AspNetCore.Cookies"]
};
}
<component type="typeof(App)" render-mode="Server" param-InitialState="initialTokenState" />
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
And this is explained here. Which leads to another question that I'll post separately.
This answer was provided by @MarvinKlein above in a comment.
In _Host.cshtml
set render-mode="Server"
(two locations) and then OnInitializedAsync()
will only be called once.