Skip to main content
ANVISoftware Solutions
Lesson 14 of 14Advanced18 min

Deployment

By the end of this lesson

Publish and host a Blazor application.

Publishing turns a project into a set of files something can host. For most .NET applications that sentence is the whole story. For Blazor it is not, because what publish produces depends on the hosting model — and therefore so does what can host it, and what can go wrong.

The decision from the first lesson in this course arrives again here, with different consequences. A Blazor Server application publishes as an ASP.NET Core application: it runs, it holds state, it needs a live connection to every user. A standalone WebAssembly application publishes as static files: nothing runs on the server, and the server's only job is to hand out those files correctly.

Most deployment problems in Blazor are one of a small number of specific things, and almost all of them are in the host rather than in your code. This lesson names them.

The same solution, published two ways. Nothing in your components changes; everything about the deployment does:

 Blazor ServerBlazor WebAssembly (standalone)
What publish producesAn ASP.NET Core application: a host executable, your assemblies, and a wwwroot of static content.A wwwroot folder: your assemblies, the .NET runtime, and the page that starts them.
What can host itAnything that runs ASP.NET Core — a container, a Windows or Linux server, a managed app service.Any static file host or CDN. Also ASP.NET Core, if you want the app and its API on one origin.
What the server does at runtimeRuns every component, and holds one circuit in memory per connected user.Serves files. All component code runs in the browser.
ScalingBounded by memory and connections. More than one instance needs sticky routing, because a circuit lives on one machine.Static files behind a CDN. Scaling becomes your API's problem rather than the UI's.
What reaches the browserNothing except what you render, so configuration stays on the server.Everything in wwwroot, including any configuration file placed there.
What the host must get rightWebSockets allowed through every proxy in front of it, and an idle timeout longer than a person's think time.Correct MIME types for the runtime files, compression served properly, and a fallback to index.html for unknown paths.
Publishing, and knowing which part of the output to deploy
Shell
# Always Release for a deployment
dotnet publish -c Release -o ./publish

# Blazor Server: the whole folder is the deployment, and it runs
cd publish
./EmployeePortal

# Standalone WebAssembly: only wwwroot is deployed
ls ./publish/wwwroot
# index.html  favicon.ico  css/  _framework/

# The compressed copies the host should be serving
ls ./publish/wwwroot/_framework/*.br | head
  • -c Release matters more for WebAssembly than for Server. A Release publish trims unused code from the runtime and your assemblies, and the download is substantially smaller. Publishing Debug output is a common and quiet cause of a slow first load.
  • For Blazor Server, everything in the output folder is the deployment. The wwwroot inside it is static content served by the same application.
  • For standalone WebAssembly, only wwwroot is deployed. The rest of the publish output is build machinery, and copying all of it to a static host is how people end up with a site that serves a directory listing.
  • _framework holds the runtime, your assemblies, and a manifest listing every file with a hash. If anything in the pipeline rewrites or re-compresses those files in a way that changes them, the integrity check fails and the application does not start.
  • Recent versions package assemblies as .wasm files rather than .dll, partly because some networks and hosts block .dll downloads outright.
  • Trimming removes code it cannot see being used, which includes code reached only through reflection. Something that works in development and throws about a missing member after publishing is a trimming problem before it is anything else.
  • Ahead-of-time compilation is available for WebAssembly. It speeds up compute-heavy code and makes the download larger, so it earns its place in a calculation-heavy application and not in a set of CRUD screens.
Static hosting settings for a published WebAssembly app — the file name varies by host, the decisions do not
JSON
{
  "mimeTypes": {
    ".wasm": "application/wasm",
    ".dat": "application/octet-stream",
    ".blat": "application/octet-stream"
  },
  "navigationFallback": {
    "rewrite": "/index.html",
    "exclude": ["/_framework/*", "/css/*", "/images/*"]
  },
  "globalHeaders": {
    "cache-control": "no-cache"
  }
}
  • A .wasm file served as text/plain is refused by the browser's streaming compiler, and the application fails at startup with a message about a module rather than anything resembling your code. Serving it as application/wasm is a host setting, not something you can fix in the project.
  • The navigation fallback exists because your routes are client-side. Ask for /employees/42 directly and the host has no such file, so it must return index.html and let the router work the address out.
  • Excluding the framework and asset folders from that fallback matters. Without the exclusion, a genuinely missing file returns an HTML page with a 200, and the error you get back is much stranger than a 404.
  • Compression is the largest single win available. Publishing produces Brotli and gzip copies of the framework files; whether they are used depends on the host serving them with the right Content-Encoding. The difference is measured in megabytes, so confirm it in the browser's network tab rather than assuming it.
  • Cache the hashed framework files hard and index.html not at all. The framework file names change when their contents change, so they are safe to cache for a long time. index.html is the file that points at them, so caching it means people keep running the version from before your last deploy.
  • None of this applies to Blazor Server, where ASP.NET Core serves the static content and already knows these types.
Hosting under a sub-path, when the application is not at the site root
C#
@* Components/App.razor, or wwwroot/index.html for standalone WebAssembly *@
<head>
    <base href="/portal/" />
</head>

// Program.cs on the host, when the application lives under /portal
app.UsePathBase("/portal");
app.UseStaticFiles();
app.UseRouting();
  • The base element tells the browser and the router where the application's root is. At the site root it is a single slash; under /portal it must be "/portal/", with the leading and trailing slash both present.
  • Get it wrong and the symptoms point away from the cause. The page loads and the framework files return 404. Or navigation works until someone refreshes. Or every relative link resolves one level too high and nothing explains why.
  • UsePathBase on the host strips the prefix before routing, so your endpoints and components do not each have to know they are living under one.
  • NavigationManager resolves addresses against the base, so an application that might move under a sub-path is safer with relative addresses than with hard-coded absolute ones.
  • If one build is deployed to more than one path, the base href has to be set per deployment. That belongs in the release pipeline, not in a file someone edits on the server after the fact.
  • Test the sub-path deployment before you need it. It is the deployment difference most likely to be discovered late, because everything works at the root on a development machine.

Summary

  • Blazor Server publishes as an ASP.NET Core application that runs and holds state; standalone WebAssembly publishes as static files in wwwroot
  • Publish in Release for WebAssembly, deploy only wwwroot, and confirm the host serves the compressed framework copies
  • A static host must return application/wasm for runtime files and fall back to index.html for client-side routes, excluding the framework folder
  • Under a sub-path, base href and UsePathBase have to agree, and the symptoms of getting it wrong point away from the cause
  • Blazor Server needs WebSockets through every proxy and sticky routing across instances, because a circuit lives in one machine's memory

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Publish both ways and look at what you get

Publish a Blazor Server application and a standalone WebAssembly application, both in Release, to separate folders.

Compare the total size, list the contents of _framework in the WebAssembly output, and find the compressed copies. Then work out, for each, exactly which folder you would hand to a host.

Show solution

The Server output is an application: an executable, your assemblies, and a wwwroot of static content. All of it is the deployment, and something has to run it. The whole folder goes to the host.

The WebAssembly output contains a wwwroot that is the deployment, and a lot of surrounding material that is not. Only wwwroot goes to a static host. Getting that wrong is the single most common first-deployment failure, and the symptom is a site that appears to be missing.

Inside _framework you will find the runtime, your assemblies as .wasm files, a manifest, and .br and .gz copies of most of it. The compressed copies only help if the host serves them with the right Content-Encoding, so confirm it in the browser's network tab and look at the transferred size rather than the file size.

Compare the totals and the first-load figures rather than the folder sizes alone. That comparison is the concrete version of the trade-off from the first lesson: the Server download is small and every interaction costs a round trip; the WebAssembly download is large once and interactions cost nothing on the network.

Shell
dotnet publish ./EmployeePortal.Server -c Release -o ./out/server
dotnet publish ./EmployeePortal.Client -c Release -o ./out/client

# What a static host needs, and nothing else
du -sh ./out/client/wwwroot

# What the first visit actually downloads is smaller than this,
# provided the host serves the .br copies
du -sh ./out/client/wwwroot/_framework

Think about it

One origin or two?

A WebAssembly portal talks to an internal API. You can host the static files on a CDN with the API on its own origin, or serve the static files from the ASP.NET Core API project so both share one origin.

Argue both, then commit to one for an internal HR portal used by staff in two offices.

Show solution

Separate origins give you a CDN in front of the UI, independent deploys, and scaling that treats the two as the different things they are. The cost is cross-origin work: CORS configuration on the API, cookies that need explicit handling to be sent at all, and a preflight request in front of some calls. None of that is difficult and all of it is one more thing that can be misconfigured in one environment only.

One origin removes all of that. Cookies work the way they do everywhere else, there is no CORS policy to keep in step, and there is one deployment. The cost is coupling: a UI change means redeploying the API, and static files are served by an application rather than by something built for it.

For an internal portal used by staff in two offices, one origin is the better answer. The traffic does not justify a CDN, the deployment is simpler, and the authorisation story is simpler because the cookie situation is simpler. Simple wins where scale is not a constraint.

The answer changes with the facts. A public application with users across several regions, or a UI that deploys several times a day independently of its API, tilts the other way. What matters is that the CORS and cookie work is a known cost rather than a surprise discovered in staging.

Saved in this browser only.

End of the published lessons

That is everything written so far in Blazor

More lessons in this course are on the way. In the meantime, the course page shows the full roadmap, and the projects are the best way to consolidate what you have covered.