06 · Render modes

What runs where in Server, WebAssembly and Auto, and when HTTP is involved

Goal: know which code answers a call in each render mode, when HTTP is involved, and what to register in which project.

See it running: the Todos example (/autoapi/example/todos).

The one idea

A component injects the interface. Which class it gets depends on where the component runs.

Where the component runsIBookService isHTTP request?JSON serialization?
Static rendering and prerenderingYour service, resolved on the serverNoNo
Interactive ServerYour service, resolved in the circuitNoNo
Interactive WebAssemblyThe generated clientYesYes
Interactive AutoYour service on the server at first, the generated client once it runs in WebAssemblyOnly in WebAssemblyOnly in WebAssembly

On the server a call is an ordinary method call. Nothing is serialized, no endpoint runs, and no endpoint convention (rate limiting, filters) applies. Tutorial 05 explains how the rules still apply there.

1. Mark and register the server

The server project holds your implementation and the generated endpoints:

// MyApp/AutoApiEndpoints.cs
using MyApp.Shared;
using RForge.Blazor.AutoApi;

namespace MyApp;

[RForgeAutoApiHost(typeof(IBookService))]
public static partial class AutoApiEndpoints { }
// MyApp/Program.cs (excerpt)
builder.Services.AddScoped<IBookService, BookService>();    // your implementation
builder.Services.AddRForgeAutoApiServer();                  // after it

// ...
app.MapRForgeAutoApi();                                     // the endpoints WebAssembly calls

2. Mark and register the WebAssembly client

The client project holds the generated HttpClient implementation:

// MyApp.Client/AutoApiClients.cs
using MyApp.Shared;
using RForge.Blazor.AutoApi;

namespace MyApp.Client;

[RForgeAutoApiClient(typeof(IBookService))]
public static partial class AutoApiClients { }
// MyApp.Client/Program.cs
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using MyApp.Client;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.Services.AddRForgeAutoApi();

await builder.Build().RunAsync();

The generated requests use relative URLs. In a project that references Microsoft.AspNetCore.Components.WebAssembly (every Blazor WebAssembly project does), the client's BaseAddress is set to the app's base address for you (tutorial 07). Without that reference, set BaseAddress yourself.

Which project needs what:

ProjectPackageNeeds
MyApp.SharedRForge.Blazor.AutoApi.AbstractionsThe interface, its types and the JSON context
MyApp (server)RForge.Blazor.AutoApi.GeneratorThe host class, your implementation, AddRForgeAutoApiServer(), MapRForgeAutoApi()
MyApp.ClientRForge.Blazor.AutoApi.GeneratorThe client class and AddRForgeAutoApi() in its Program.cs

An app that uses only Interactive Server never makes the HTTP call, so it needs no client project. It still uses the host class for AddRForgeAutoApiServer().

3. One component, every mode

@* MyApp.Client/Pages/Books.razor *@
@page "/books"
@rendermode InteractiveAuto
@using MyApp.Shared
@inject IBookService BookService

<h1>Books</h1>
<p>Runtime: @(OperatingSystem.IsBrowser() ? "WebAssembly" : "Server")</p>

@if (_books is null)
{
    <p>Loading...</p>
}
else
{
    <ul>
        @foreach (var book in _books)
        {
            <li>@book.Title by @book.Author</li>
        }
    </ul>
}

@code {
    private List<BookDto>? _books;

    protected override async Task OnInitializedAsync() => _books = await BookService.GetAllAsync();
}

The component doesn't mention HTTP, the render mode or the generated client. OperatingSystem.IsBrowser() is only there to show where it runs.

When you request /books, the server prerenders the page. The prerendered HTML says Runtime: Server and already lists the books, and the only HTTP request the server sees is the page itself: the call went straight to BookService.

4. What Interactive Auto does

  1. Prerender (server). The component runs once on the server, calling your service directly.
  2. First visit. The page becomes interactive on the server, over a circuit, while the WebAssembly files download. Calls are still direct.
  3. Later visits. The page becomes interactive in WebAssembly. Calls are HTTP requests to the generated endpoints.

Blazor starts the component again when it becomes interactive, so OnInitializedAsync runs twice: once while prerendering and once when the interactive instance starts. That means two calls to the service. Persist the first result with PersistentComponentState if you want to avoid the second.

5. What differs between the modes

The interface hides the mode, but a few things still differ:

Server (direct call)WebAssembly (HTTP)
Returned objectsThe very instances your service returnedCopies deserialized from JSON
Service lifetimeScoped = one per circuit, or per request when prerenderingA new scope for every HTTP request on the server
Endpoint conventions (tutorial 08)Don't runRun
CancellationTokenThe token you passGoes to HttpClient. The service receives the request's RequestAborted
Expected errors (RfException family)Thrown by your codeRethrown on the client, with the same type, message, status and code
Unexpected errorsThe original exceptionRfApiException (tutorial 04)
AuthorizationThe decorator (tutorial 05)The endpoint

Two consequences:

  • Don't keep state in a Scoped service's fields. A circuit reuses one instance for many calls, but every HTTP request gets a new one, so the same code would behave differently per mode. Keep state in a Singleton store, a database or a cache, and inject it, as the Todos example does with its DemoTodoStore.
  • Return immutable types. If a component mutates the list or object it received, that changes your service's data when the call was direct, and only a copy when it came over HTTP. Records and copies (list.ToList()) behave the same in both.

Best practices

  • Inject the interface everywhere, never the generated client or the implementation.
  • Register the generated client only in WebAssembly projects, and the implementation only on the server.
  • Return records and copies, and keep state outside the service instance.
  • Test in both modes. Run a page once in Interactive Server and once in WebAssembly, since the two code paths are different.
  • Don't count on ordering between prerender and interactive calls. Both run, in that order.

Common mistakes

SymptomCauseFix
No service for type 'MyApp.Shared.IBookService' has been registered in the browserAddRForgeAutoApi() missing from MyApp.Client/Program.csAdd it (step 2)
The same error on the serverThe implementation isn't registeredRegister it before AddRForgeAutoApiServer() (tutorial 05)
Works on Server, 404 in WebAssemblyMapRForgeAutoApi() isn't called, or RoutePrefix differsMap the endpoints, and keep the defaults in one shared library
An invalid request URI was provided. Either the request URI must be an absolute URI or BaseAddress must be set.The client project doesn't reference Microsoft.AspNetCore.Components.WebAssembly, so no base address was setReference it, or set BaseAddress (tutorial 07)
Data differs between Server and WebAssemblyState kept in a Scoped service's fields, or in objects a component mutatedMove state out of the service and return immutable results
Items load twice on the first visitPrerender and interactive start both call the servicePersist the result with PersistentComponentState

Next: 07 · HttpClient setup

An unhandled error has occurred. Reload 🗙