C
C#•10mo ago
Daryl

Blazor Pages rendering before async methods finish, breaking the page?

The below code is crashing my app because applicationUser is null when it's referenced in the HTML. This is a common issue I keep facing, how are you meant to handle it?
protected override async Task OnInitializedAsync()
{
while(userId == null)
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}

transactions = DataService.GetAllTransactions(userId);
mostExpensiveTransactions = transactions.OrderByDescending(t => t.Amount).Take(5).ToList();

while(applicationUser == null)
{
applicationUser = await UserManager.FindByIdAsync(userId);
}
}
protected override async Task OnInitializedAsync()
{
while(userId == null)
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}

transactions = DataService.GetAllTransactions(userId);
mostExpensiveTransactions = transactions.OrderByDescending(t => t.Amount).Take(5).ToList();

while(applicationUser == null)
{
applicationUser = await UserManager.FindByIdAsync(userId);
}
}
I thought the while loop would ensure that it can never continue but I guess it continues because it's returning a Task? ChatGPT suggests I added a Delay of 200ms after the command but it doesn't really feel like it would guarantee the issue is solved. Thanks!
95 Replies
mg
mg•10mo ago
it won't, and that's why you shouldn't be using chatgpt you need to conditionally render based on your state check if it's null in your markup and only render the stuff that depends on it if it's not null
Daryl
DarylOP•10mo ago
Ahhh so in the HTML itself put the @if(applicationUser == null) bit to ensure it can never do it?
mg
mg•10mo ago
yep my components are full of if (not null) { stuff } else { loading icon } if you want you could make a wrapper component for that maybe there's one built in idk
Daryl
DarylOP•10mo ago
Perfect thank you! I should've thought of that, it was a demo in the Weather Forecast class... Something I don't quite understand is how it realises the variable is now no longer null so re-runs the page/component?
mg
mg•10mo ago
mg
mg•10mo ago
this could help
Daryl
DarylOP•10mo ago
Thanks, will give it a read 🙂 Thank you!
Jimmacle
Jimmacle•10mo ago
also that DB access should definitely be async
Daryl
DarylOP•10mo ago
I was hoping if I didn't have them async I would be able to avoid this issue. However, there doesn't seem to be a UserManager.FindById(), it has to be aysnc.
Jimmacle
Jimmacle•10mo ago
if they aren't async your page is going to freeze while it's talking to the database
mg
mg•10mo ago
if you overrode the sync oninitialized method I think it would fix the problem but you don't want that because it introduces a much worse problem
Jimmacle
Jimmacle•10mo ago
there is no reason not to use async options for web stuff, especially blazor
Daryl
DarylOP•10mo ago
You're right, freezing the page when it's talking to the DB isn't a great idea. I'll sort that out. One more question - and this may be a very stupid one - But is there any easy way to get data from the Server to the Client? For example, I wanted the Server to query the DB, get a list of bank transactions and then send them to the client so the client can manipulate them locally and push them back to the server when complete. That way the strain on the server is minimal. The only thing I could find was to create an API endpoint and somehow get the auth token to the Client but it seemed over kill
Angius
Angius•10mo ago
Also, don't try to "avoid the issue" of async code. It's not an issue.
so the client can manipulate them locally
You want the user to be able to just do anything?
Jimmacle
Jimmacle•10mo ago
if you have a server and client you'll need an API for them to communicate and some kind of authorization to protect it
Daryl
DarylOP•10mo ago
My thought process would be: User signs in, server downloads the transactions for that user to the client. User can then edit the data in their browser, hit submit and it'll push the changes to the server which can then update the DB.
Jimmacle
Jimmacle•10mo ago
but i suspect you're using blazor server considering you're accessing the DB directly from your components
Angius
Angius•10mo ago
Browser has exactly zero security, just FYI If you let the user edit data however, they will be able to edit it however Send a request to the backend that describes the changes Validate that the changes are... valid Perform them if so
Daryl
DarylOP•10mo ago
That's fine, the server could validate the changes before pushing to the database, that wouldn't really be an issue. To be honest I've gone off the idea anyway because I see how smooth these websockets will make stuff but I'm still curious to know how you'd do it This is the bit I was struggling with: I could get the user to sign in using the server side and I could access the API in the browser which would print out lots of json output. However, if I tried to do any sort of HttpClient request from the client side it failed because it didn't seem to have access to the auth token. Since I've had that issue I don't really know why I would want client side things anymore if I can't get my db data to it... Again, could be me doing something stupid!
Jimmacle
Jimmacle•10mo ago
that will depend on how your authentication system works
Daryl
DarylOP•10mo ago
When I created the project I set up Identity and then configured Microsoft SSO, if that helps at all 😕
Jimmacle
Jimmacle•10mo ago
you're most likely doing it wrong because that's how plenty of SPAs do it
Daryl
DarylOP•10mo ago
What's an SPA sorry?
Jimmacle
Jimmacle•10mo ago
single page application the kind of webapp that needs access to the token itself
Daryl
DarylOP•10mo ago
Any ideas where I may have gone wrong? I read somewhere I should just create a HttpClient on the client side. I realise it's usually best practice but they said the browser would be managing the ports anyway. So I did that but then obviously it doesn't naturally emed the tokens. At the time I couldn't for the life of me get the server-side httpclient to inject into the client side one. The site would load but when I tried to do things it would error saying the HttpClient I had injected was null.
Jimmacle
Jimmacle•10mo ago
it really depends on your setup, whether you have cross site cookies, http only cookies, cors issues, etc
Daryl
DarylOP•10mo ago
No worries, I gave up in the end and started again anyway!
Jimmacle
Jimmacle•10mo ago
not something i personally have a lot of experience with other than doing it wrong myself
Daryl
DarylOP•10mo ago
Fair enough! 🙂 Thank you though! I appreciate it
mg
mg•10mo ago
you can modify the database in the same way that you query it unless I'm misunderstanding something just like you have some code in your initialization method to get the data, you can have a form or something with a handler that updates the data
Daryl
DarylOP•10mo ago
Sorry i went to bed. So the idea was the user would make changes locally, submit it to the server which would rub some validation and submit it if ok. The problem I had was I couldn't even get the data to the client. It just didn't seem to have any knowledge that I was authenticated
mg
mg•10mo ago
is this blazor server?
Daryl
DarylOP•10mo ago
yeah
mg
mg•10mo ago
with blazor server you're authenticated once you load the app wdym when you say it didn't have any knowledge you were authenticated
Jimmacle
Jimmacle•10mo ago
yeah that's what was confusing me with blazor server the C# code is running on the server already you don't need a http client because your code is running in the same process as the rest of your server
mg
mg•10mo ago
one of the main benefits of blazor server; you don't need to authenticate every request "request" used loosely, as there's really only the initial requests before you establish the signalR connection
Jimmacle
Jimmacle•10mo ago
yeah, you don't need a formal web API at all because there's no reason to use one i send all my "requests" through mediatr so there's still decoupling but no actual http requests need to be involved
Daryl
DarylOP•10mo ago
This is all exactly why I got really confused. I had logged in, I was authenticated, I wanted to query the DB and return the resuts (List<Transactions> to the client so it could manipulate the data and I just couldn't get it to the client. Then when I googled it more it was suggested I needed to use an API to bridge the two I thought the server and the client could work together but I couldn't get data from one to the other. But maybe I did something really stupid
mg
mg•10mo ago
that suggestion is wrong in the context of blazor server
Daryl
DarylOP•10mo ago
This wasn't just blazor server. This was a Blazor solution which creates the Server and Client projects
mg
mg•10mo ago
that sounds like blazor webassembly though i think that still just creates one project? either way, in blazor server, you do not need a separate api
Daryl
DarylOP•10mo ago
two seconds opening up VS to check
Daryl
DarylOP•10mo ago
Blazor Web App. Not WebAssembly.
No description
Daryl
DarylOP•10mo ago
What complicates it more is latest version of Blazor doesn't seem to be very well documented just yet. Or wasn't a month ago when I was trying this.
mg
mg•10mo ago
looks like "Blazor Web App" is webassembly that's annoying
Daryl
DarylOP•10mo ago
I'm not sure I understand, it's got a server side so should do both?
mg
mg•10mo ago
mg
mg•10mo ago
Blazor Server and Blazor WebAssembly are two distinct types of application Blazor Server runs all your C# code on the server and sends UI updates to the client over a SignalR (websocket) connection Blazor WebAssembly runs all your C# code on the client via WebAssembly and relies on HTTP requests to interact with the server, like a typical SPA
Daryl
DarylOP•10mo ago
Yeah and I think that may be hte issue I had with webapp. I was having to send new HTTP Request to the server so I had to have an API?
mg
mg•10mo ago
Yes, with Blazor WebAssembly, you need an API
Daryl
DarylOP•10mo ago
Yeah... That's where I got to previously. I was hoping there was some way I could go between the browser and the server without an API but maybe it's just the Render Modes
mg
mg•10mo ago
That's exactly what Blazor Server is lol
Daryl
DarylOP•10mo ago
so what situation would you maybe one the server/client one?
mg
mg•10mo ago
read this It explains all of the benefits and limitations of each hosting model
Daryl
DarylOP•10mo ago
ok will do cheers
Jimmacle
Jimmacle•10mo ago
render modes don't apply to wasm, only server wasm webapps are always fully interactive
Daryl
DarylOP•10mo ago
Sorry, another question about handling async calls. It was mentioned yesterday that I should be putting the logic in the HTML side (so if accounts == null say loading). That way the component will automatically update when this is populated. However, what do I do if I need to do two async calls? In the below example, I need to get the AuthenticationState before I can determine the user and userId variables. If they fire too earlier then accounts will be null.
protected override async Task OnInitializedAsync()
{
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authenticationState.User;
var userId = userManager.GetUserId(user); // Get the user's ID
accounts = await DataService.GetAllUserAccountsAsync(userId);
}
protected override async Task OnInitializedAsync()
{
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authenticationState.User;
var userId = userManager.GetUserId(user); // Get the user's ID
accounts = await DataService.GetAllUserAccountsAsync(userId);
}
Or do I get the user in the code and then trigger a second method in the HTML? This is what I'm currently doing but not sure if it's the best way? Note how I wait until applicationState isn't null to call the next method.
<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

@if(authenticationState == null)
{
<span>Loading...</span>
}
else
{
RetrieveAccounts();
if(accounts != null)
{
foreach(var account in accounts)
{
<div>@account.Name</div>
}
}
}

@code {
List<Account> accounts;
List<Transaction> transactions;
List<Merchant> merchants;
AuthenticationState authenticationState;

protected override async Task OnInitializedAsync()
{
authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
}

public async void RetrieveAccounts()
{
var user = authenticationState.User;
var userId = userManager.GetUserId(user); // Get the user's ID
accounts = await DataService.GetAllUserAccountsAsync(userId);
}
}
<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

@if(authenticationState == null)
{
<span>Loading...</span>
}
else
{
RetrieveAccounts();
if(accounts != null)
{
foreach(var account in accounts)
{
<div>@account.Name</div>
}
}
}

@code {
List<Account> accounts;
List<Transaction> transactions;
List<Merchant> merchants;
AuthenticationState authenticationState;

protected override async Task OnInitializedAsync()
{
authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
}

public async void RetrieveAccounts()
{
var user = authenticationState.User;
var userId = userManager.GetUserId(user); // Get the user's ID
accounts = await DataService.GetAllUserAccountsAsync(userId);
}
}
`
mg
mg•10mo ago
That's not what you want to do HTML is markup, not programming Honestly I'm not even sure what putting RetrieveAccounts(); in there like that does The simple way to think about it is just "don't try to render things that are null"
Daryl
DarylOP•10mo ago
It is working... But I don't know how else I could do it. If I put it all in the OnInitialize then there's a risk it'll try to get the accounts but the userprincipal is null because async
mg
mg•10mo ago
You want to render a list of accounts. You can't do that if the list of accounts is null. What do you think the only thing you need to null check is?
Daryl
DarylOP•10mo ago
I don't know, lol... The issue I have is to get the accounts I have to get the UserID so I can do a LINQ query. So I have to check the user has been retrieved from authenticationState first and then I can pass through the data to the service which gets the accounts. So Ithink have to check each one isn't null and call them one after the other... It just feels messy I guess what I'm saying is I can't get the accounts without the user details and that's async too. That's where I'm tripping up.
Jimmacle
Jimmacle•10mo ago
async void :bonk: especially bad in blazor, pretty sure it's completely illegal in wasm
Daryl
DarylOP•10mo ago
Sorry what do you mean?
Jimmacle
Jimmacle•10mo ago
you basically never want to use async void
Daryl
DarylOP•10mo ago
*sorry
Jimmacle
Jimmacle•10mo ago
if you want to load your accounts after you get the authentication state, just do it in OnInitializedAsync
protected override async Task OnInitializedAsync()
{
authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
await RetrieveAccounts();
}

public async Task RetrieveAccounts()
{
var user = authenticationState.User;
var userId = userManager.GetUserId(user); // Get the user's ID
accounts = await DataService.GetAllUserAccountsAsync(userId);
}
protected override async Task OnInitializedAsync()
{
authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
await RetrieveAccounts();
}

public async Task RetrieveAccounts()
{
var user = authenticationState.User;
var userId = userManager.GetUserId(user); // Get the user's ID
accounts = await DataService.GetAllUserAccountsAsync(userId);
}
Daryl
DarylOP•10mo ago
Maybe I've misunderstood async then I thought:
await MyFunction();
Console.WriteLine("Cheese");
await MyFunction();
Console.WriteLine("Cheese");
"Cheese" would print before MyFunction() may have finished?
Jimmacle
Jimmacle•10mo ago
no await specifically waits until the async call has completed
Daryl
DarylOP•10mo ago
So the method pauses/waits at the await but lets the method invoker continue?
Jimmacle
Jimmacle•10mo ago
essentially the method pauses but the underlying thread is free to do other work
Daryl
DarylOP•10mo ago
but "other work" is not anything to do with the function we're in
Jimmacle
Jimmacle•10mo ago
it could be any other tasks scheduled on the thread pool
Daryl
DarylOP•10mo ago
Thread Pools bit out of my league at the moment but it sounds like that's where I've been tripping up. I thought it would carry on because a Task has been returned, even if it's technically an empty task for now.
Jimmacle
Jimmacle•10mo ago
it would carry on, if you didn't await the task being returned
Daryl
DarylOP•10mo ago
AH lightbulb moment
Jimmacle
Jimmacle•10mo ago
e.g.
var task = AsyncMethod();
Console.WriteLine("Cheese");
await task;
var task = AsyncMethod();
Console.WriteLine("Cheese");
await task;
Daryl
DarylOP•10mo ago
You can wait a variable?
Jimmacle
Jimmacle•10mo ago
you can await tasks async methods return tasks, unless you use async void (which is one of the reasons you should never do that)
Daryl
DarylOP•10mo ago
So in the example you just gave: - task is assigned an empty task
Jimmacle
Jimmacle•10mo ago
no
Daryl
DarylOP•10mo ago
I pushed enter too early
Jimmacle
Jimmacle•10mo ago
task is assigned a task representing the async work AsyncMethod is doing it's not empty
Daryl
DarylOP•10mo ago
Ok sorry just bare with me here, I'm struggling as it is without getting the terminology exact So in the example you just gave: - task is assigned a task but that represents the async work - You then print cheese - Then you are waiting for the task to finish and then I guess the variable is changed from a task to the actual result?
Jimmacle
Jimmacle•10mo ago
the variable is always a task, tasks have a completion state and optionally a result object
Daryl
DarylOP•10mo ago
Ok, good to know. I always found it interesting that you can access the data as normal afterwards.
Jimmacle
Jimmacle•10mo ago
you could var result = await task; or something to that effect the problem with async void is that you can't await it, so your code would actually be trying to render a list that you may not have received yet it also has other implications for handling exceptions
Daryl
DarylOP•10mo ago
Yeah that makes sense. I don't really need that method at all now I understand it a bit better. My big mistake was thinking the method would continue processing Thanks!
Jimmacle
Jimmacle•10mo ago
also as a side tip, it is generally better to separate your presentation logic and business logic what you have is close but it seems to be doing extra authentication related stuff in order to set up the data service (idk what it does, maybe it's needed maybe it isn't)
Daryl
DarylOP•10mo ago
If you were writing an application with a Database and the User should only see their Accounts, how would you implement that? I seem to have to reverse-engineer their claims to get their userId and then I can do a Linq Query based on it.
Jimmacle
Jimmacle•10mo ago
yes, you'd pull it out of the authenticated user's claims but you don't have to do that in a component, you could do that in the service itself
Daryl
DarylOP•10mo ago
Oh interesting, I didn't realise the service could access that data I just assumed it would have to be on the page.
Jimmacle
Jimmacle•10mo ago
with blazor server all your C# is running on the server in a single service scope per tab so anything you get from DI can resolve any other services you've registered
Daryl
DarylOP•10mo ago
Ok great, thank you. I hadn't considered that the cookies etc can be accessed by services. That'll make things a lot easier Another question, I changed a method to it's async version but I'm getting an error
authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authenticationState.User;
var userId = await userManager.GetUserIdAsync(user);
authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authenticationState.User;
var userId = await userManager.GetUserIdAsync(user);
The error I'm getting is: Argument 1: cannot convert from 'System.Security.Claims.ClaimsPrincipal' to 'MyApp.Data.ApplicationUser' But this seemed to be fine when I wasn't doing the async version
Jimmacle
Jimmacle•10mo ago
the signatures might be different between the sync and async versions i don't use identity so i'm not familiar with those APIs
Daryl
DarylOP•10mo ago
Ok no worries, thank you!
Want results from more Discord servers?
Add your server