Elshad Shabanov
Elshad Shabanov

Reputation: 603

In .Net 8 Blazor app how to fetch data only on client, while keeping prerender on the server

I have an app created from a new .Net 8 Blazor template for Visual Studio. Interactive render mode: WebAssembly Interactive location: Global The template creates two projects: Server, Client

The problem is - when I try to load data from API using HttpClient within the OnInitialized method, I get an error that HttpClient is not a registered service. That is happening because the app tries to pre-render the page on the server, and on the server, there is no registered HttpClient service.

I spent two days trying to resolve the issue, and found two kinds of solutions, that still do not work for me:

  1. Create a shared interface and create two service classes (ClientService, ServerService) inherited from it on both the Server and Client projects. And then within OnInitialized method call the ClientService. During prerendering on the server via dependency injection, ServerService will be called instead. This method is not suitable for my scenario as it does not hit controller's authorization mechanism, and goes straight to data fetch from the service. https://blazor.syncfusion.com/documentation/common/how-to/create-blazor-webassembly-prerendering

  2. Register named HttpClient both on the server and client. In the new Blazor Web App template using .net 8, how do I make an HTTP request from a InteractiveWebAssembly mode component?

    services.AddHttpClient("MyClient", client =>
        {
            client.BaseAddress = new Uri("https://localhost:7110/");
        });

But I couldn't make this work. simply using @inject HttpClient HttpClient on the page seems does not work, and throws an error.

In reality, I don't need the data fetched on the server. I need it only when the component got initialized on the client, like in pre-.Net 8 (6, 7) Blazor Webassembly. One piece of advice was to fetch data within the OnAfterRender method. But OnAfterRender runs a bit late compared to OnInitialized. For example in .Net 7 OnInitialized runs before the rendering takes place. But unlike .Net 8 it does not run both on the Server and Client, only on Client.

In other words I want to prerender all UI elements, except for API calls. Any thoughts on how to properly set it up?

Below are my project files.

Program.cs on Client:

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

Home.razor on Client:

@page "/"
@inject HttpClient HttpClient

<div>Elements: @forecasts?.Count()</div>
Some UI controls here 

@code {
    IEnumerable<Weather>? forecasts;

    protected override async Task OnInitializedAsync()
    {
        // run this part of code only when component (page) initialized on client-side
        forecasts = await HttpClient.GetFromJsonAsync<IEnumerable<Weather>>("api/weather/list");
    }
}

WeatherController.cs on Server

    [Authorize]
    [ApiController]
    [Route("api/[controller]")]
    public class WeatherController : ControllerBase
    {
        ILogger<WeatherController> _logger;
    
        public WeatherController(ILogger<WeatherController> logger)
        {
            _logger = logger;
        }
    
    
        [HttpGet]
        [Route("list")]
        public async Task<ActionResult<IEnumerable<Weather>>> GetList()
        {
            var startDate = DateOnly.FromDateTime(DateTime.Now);
            var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
            var result = Enumerable.Range(1, 5).Select(index => new Weather
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            });
            return Ok(result);
        }
    }

Upvotes: 3

Views: 1923

Answers (5)

S Mxller
S Mxller

Reputation: 141

Implement an interface for the Service as descriped here.

This requires you to make an additional abstraction. HttpClient gets injected into the Client Service and not the page directly. On the Client the interface is injected with a Service which injects the HttpClient and makes the API Request. On the Server Side you can inject the interface with a Service that does not return any Data (Empty List or null, depending on your implementations)

Another option is to only Prerender the MainLayout with a Splash Screen or just a Splash Screen without the MainLayout. App.razor

    <HeadOutlet @rendermode="new InteractiveWebAssemblyRenderMode(true)" />
</head>

<body>

    <Routes @rendermode="new InteractiveWebAssemblyRenderMode(true)"></Routes>

Routes.razor:

<CascadingAuthenticationState>
    <ComponentRoot>
        <Router AppAssembly="@typeof(Program).Assembly">
            <Found Context="routeData">

                <Prerender>
                    <Server>
                        <MainLayout>
                            <Body>
                                ... Splash Screen
                            </Body>
                        </MainLayout>
                    </Server>
                    <Client>

                        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                            <NotAuthorized>
                                @if (context.User.Identity?.IsAuthenticated != true)
                                {
                                    <RedirectToLogin />
                                }
                                else
                                {
                                    <p role="alert">You are not authorized to access this resource.</p>
                                }
                            </NotAuthorized>
                        </AuthorizeRouteView>

                    </Client>
                </Prerender>
            </Found>
            <NotFound>
                <LayoutView Layout="@typeof(MainLayout)">
                    <ErrorPage StatusCode="404">
                    </ErrorPage>
                </LayoutView>
            </NotFound>
        </Router>
    </ComponentRoot>
</CascadingAuthenticationState>

Prerender.razor:

@if (BlazorRenderState.IsPrerender)
{
    @Server
}
else
{
    @Client
}
@code{
 [Parameter]
 public RenderFragment Server { get; set; }

 [Parameter]
 public RenderFragment Client { get; set; }

 [Inject]
 private IBlazorRenderStateService BlazorRenderState { get; set; }
}

IBlazorRenderStateService WASM and Server:

public class WasmBlazorRenderStateService : IBlazorRenderStateService
{
    public bool IsPrerender => false;
}

...

public class ServerBlazorRenderStateService : IBlazorRenderStateService
{
    private readonly HttpContext? httpContext;

    public ServerBlazorRenderStateService(IHttpContextAccessor contextAccessor)
    {
        this.httpContext = contextAccessor.HttpContext;
    }

    public bool IsPrerender => this.httpContext is not null && this.httpContext.Response.HasStarted == false;
}

Everything in is only displayed while in prerender. No need to implement Services on Server, since the components wont Render on Server.

Upvotes: 1

Liero
Liero

Reputation: 27350

In order to skip API calls on prerender, you just need to check if you are currently prerendering

protected override async Task OnInitializedAsync()
{       
    if (!IsPrerendering)
    {
        //call your API
    }        
}

so you can narrow the question down to:

How to detect prerendering in Blazor?

  • This has been asked on SO, see Detecting server pre-rendering in Blazor server app
  • In summary, there is no built in API, but there are various options, depending on hosting model and render mode
    • RuntimeInformation.ProcessArchitecture != Architecture.Wasm;
    • IHttpContextAccessor.HttpContext.Response.HasStarted
    • You obviously have different startup logic when registering services, so you could set some boolean flag there

Upvotes: -1

Bartosz Pawluk
Bartosz Pawluk

Reputation: 55

Your option number one - creating a shared interface with two implementations - is the recommended approach. The server implementation will bypass the API authorization as you mentioned, though. For that reason you need to secure the client components in addition to securing your API. You can use AuthorizeView for that purpose.

Maintaining two implementations this way requires some additional effort. I created a Blazor library that abstracts away all the boilerplate code to make it easier. I recommend checking it out :)

Upvotes: 0

Ed Mendez
Ed Mendez

Reputation: 1693

In Your Home.razor component on the client add the following.

@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))

This will disable the pre-rendering and your page should load only once using the registered services from the client side.

I had a similar situation, and using debug figured out that the first load, it was being rendered on the server and errored because it couldn't find the registered services. The subsequent load, it was using the client side registered services.

Upvotes: 1

Qiang Fu
Qiang Fu

Reputation: 8616

You could check if it is not prerender then fetch the api.

@page "/"
@using System.Runtime.InteropServices
@inject HttpClient HttpClient

<div>Elements: @forecasts?.Count()</div>

@code {
    IEnumerable<Weather>? forecasts;

    protected override async Task OnInitializedAsync()
    {
        var isPrerendering = RuntimeInformation.ProcessArchitecture != Architecture.Wasm;
        if (!isPrerendering)
        {
            forecasts = await HttpClient.GetFromJsonAsync<IEnumerable<Weather>>("api/weather/list");
        }        
    }
}

Upvotes: 3

Related Questions