The Blazor App That Signs You Up Behind Your Back
DShop.Blazor is a Blazor 0.7 preview app whose naive choices - component classes as singletons, a silent auto-signup, a shared HttpClient - all work because of the runtime they run on. A study in patterns that are only correct by accident.
The Blazor frontend works, and every reason it works is slightly alarming. DShop.Blazor is a 0.7.0 preview app — Blazor before a stable release, before .razor files, components written as .cshtml with @functions, running on the mono interpreter in the browser and launched with dotnet blazor serve. Its author had no stable guidance to follow, so they invented patterns, and the patterns they invented are correct only because of properties of the runtime they happen to run on. Change the runtime and each one becomes a bug. This is the more interesting frontend to read precisely because, unlike the Angular app, it is not broken — it is a working system standing on foundations that would collapse under a different load.
Components as singletons
The organising idea is “component-class-as-service.” A component's logic lives in a plain C# class registered in DI, and the .cshtml view is a thin shell that delegates to it. MainLayoutComponent is such a class, and here is how it's registered:
services.AddSingleton<MainLayoutComponent, MainLayoutComponent>();
services.AddSingleton<IAuthService, AuthService>();
services.AddSingleton<IIdentityService, IdentityService>();
The component is a singleton. Its view binds to it and calls into it on init:
@inject MainLayoutComponent component
@functions {
protected override async Task OnInitAsync() => await component.OnInit();
}
This is hand-rolled MVVM before Blazor had an opinion about state, and as a pattern it is genuinely reasonable — separate the logic from the markup, make it injectable and testable. The problem is the lifetime. A singleton component holds one instance of its state for the entire application. In client-side WASM that is fine, because the entire application is one user in one tab — a singleton is effectively per-session. Move the identical code to Blazor Server, where one process serves many users' circuits, and that singleton becomes shared mutable state across every connected user: your cart is my cart, your identity is my identity. The pattern is not wrong or right in itself; it is correct exactly as long as “singleton” and “one user” mean the same thing, and on WASM they do.
The signup that happens without you
Now the title's promise, in MainLayoutComponent.OnInit — which runs on every page load:
public async Task OnInit()
{
var accessToken = await _authService.GetAccessTokenAsync();
if (accessToken is null)
{
var identity = _identityService.CreateDefaultIdentity();
await _identityService.SignUpAsync(identity);
var tokens = await _identityService.SignInAsync(identity);
await _authService.SetAccessTokenAsync(tokens.AccessToken);
await _customersService.CreateDefaultAsync();
}
}
If you arrive without a token, the app invents an account and signs you into it before you have done anything. And CreateDefaultIdentity is where it gets uncomfortable:
public Identity CreateDefaultIdentity()
=> new Identity
{
Email = $"{Guid.NewGuid()}@mailinator.com",
Password = "<a fixed, hard-coded password>" // a constant string in the source
};
Every first-time visitor is silently registered as {some-guid}@mailinator.com with a single hard-coded password shared by every auto-created account. On one hand it is delightful zero-friction onboarding — the shop just works, no sign-up wall, and the Blazor buy-loop runs because there is always a valid token. On the other hand it is a security anti-pattern in ten lines: accounts created without consent, on a public disposable-mail domain, all sharing one password, so anyone who knows the scheme can log into any auto-provisioned account by guessing its GUID. Zero-friction onboarding and self-service account takeover are, here, the same feature. (That the Identity service happily honours whatever role such a request asks for is the security inversion's problem; this is just the frontend cheerfully using it.)
The shared client that only works because nothing is concurrent
The HttpService base class attaches auth by mutating the shared HttpClient's default headers on every call:
private async Task SetAuthorizationHeaderAsync()
{
var accessToken = await _authService.GetAccessTokenAsync();
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
}
Setting DefaultRequestHeaders mutates state shared by every request that client will ever make. If two requests were in flight, one could overwrite the other's Authorization between the header set and the send. On WASM there is no such race — the mono runtime is single-threaded, requests are effectively serialised, so mutating the shared client is safe. Notice that this is the same class of bug as the Angular app's shared mutable httpOptions — a shared request-config object mutated per call — except here it attaches the token correctly and the runtime protects it from the race. Angular's version discarded the header and had real concurrency; Blazor's version keeps the header and has no concurrency. Same smell, opposite outcome, decided entirely by the runtime.
One more tell in the auth wiring: AuthService's constructor takes an HttpClient and never uses it — a dead injected dependency, stored nowhere, calling nothing. Harmless, but a sign the class was copied from a template it didn't need.
Config smuggled in as an embedded resource
Configuring a client-side app in 2018 was an unsolved problem — there is no server to read appsettings.json from — and the solution here is clever for the era. The settings file is compiled into the assembly as an embedded resource, chosen by build configuration, and read back from the manifest stream:
private static AppSettings GetAppSettings()
{
using var stream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream("appsettings.json");
using var reader = new StreamReader(stream);
return Json.Deserialize<AppSettings>(reader.ReadToEnd());
}
The .csproj embeds appsettings.dev.json in Debug and appsettings.prod.json in Release, both renamed to the logical name appsettings.json, so the same code reads whichever the build baked in. It is a real solution to “how does a serverless client get its config” — config as a build artefact. The honest-ledger footnote is that the two files are identical, both pointing at http://localhost:5010, and 5010 is a port nothing in the estate serves — the gateway is on 5000. So dev and prod are the same config, and the API URL it names is a ghost; the app's reads only work if you happen to run the gateway on 5010 yourself. The identity URL, at least, correctly points at :5002, which is why the auto-signup flow works while the product calls point nowhere.
jQuery in a WebAssembly app, and the rest of the ledger
The remaining scars are quick and consistent with the theme:
- jQuery 3.3.1 from a CDN, loaded in
index.html, into a WebAssembly app whose premise is running C# in the browser — present only so a Toastr interop package has a jQuery to call. - No pagination. The product list ignores the gateway's paged envelope and renders whatever it gets.
Task.Delay(1000)after writes, the sleep through eventual consistency the SignalR service existed to remove.- The Dockerfile ships the SDK image and runs
dotnet runas its production entrypoint — for a WASM app that compiles to static files a plain web server could host, the container is a full .NET SDK running the dev server in production.
To be fair to the app: it is a preview, exploring a framework nobody had shipped yet, and it works — which is more than its tidier sibling can say. The component-as-service idea, the embedded-resource config, the clean HTTP base class are all reasonable inventions for a moment with no playbook. Judged against 2018's Blazor, it is ambitious and largely successful.
The durable lesson is about the fragility hiding inside “it works.” Code that is correct because of a runtime property is correct until the runtime changes, and nothing in the code says which property it's leaning on. The singleton component leans on single-user; the shared HttpClient leans on single-threaded; the auto-signup leans on nobody minding. Port this app to Blazor Server — same language, same framework family, a supported migration on paper — and the singletons leak users into each other, the shared client races, and the pattern that made it work becomes the bug that breaks it. When you inherit code that works for reasons the code doesn't state, your first job is to find the property it's standing on, because that property is the thing you're not allowed to change.
Next, we leave the frontends and read how the whole estate is packaged: a pedagogy ladder of Dockerfiles.