aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

Unsure how to inject HttpClient for InteractiveAuto render mode (RC2)

Open SpaceShot opened this issue 2 years ago • 44 comments

I'm not sure if this is a problem in RC2 or if I'm just too new to InteractiveAuto render mode.

I have a component in the client project which calls back to the server for its api calls since the actual call to the third party api needs to be on the server (auth tokens, secrets, and the like).

Per the documentation in Call a web API in Blazor on the WebAssembly pivot, it says I can register an HttpClient back to the base address with:

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

Then I wrote the component to make the call back to localhost... then the server project happens to have a minimal api route to make the "real" call.

This has worked fine as long as I don't put the component on the home page. What I observe happening is that if I place the component on any "secondary page" and let the home page load, the wasm assembly must be streamed down and a breakpoint on the above line fires.

If I put it on the home page, that breakpoint doesn't fire, but the component is rendered. In OnInitializeAsync() it tries to use the HttpClient to call home but BaseAddress wasn't set, so it fails.

Should components intended to be used with InteractiveAuto look to a different part of the lifecycle?

I think another option was to also register an HttpClient in the server Program.cs, but I can't figure out how to do this so that it is picked up in the initial render (perhaps it is using Blazor Server for that or prerendering... it's auto after all).

I'm not sure this is a bug or I'm ahead of the curve on the documentation for Auto modes. I can provide a sample if this behavior seems like a potential bug.

Note: All components and pages are in the Client project. I am experimenting with the experience for global InteractiveAuto. I only moved Error.razor to server side per known RC2 issue.

SpaceShot avatar Oct 18 '23 18:10 SpaceShot

I think this may be what I was looking for... will try out and update the issue: Register common services

SpaceShot avatar Oct 19 '23 02:10 SpaceShot

You can use an abstraction over httpclient o even a mediator that can use its dependencies depending on the running mode.

CrahunGit avatar Oct 19 '23 09:10 CrahunGit

To utilize AutoMode, you could inject an interface and implement it using the HTTPClient for WASM and EntityFramework (or your preferred data retrieval method or an external API) for the server.

Example:

namespace Application.Common.Interfaces;
public interface IDataLoaderService<T>
{
	Task<HashSet<T>> LoadData();
}

using System.Net.Http.Json;
using Application.Common.Interfaces;

namespace Application.Features.YourTypeTable;

public class HttpYourTypeDataLoaderService(HttpClient _httpClient) : IDataLoaderService<YourType>
{
    public async Task<HashSet<YourType>> LoadData()
    {
        var items = await _httpClient.GetFromJsonAsync<HashSet<YourType>>("yourURL");
        return items ?? [];
    }
}
using Application.Common.Interfaces;
using Application.Db;
using Microsoft.EntityFrameworkCore;

namespace Application.Features.YourTypeTable;

public class DbYourTypeDataLoaderService(ApplicationDbContext _dbContext) : IDataLoaderService<YourType>
{
    public async Task<HashSet<YourType>> LoadData()
    {
        return new HashSet<YourType>(await _dbContext.YourTable.ToListAsync()) ?? [];
    }
}

For server-side registration:

builder.Services.AddDbContext<ApplicationDbContext>();
builder.Services.AddScoped<IDataLoaderService<YourType>, DbYourTypeDataLoaderService>();

For WASM:

builder.Services.AddScoped<IDataLoaderService<YourType>, HttpYourTypeDataLoaderService>();

In the razor page:

    [Inject]
    private IDataLoaderService<YourType> _dataLoaderSevice { get; set; } = null!;
	
     private HashSet<YourType>? _items;
	
     private async Task LoadData() => _items = await _dataLoaderService.LoadData();

    protected override async Task OnInitializedAsync()
    {
        await LoadData();
    }
	

mahald avatar Oct 19 '23 09:10 mahald

The biggest problem I have is that to obtain the baseAddress needed for the callback I need IWebAssemblyHostEnvironment, which is not available server-side. The client side is not being instantiated "in time" because in Auto mode, Blazor is streaming down WASM while trying to render the page without. So the control fails because it has no way to call back into the host.

Register Common Services isn't working for me because I can't pass the WebAssemblyHostBuilder down into a method that would be callable by both server side and client side to register the common service (an HttpClient service).

I feel like most of the documentation (understandably) is still derived from the world where you have chosen Blazor WebAssembly or Blazor Server and here at this intersection of InteractiveAuto, there are gaps in the docs that I can't figure out how to fill in at the moment.

SpaceShot avatar Oct 19 '23 11:10 SpaceShot

To be more clear, this is the service registration I am looking to register in a common way that will work for server-side and client-side and therefore be available for use with InteractiveAuto render mode:

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

SpaceShot avatar Oct 19 '23 11:10 SpaceShot

Added sample on GitHub: https://github.com/SpaceShot/InteractiveAutoSample

SpaceShot avatar Oct 19 '23 18:10 SpaceShot

I'm having the same problem. If you register httpClient in both projects you will be successful, but I don't know if it is the best practice.

Exemple:

To Wasm use: builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

To Server use: builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("http://localhost:port") });

MarcosMusa avatar Oct 19 '23 18:10 MarcosMusa

one way to do it: https://github.com/SpaceShot/InteractiveAutoSample/pull/1

mahald avatar Oct 19 '23 20:10 mahald

one way to do it: SpaceShot/InteractiveAutoSample#1

Why is the API called twice when reloading the page? Put a console log in "WebScraperServiceServer" to reproduce.

MarcosMusa avatar Oct 19 '23 21:10 MarcosMusa

because of prerendering that happens first on the server then wasm is loaded and it's called again.

mahald avatar Oct 19 '23 21:10 mahald

if you want to change this you either inject a service to detect prerendering or use the OnAfterRenderAsync hook instead of the OnInitlializedAsync Hook. Or disable prerendering.

protected override async Task OnAfterRenderAsync(bool firstRender)
 {
     if (firstRender)
     {
         Text = await _webScraper.Get();
         StateHasChanged();
     }
 }

mahald avatar Oct 19 '23 21:10 mahald

PS: The Template you are using is the "Global WASM" Template so everything is running in WASM and you can't use InteractiveServer. What you see is the prerendering that takes place on the Server.

mahald avatar Oct 19 '23 21:10 mahald

Thanks for the pull request @mahald. I'll check it out but I think I see what you are saying. You've created an implementation where if you're on Server Interactivity then it just calls the external service and if you're on WASM then it calls back into the local endpoint. I did use the Global InteractiveAuto template. Mostly I am playing with InteractiveAuto mode to try and understand the nuances like this.

SpaceShot avatar Oct 20 '23 01:10 SpaceShot

Why not use the IHttpClientFactory with a named HttpClient to your third party api?

bcheung4589 avatar Oct 27 '23 03:10 bcheung4589

Hi @bcheung4589 I have been experimenting with InteractiveAuto, although really it can all be applied to InteractiveWebAssembly as well. In this mode, I presented a sample app with a sample call to a third party API. Let's imagine that the API to be called isn't open to the public and I need to protect the keys or credentials or access to that API. I choose not to simply call it from the component, which resides in the Client project of the Blazor solution, because then it would be available to prying eyes of anyone who used the application and had the client DLLs streamed down to their browser.

Therefore, my goal was to implement a sort of backend-for-frontend pattern, where the call was safely protected on the server. In this case, I was calling back into a minimal API in the web server itself. I was then looking for how to make that callback from client and server for both scenarios in InteractiveAuto. Thanks to @mahald, I learned I was thinking about the solution too naively, and that when I am on the server, why not simply make the call. His solution reminded me implementing the call as a service makes that easier and then I get what I want. When using a component with server interactivity, the call is made safely. When using a component with webassembly interactivity, the call is essentially marshaled to the server to make for the component, keeping whatever secrets safe.

In this "new world" of InteractiveAuto, I had been exploring how to perform functions like this, since that world essentially says make your component "client-side ready" and then you're good for whatever happens.

My sample is designed for a small web app where maybe it would be fine to keep it all together as shown. In a larger enterprise app you might still use secure cookies with strict policy so that client components make calls safely to the server and then "forwarded" along to the target. The Duende BFF sample does this for you, but not every app needs enterprise level infrastructure. It depends.

I think the outcome of this issue might be some documentation with InteractiveAuto specific advice, but I don't speak for the team. I'm grateful to @mahald for the sample as it really showed me the way and I'm using the technique now in the real app.

SpaceShot avatar Oct 27 '23 20:10 SpaceShot

I have been experimenting with InteractiveAuto, although really it can all be applied to InteractiveWebAssembly as well. In this mode, I presented a sample app with a sample call to a third party API. Let's imagine that the API to be called isn't open to the public and I need to protect the keys or credentials or access to that API. I choose not to simply call it from the component, which resides in the Client project of the Blazor solution, because then it would be available to prying eyes of anyone who used the application and had the client DLLs streamed down to their browser.

In that case I really do have to recommend using the IHttpClientFactory.

  • Add the httpclient config into appsettings.json
  • Create a IOptions (HttpClientOptions) for your settings.
  • Create a AuthenticateMessageHandler to handle the third-party settings (https://learn.microsoft.com/en-us/aspnet/web-api/overview/advanced/httpclient-message-handlers) and add the handler to the named httpclient. See also usage of BaseAddressAuthorizationMessageHandler for idea's: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios?view=aspnetcore-8.0
  • Inject IHttpClientFactory, and call IHttpClientFactory.CreateClient(HttpClientName) to get a configured HttpClient.

This should work for both modes and isnt Server specific. Keep the appsettings.json ofcourse on server and never client, just a disclaimer.

His solution reminded me implementing the call as a service makes that easier and then I get what I want. When using a component with server interactivity, the call is made safely. When using a component with webassembly interactivity, the call is essentially marshaled to the server to make for the component, keeping whatever secrets safe.

The con is now, that your component is coupled to your server call and not the third-party call. If you need manipulation before sending the request to the third-party, then yes, you need the server as intermediate.

The Duende BFF sample does this for you, but not every app needs enterprise level infrastructure. It depends.

I have removed Duende as dependency in my project, and only use .NET Identity Core (with EF Core ofcourse).

I think the outcome of this issue might be some documentation with InteractiveAuto specific advice, but I don't speak for the team.

There has not been a offered guide/advise by .NET team probably because its so new and they might want to see what we as community do. (just my thought :))

What is important to understand is the conceptuel design of the web app.

1 Component runs once in browser if mode is WebAssembly. 2 Auto Component runs once on Server if AutoMode is on, then as WASM component. (2 calls in total on first load) 2.1 WASM Component calls Server endpoint with HttpClients after pageload. 2.2 You can configure the HttpClient how you like, so you could have component that never calls your own server.

If you would like to visualize this; try this (name as you see fit yourself, this is my personal preference):

Shared project: /Communication/IRenderContext.cs

/// <summary>
/// Provide the render mode information in which the component is rendering.
/// </summary>
public interface IRenderContext
{
    /// <summary>
    /// Rendering from the Client project. Using HTTP request for connectivity.
    /// </summary>
    public bool IsClient { get; }

    /// <summary>
    /// Rendering from the Server project. Using WebSockets for connectivity.
    /// </summary>
    public bool IsServer { get; }

    /// <summary>
    /// Rendering from the Server project. Indicates if the response has started rendering.
    /// </summary>
    public bool IsPrerendering { get; }
}

Client project: /Communication/ClientRenderContext.cs

/// <inheritdoc/>
public sealed class ClientRenderContext : IRenderContext
{
    /// <inheritdoc/>
    public bool IsClient => true;

    /// <inheritdoc/>
    public bool IsServer => false;

    /// <inheritdoc/>
    public bool IsPrerendering => false;
}

/Program.cs

// Add Render Context for the Client.
builder.Services.AddSingleton<IRenderContext, ClientRenderContext>();

Server project: /Communication/ServerRenderContext.cs

/// <inheritdoc/>
public sealed class ServerRenderContext(IHttpContextAccessor contextAccessor) : IRenderContext
{
    /// <inheritdoc/>
    public bool IsClient => false;

    /// <inheritdoc/>
    public bool IsServer => true;

    /// <inheritdoc/>
    public bool IsPrerendering => !contextAccessor.HttpContext?.Response.HasStarted ?? false;
}

/Program.cs

// RenderContext communicates to components in which RenderMode the component is running.
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<IRenderContext, ServerRenderContext>();

So now you can inject the IRenderContext into your components.

@inject IRenderContext RenderContext
@if (RenderContext.IsClient)
{
	// Render me only on WASM. No server calls anymore.
	// Blazor loading module...
}
else 
{
	// Render me only on Server. No HTTP calls.
}

Now you have control in your components what to render in what mode.


FACADE pattern

// this is my own personal implementation, do whatever you like here // but for this layer, I personally think the Facade pattern is absolutely perfect for this.

Shared project: /Facades/IAppFeatureFacade.cs

/// <summary>
/// The Facade layer is an abstraction layer for communication
/// between the Components and Application depending on the Blazor RenderMode.
/// 
/// Server Facades use WebSockets for requests; Client Facades use HTTP for requests.
/// </summary>
public interface IAppFeatureFacade { }

/Facades/IClientContactFacade.cs

/// <summary>
/// The IClientContactFacade exposes all the features concerning client contacts entities.
/// </summary>
public interface IClientContactFacade : IAppFeatureFacade
{
    /// <summary>
    /// Get client contact by id.
    /// </summary>
    /// <param name="id"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    Task<GetClientContactByIdResponse?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
}

Client project: /Facades/HttpClientContactFacade.cs // stripped but you get the idea

/// <summary>
/// The HttpClientContactFacade exposes all the API features 
/// available concerning client contact entities.
/// </summary>
/// <param name="httpClientFactory"></param>
public class HttpClientContactFacade(IHttpClientFactory httpClientFactory) : IClientContactFacade
{
    private readonly HttpClient http = httpClientFactory.CreateServerClient();

    /// <inheritdoc/>
    public async Task<GetClientContactByIdResponse?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
    {
        var response = await http.GetFromJsonAsync<GetClientContactByIdResponse>($"clientcontact/{id}", cancellationToken: cancellationToken);

        return response;
    }
}

/Program.cs

builder.Services.AddScoped<IClientContactFacade, HttpClientContactFacade>();

Server project: /Facades/ClientContactFacade.cs // stripped but you get the idea

/// <inheritdoc/>
public class ClientContactFacade(ISender sender) : IClientContactFacade
{
    /// <inheritdoc/>
    public async Task<GetClientContactByIdResponse?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
        => await sender.Send(new GetClientContactByIdRequest(id), cancellationToken);

}

/Program.cs

builder.Services.AddScoped<IClientContactFacade, ClientContactFacade>();

/Endpoints/ClientContacts.cs // Minimal API

public class GetClientContactByIdEndpoint : IFeatureEndpoint
{
    public void AddRoutes(IEndpointRouteBuilder app)
        => app.MapGet("clientcontact/{id:Guid}", async (Guid id, IClientContactFacade contacts, CancellationToken cancellationToken)
            => (await contacts.GetByIdAsync(id, cancellationToken))?.ToResponse()
        ).WithTags("Client Contacts");
}

As you probably noticed, the ClientContactFacade is just a layered HttpClient call between the component and server that just calls the endpoint which in turn calls the ServerFacade. Why? to make sure both client- and server calls, run the same code path.

@inject IRenderContext RenderContext
@inject IClientContactFacade ClientContacts

@if (RenderContext.IsClient)
{
	// Render me only on WASM. No server calls.
	// ClientContacts.GetByIdAsync() => HttpClientContactFacade
}
else 
{
	// Blazor loading module...
	// Render me only on Server. No HTTP calls.
	// ClientContacts.GetByIdAsync() => ClientContactFacade
}

// ClientContacts.GetByIdAsync() => Calls ClientContactFacade first, then HttpClientContactFacade once on pageload (so 2 calls in total). But every call after is on HttpClientContactFacade.

bcheung4589 avatar Oct 27 '23 22:10 bcheung4589

I was hit by this today whilst using InterativeWebAssembly render mode globally after upgrading from a .net7 project.

  1. Home page (framework downloads) > Pricing page (with http call) > OK.
  2. Hit F5 - page reloads instantly but the base url is null on the named HttpClient, presumably because the wasm hasn't downloaded yet.

Since I'm using the InterativeWebAssembly globally, I was confused to see this behaviour because I was expecting that mode to act just like the old wasm mode in net7.

I remember watching a recent video suggesting that you have 2 versions of every component which fetches data, a client version (using http) and a server version (using direct service calls).

However, why doesn't InterativeWebAssembly work like wasm did in .net7, with the little timer visible during page load? I wasn't expecting fancy magic unless I picked one of the other render modes such as auto...

Update: I just discovered <Routes @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" /> re: my last sentence.

byte-projects avatar Nov 16 '23 23:11 byte-projects

Without seeing your app, try turning off prerender and see what happens. Not saying I'm sure, when I get home I'll try to replicate your scenario as best I can.


From: Byte Projects @.> Sent: Thursday, November 16, 2023 6:31:24 PM To: dotnet/aspnetcore @.> Cc: Chris Gomez @.>; Author @.> Subject: Re: [dotnet/aspnetcore] Unsure how to inject HttpClient for InteractiveAuto render mode (RC2) (Issue #51468)

I was hit by this today whilst using InterativeWebAssembly render mode globally after upgrading from a .net7 project.

  1. Home page (framework downloads) > Pricing page (with http call) > OK.
  2. Hit F5 - page reloads instantly but the base url is null on the named HttpClient, presumably because the wasm hasn't downloaded yet.

Since I'm using the InterativeWebAssembly globally, I was confused to see this behaviour because I was expecting that mode to act just like the old wasm mode in net7.

I remember watching a recent video suggesting that you have 2 versions of every component which fetches data, a client version (using http) and a server version (using direct service calls).

However, why doesn't InterativeWebAssembly work like wasm did in .net7, with the little timer visible during page load? I wasn't expecting fancy magic unless I picked one of the other render modes such as auto...

— Reply to this email directly, view it on GitHubhttps://github.com/dotnet/aspnetcore/issues/51468#issuecomment-1815494040, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AAEVHCHSXQJKFH6NR7AH3ALYE2O4ZAVCNFSM6AAAAAA6F7SS5SVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQMJVGQ4TIMBUGA. You are receiving this because you authored the thread.Message ID: @.***>

SpaceShot avatar Nov 17 '23 00:11 SpaceShot

Server and client are using the same lifecycle events. Therefore OnInitialized and OnInitializedAsync are called both on server prerendering and client when using interactive web assembly mode. I think we need a better self-explanatory programming model here. Is adding a parameter like bool preRender doable?

lnaie avatar Nov 20 '23 08:11 lnaie

hello, your facade thing helped me a lot bcheung4589 Thx. Initial call is made server side with direct access and after with http access. Happy about that.

But the issue is that at the first render it calls OnInitializedAsync, it retieves my collection and shows it for 1or 2 sec and it disapears... very strange.

If I click on a button to re-fill it, it works form client side with httpclient but I cannot understand why the first call in OnInitalizedAsync is clearer (server-side) (seems to be related to the component being loaded client side)

fdonnet avatar Nov 24 '23 13:11 fdonnet

But the issue is that at the first render it calls OnInitializedAsync, it retieves my collection and shows it for 1or 2 sec and it disapears... very strange.

If I click on a button to re-fill it, it works form client side with httpclient but I cannot understand why the first call in OnInitalizedAsync is clearer (server-side) (seems to be related to the component being loaded client side)

Ive noticed this as well, my solution for now is not rendering on first call (so on prerendering I show "Module loading") which prevents the page flicker => which I find absolutely unacceptable. So Im doing it this way for now, which kinda defeats it purpose, as Im using the WASM part mostly. But currently dont have time to deep dive for a real solution :(

On pages that I dont use Interactivity, I dont have that page-flicker issue.

Another issue I have is that on the server call (so after prerendering) it doesnt hit middleware, unless you always use WASM (so it goes through an endpoint => calls middleware). But just running it InteractiveServer wont call middleware on its Server run, and registered scoped services in the ServiceCollection wont help.

So if you are planning to get a certain Id from the Claims (in middleware) using Interactivity, it can become very tricky.

Your results disappears because the second run is not doing something that it should, in my case it was: middleware isnt run, so my scoped TenantAccessor didnt get updated through middleware.

bcheung4589 avatar Nov 24 '23 14:11 bcheung4589

@bcheung4589 ... ok you are facing exactly the same issues as me. So I m not crazy... private static IComponentRenderMode _rendermode = new InteractiveAutoRenderMode(prerender: false);

Like you, I find that unacceptable too. Because what's the purpose of auto mode if you have this flickering with prerender on or be forced to wait the wasm loading before doing someting without prerender ?

And like you, if i use some -autorizedview- between the call it's weird and I think it's due to my user middleware too. Not able to retrieve the user to get accesstoken in distibuted cache before a call to an external api.

The final goal was to protect all the external api call with the server layer for all cases

  • first render direct via your server facade
  • after wasm load, from client through a server endpoint that will call the external api with the retrived token from cache

With all the marketing they made... I think we will be able to manage some amazing stuff but I m blocked on my inital tentative for weird simple things....

fdonnet avatar Nov 24 '23 15:11 fdonnet

You can solve this with the proposed solution in: https://github.com/dotnet/aspnetcore/issues/52350

bcheung4589 avatar Nov 24 '23 15:11 bcheung4589

Thx @bcheung4589 ,

For now, I m able to persist authentification/autorization state both side with this guy example https://github.com/CrahunGit/Auth0BlazorWebAppSample/issues/1

I m able to develop component that can work both side because of your facade pattern. (example calling an external api

  • Direct call to the httpclient (server side)
  • Call to server side blazor endpoint that call the httpclient (wasm side)

Authentification is in place with Keycloack (openid), the tokens to access external api are stored in distributed cache (server side). To keep my user injected correctly I followed the circuit accessor example from learn Microsoft and it seems to work.

Now what is missing, is the flickering with prerender... :-) I hope I will be able to find a solution... Why the rerender after the inital run when you have a perfect first version from the server... sad.

fdonnet avatar Nov 24 '23 15:11 fdonnet

@bcheung4589 for info it works now for no flcikering with @inject PersistentComponentState ApplicationState as explained here at the bottom of this article https://learn.microsoft.com/en-us/aspnet/core/blazor/components/prerendering-and-integration?view=aspnetcore-7.0&viewFallbackFrom=aspnetcore-8.0&pivots=server

but it's not a very elegant solution...

Interactive auto mode can work with prerender only if you persist the data for the double call to OnInitializedAsync... I hope, on time, they will come with a more integrated solution..

fdonnet avatar Nov 24 '23 18:11 fdonnet

@bcheung4589 for info it works now for no flcikering with @Inject PersistentComponentState ApplicationState as explained here at the bottom of this article https://learn.microsoft.com/en-us/aspnet/core/blazor/components/prerendering-and-integration?view=aspnetcore-7.0&viewFallbackFrom=aspnetcore-8.0&pivots=server

but it's not a very elegant solution...

Interactive auto mode can work with prerender only if you persist the data for the double call to OnInitializedAsync... I hope, on time, they will come with a more integrated solution..

In my humble opinion, I dont like the solution (beside sharing your opinion of not finding it elegant). But I do get how you arrived at it, you want to preserve the state it loses at the second render call.

What I would like is, not preserve the state, but just dont execute the main code on the first call (prerender). With main code I mean the code to get the data I want to show (in a table or whatever).

Below are some minimal excerpts from my code.

In your razor component you do your checks: is it loading? did I get any results after loading?

@inject IRenderContext RenderContext

@if (!isLoading)
{
	if (users.Any())
	{
		<p>@users.Count() items found.</p>
		<QuickGrid Items="users" Class="table table-striped">
			<TemplateColumn Title="Name" SortBy="GridSort<UserViewModel>.ByAscending(p => p.FirstName)">
				<span @onclick="@(() => NavigateTo(context))" role="button" title="View details of @context.UserName">@context.FirstName @context.LastName</span>
			</TemplateColumn>
		</QuickGrid>
	}
	else
	{
		<p class="text-muted">No users found.</p>
	}
}
else {
	<p class="text-muted">Loading data..</p>
}

@code {

    private IQueryable<UserViewModel> users = null!;
    private bool isLoading = true; // notice we start with loading immediately

    protected override async Task OnInitializedAsync()
    {  
        if (RenderContext.IsPrerendering)
        {
            return; // no need for prerendering
        }
		
        // Require User or move away.
        await UserService.RequireUserAsync(AuthenticationStateProvider, Navigation);

        await LoadDataAsync();
    }

    private async Task LoadDataAsync()
    {
        isLoading = true;

        // get your users

        isLoading = false;
    }
	
}

How does this work?

  1. we wrap the razor/html code with a check to see if we are loading (isLoading) and initialize it with true, so we start of with the loading state.
  2. if the page is prerendering (1e call) it wont load the data by calling LoadDataAsync (and setting isLoading on false); keeping it loading state.
  3. on the 2e call it will load the data and set isLoading to false, showing our data without the flicker.

With this setup you can decide what data to load in LoadDataAsync => which will only load after the prerender (without middleware execution). This doesnt solve the issue that middleware isnt called, therefore you need the UserService.RequireUserAsync in here. You could now create a blazor component <LoadingArea IsLoading="isLoading" /> or whatever name you like, and put the [@if (!isLoading)] check in there.

Now! Create a new component UserRequiredState and add the UserService.RequireUserAsync() call in there. Add the UserRequiredState-component to the razor components where you require users. I havent implemented UserRequiredState-component myself yet, but it should work and I find it much more of a blazor-minded solution (using components) :)

bcheung4589 avatar Nov 25 '23 12:11 bcheung4589

I will try your way for when I have a lot of data to load, for when it's a small payload, i think it's good to have the view quickly and it works with this persitence workarround, no flickering and data are available in the first round.

fdonnet avatar Nov 27 '23 09:11 fdonnet

Check out this solution https://stackoverflow.com/a/63833663 Instead of injecting the httpClient, just inject the ApiService and use ApiService.HttpClient It's working for me :-)

PierreSpaak avatar Dec 04 '23 16:12 PierreSpaak

public bool IsPrerendering => !contextAccessor.HttpContext?.Response.HasStarted ?? false;

I like your approach @bcheung4589 but I would just query the use of HttpContext above, in light of the docs.

IHttpContextAccessor must be avoided with interactive rendering because there isn't a valid HttpContext available.

IHttpContextAccessor can be used for components that are statically rendered on the server. However, we recommend avoiding it if possible.

HttpContext can be used as a cascading parameter only in statically-rendered root components for general tasks, such as inspecting and modifying headers or other properties in the App component (Components/App.razor). The value is always null for interactive rendering.

Your approach detects interactive server rendering when Response.HasStarted = true, and in my .NET 8 Blazor Web App dev environment that's exactly what I'm seeing.

So I'm unclear how to square that with the docs that state HttpContext is always null for interactive rendering.

danielgreen avatar Dec 06 '23 16:12 danielgreen

public bool IsPrerendering => !contextAccessor.HttpContext?.Response.HasStarted ?? false;

I like your approach @bcheung4589 but I would just query the use of HttpContext above, in light of the docs.

IHttpContextAccessor must be avoided with interactive rendering because there isn't a valid HttpContext available. IHttpContextAccessor can be used for components that are statically rendered on the server. However, we recommend avoiding it if possible. HttpContext can be used as a cascading parameter only in statically-rendered root components for general tasks, such as inspecting and modifying headers or other properties in the App component (Components/App.razor). The value is always null for interactive rendering.

Your approach detects interactive server rendering when Response.HasStarted = true, and in my .NET 8 Blazor Web App dev environment that's exactly what I'm seeing.

So I'm unclear how to square that with the docs that state HttpContext is always null for interactive rendering.

You do have a point about the docs, Im also not clear about that.

I just always get the HttpContext with the IHttpContextAccessor and HttpClients with IHttpClientFactory as recommended practice.

As a sidenote: getting HttpClients with IHttpClientFactory gives even more benefits because of being configurable through appsettings. Those kind of perks are always available if you use recommended/best practices.

bcheung4589 avatar Dec 06 '23 18:12 bcheung4589