dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.2k stars 9.94k forks source link

Component will be prerendered (executed) in server with InteractiveWebAssembly settings. #51342

Closed nakamacchi closed 11 months ago

nakamacchi commented 11 months ago

Is there an existing issue for this?

Describe the bug

[Symptoms] When InteractiveWebAssembly is specified in the razor component created on the Client Project side, the initial processing is executed on the server side.

[Problems caused by this] If HttpClient is @inject on the WASM razor component, runtime will try to inject the server-side HttpClient on the first operation. An exception occurs when HttpClient is registered only on the WASM Client project side.

This behavior is reasonable if we use InteractiveAuto, but I suspect this is a bug when using InteractiveWebAssembly. (Maybe it is by-design, but it is very confusing.)

Expected Behavior

[Expected behavior] When InteractiveWebAssembly is specified in the razor component created on the Client Project side, all processing is executed on the browser side.

Steps To Reproduce

No response

Exceptions (if any)

[Workaround] It can be avoided by disabling prerendering by specifying below. However, the settings are very roundabout.

@rendermode InteractiveWebAssemblyWithoutPrerendering @code { static IComponentRenderMode InteractiveWebAssemblyWithoutPrerendering = new InteractiveWebAssemblyRenderMode(prerender: false); }

or

@attribute [RenderModeInteractiveWebAssembly(prerender: false)] (I've heard that this is not deprecated in GA.

[Suggestion]

.NET Version

No response

Anything else?

No response

augustevn commented 11 months ago
  1. The Blazor pre-rendering setup has always worked liked that, Server does first serve and needs those same service registrations, middlewares etc.

  2. If you only want Blazor WASM without pre-rendering, you can remove the Server project or just start from the blazorwasm (Blazor WebAssembly Standalone App) or blazorwasm-empty templates instead of the blazor (Blazor Web App) template.

Some things are bit confusing at first indeed. Feel free to watch: https://www.youtube.com/@kis.stupid I have some videos on Blazor pre-rendering, upgrading to .NET 8 etc.

mahald commented 11 months ago

The prerenderer always runs on the Server. This may means you will need to add your "Service" for DepndecyInjection in the .Client for WebAssembly and in The Server for prerender.

You could disable prerender like this if you don't want it (then it will only run in Browser):

@rendermode _rendermode

......

@code 
{
            private static IComponentRenderMode _rendermode = new InteractiveWebAssemblyRenderMode(prerender: false);
} 
mahald commented 11 months ago

PS works for all Render modes: private static IComponentRenderMode _rendermode = new InteractiveWebAssemblyRenderMode(prerender: false);

private static IComponentRenderMode _rendermode = new InteractiveServerRenderMode(prerender: false);

private static IComponentRenderMode _rendermode = new InteractiveAutoRenderMode(prerender: false);

mahald commented 11 months ago

Using pre-render mode can enhance the responsiveness of your site, so it might be beneficial to incorporate it. One approach is to only inject the IServiceProvider and obtain other services manually. By doing so, you can opt not to fetch data in OnInitializedAsync if not all services are available, perhaps because they weren't registered on the backend.

This also simplifies the process if there's a need to support InteractiveServer. The DataLoaderService can then utilize dependency injection to access either the HttpClient (for WASM) or EntityFramework (for Server) and fetch the data.

PS: you don't need to use generic types; this is since my component has a @typeparam T


    @if (_items == null)
    {
        // LoadingSpinner.gif
        <MudProgressCircular Color="Color.Default" Indeterminate="true" />
    }
    else
    {
        // display your data ...
    }

@code { 
    ...
    [Inject]
    public IServiceProvider ServiceProvider { get; set; } = null!;
    ...
    private IValidator<T> _validatorService = null!;
    private ParameterizedDialogService _parameterizedDialogService = null!;
    private IDataLoaderService<T> _dataLoaderSevice = null!;
    private IDataUpdateService<T> _dataUpdateService = null!;
    ...
    private HashSet<T>? _items;
    ...
    private async Task LoadData() => _items = await _dataLoaderSevice.LoadData();

    protected override async Task OnInitializedAsync()
    {
        _dataLoaderSevice = ServiceProvider.GetService<IDataLoaderService<T>>()!;
        _dataUpdateService = ServiceProvider.GetService<IDataUpdateService<T>>()!;
        _parameterizedDialogService = ServiceProvider.GetService<ParameterizedDialogService>()!;
        _validatorService = ServiceProvider.GetService<IValidator<T>>()!;

        if (_dataLoaderSevice == null
            || _dataUpdateService == null
            || _parameterizedDialogService == null
            || _validatorService == null)
        {
            // exit since at least one service could not be retrieved
            return;
        }

        await LoadData();
    }

    // this LifeCylce hook is not called during preRender or SSR mode
    protected override void OnAfterRender(bool firstRender)
    {
        if (!firstRender)
        {
            return;
        }

        // ensure error messages if services are missing and we enter an Interactive Mode.
        // else the spinner will spinn forever without an error message.
        _dataLoaderSevice ??= ServiceProvider.GetRequiredService<IDataLoaderService<T>>();
        _dataUpdateService ??= ServiceProvider.GetRequiredService<IDataUpdateService<T>>();
        _parameterizedDialogService ??= ServiceProvider.GetRequiredService<ParameterizedDialogService>();
        _validatorService ??= ServiceProvider.GetRequiredService<IValidator<T>>();
    }
}
[MyTypeHttpDataLoaderService.cs]

public class MyTypeHttpDataLoaderService(HttpClient httpClient) : IDataLoaderService<MyType>
{
    private readonly HttpClient _httpClient = httpClient;

    public async Task<HashSet<MyType>> LoadData()
    {
        var items = await _httpClient.GetFromJsonAsync<HashSet<MyType>>("relativ url");
        return items ?? [];
    }
}
Register for WASM:
...
builder.Services.AddScoped<IDataLoaderService<MyType>, MyTypeHttpDataLoaderService>();
...
nakamacchi commented 11 months ago

Thank you for suggestion, all. I understand the workaround and prerendering is working on the server-side only. So the discussion points are below. Note: This problem will be problematic only when in the mixed application (blazor server and blazor wasm in single application).

1. Default prerendering mode should be always true in every application?

-> I understand that prerendering is very nice feature for SEO in such like B2C website, but it is not necessary needed for intranet application (LOB application) or in the authenticated page. In such page or application, prerendering is not good because for double work in some case.

2. Component with InteractiveWebAssembly mode should be run in client-side?

-> I think the developer will expect this switch to control the location which the component should be run. But in default behavior (i.e., prerendering is enabled), The component with InteractiveWebAssembly mode will be run in the Server too. This is very confusing, and it seems that the programming model is broken.

So I want to recommend that the default prerendering mode should be disabled, or @rendermode without prerendering option should be prepared by built-in.

mahald commented 11 months ago

The primary concern with disabling prerendering is that the page can appear unresponsive, especially if the dotnetWasm isn't cached and is being downloaded for the first time. With slow internet connectivity, this might result in users seeing a blank screen for several seconds after clicking a menu link. While this might be acceptable for an internal company dashboard, it's problematic for public-facing sites. Visitors might perceive the site as malfunctioning and either leave or continuously refresh the page. As a result, potential customers could be lost simply due to the absence of prerendering. Therefore, enabling it by default is advisable.

javiercn commented 11 months ago

@nakamacchi thanks for contacting us.

As several folks point out, the default approach is to prerender. This is not something that we currently plan to change.