You do not need to rewrite an existing application to use WASM. Instead, you should compile a single hot spot, such as an image filter or a pricing engine, into a .wasm module and call it from your existing JavaScript source code. A hot spot is defined as a small portion of the source code that will consume most of the processor cycles. For example, this might be an image filter or a pricing engine. On the server side, runtimes such as Wasmtime load .wasm modules within a host process, thereby making WASM a safe, language-neutral plugin format for existing systems.

  • Sidebar found, but no caption specified. Sidebar cannot be added:

Neither stage of this pipeline runs inside the web browser. By default, Blazor WebAssembly applications send your IL assemblies down to the web browser where a Mono .NET runtime, compiled to WebAssembly and running with an interpreter, runs the IL. Interpretation maintains a small download size but is slow on CPU-bound work. Ahead of Time (AOT) closes this gap by directly compiling your IL to WebAssembly at publish time.

This article helps you learn how to leverage WebAssembly and .NET to build performant, scalable, cross-platform applications with stellar security. We'll look at Blazor WebAssembly, AOT compilation, and the evolving WASI-based Windows Workloads, explaining how they fit together and why WASM is an appealing option for building compute-heavy and highly responsive applications.

If you're to work with the code examples discussed in this article, you need the following installed in your system:

  • Visual Studio 2026
  • .NET 10.0
  • ASP.NET 10.0 Runtime
  • Entity Framework Core

If you don't already have Visual Studio 2026 installed in your computer, you can download it from here: https://visualstudio.microsoft.com/downloads/.

Understanding the Problem

For many years, building an application that can run in the web browser, on the desktop, or on a mobile device has forced a compromise: you have either had to duplicate the codebase for each platform, i.e., write separate complete codebases for the same application, or take your web application and wrap it as a natively installed application. However, the end users of today's applications do not care which device the application runs on; they only expect it to work the same way on each platform. Essentially, today's users want the application to be responsive and to behave consistently regardless of where it is running.

An Overview of WebAssembly (WASM)

WebAssembly (WASM) is a compact binary instruction format. It runs at (almost) native speed while strict browser-enforced security sandboxing is in effect. WASM runs on a stack-based virtual machine and has two formats: binary for execution and text (WAT) for reading and debugging modules.

In .NET applications, WASM helps achieve dual benefits by working on the same C# codebase, libraries, and test suite. An application can be executed in a browser via Blazor WebAssembly, on a server via ASP.NET Core, and outside of a browser via a WASI-targeted library/component. With .NET 10, AOT (Ahead-of-Time) compilation support for WebAssembly is available, closing performance gaps compared to earlier releases.

WASM is unique in that its format is not targeted to a specific CPU architecture; it is a virtual instruction set architecture that can be easily converted into the native machine code of the running chip. Because of this abstraction, the same compiled module (.wasm) can run across operating systems (e.g., Windows, Mac, Linux, iOS) and also on different IoT devices without the need for recompilation.

Benefits

The key benefits of WASM include:

Language neutral: WASM is neutral in terms of language—developers can create a WASM/WebAssembly executable using any programming language that has a back-end compiler that will compile it into WASM/WebAssembly (i.e., C++, Rust, Go, C#). Developers can use their existing libraries and codebases to build web applications.

Portability: WASM is designed to be a platform-independent runtime for many different types of devices such as servers, edge devices, and embedded devices. Since all WASM modules are compiled into a single binary, the same WASM binary can be executed on both server-side applications and client-side applications.

Interoperability: The ability to communicate easily with JavaScript means that WebAssembly modules can be used in your web applications, allowing a gradual transition to WebAssembly in your web environment. In other words, interoperability support enables WebAssembly to share data with JavaScript engines and modules written in other programming languages.

Performance: WebAssembly is designed for performance-critical tasks, i.e., those tasks that require more resources such as video editing, gaming, etc. Because a WASM/WebAssembly binary is small, it uses fewer resources, i.e., minimal storage, transfer, and loading time than an equivalent-sized JavaScript file. When compiled, WebAssembly programs execute at almost the same speed as native (compiled) code because a WASM/WebAssembly file is an efficient format that compiles with very low overhead, unlike JavaScript, which is interpreted.

Security: The WASM/WebAssembly code runs in an isolated, protected memory space (also known as a sandbox); thus, a WASM/WebAssembly module cannot arbitrarily read or write to memory or resources outside those granted by the host system. This isolation from the host platform ensures that malicious code cannot affect the system. WASM/WebAssembly uses strong memory bounds checking on linear memory and a strongly typed instruction set, which greatly reduces the likelihood of many types of undefined behavior bugs common in native code written in other languages.

Architecture Components

The WASM architecture consists of several core components working together:

Module: Within the context of WASM, modules are defined as deployable units represented by a binary file containing everything that the system needs such as functions, memory, tables, etc.

Stack-based virtual machine: The stack-based virtual machine allows instructions to push and pop from a single implicit operand stack rather than the multiple named registers used in traditional programming, making it more efficient to write and easier to validate.

Linear memory: This is a continuous, resizable block of memory used by the module, just like the RAM a CPU uses. The linear memory will be used by the host environment for all your host function calls, as well as by the module itself.

Tables: In the WASM architecture, tables are a way of holding an array of indexed items (usually function references).

Imports/exports: Imports and exports are how modules access functions in their host environment (e.g., via JavaScript) and how they expose their functions and memory to the host environment.

Host environment: The embedder/host environment is the environment around the module (the host's JavaScript engine or standalone runtime) that loads the module into memory, creates any required imports from the host environment, and makes calls to exported functions in the binary from the host environment.

How Does It Work?

The process from compilation of the source code to execution consists of the following steps:

  1. Compile: In this step, the source code written using C#, Rust, or C++ is compiled into a .wasm binary file via the respective language tool-chain, i.e., not native machine code.
  2. Load and validate: In this step, the host runtime reads the binary and performs validation which consists of confirming types, the amount of data on the stack and limits on memory allocation.
  3. Instantiate: In this step, the runtime will allocate space for linear memory and create tables for linking to import functions from the host (e.g., DOM APIs via JavaScript), and to create an instance of the created module.
  4. Execute: In this step, the runtime will either execute bytecode or compile the bytecode into machine code just before or after it runs it in the module's sandbox.
  5. Interop: In this step, the functions exported from the source are invoked from JavaScript and vice versa by sending data via linear memory or component-based interfaces.

Figure 1 demonstrates the entire process diagrammatically.

Figure 1: The WASM architecture
Figure 1: The WASM architecture

The WebAssembly System Interface (WASI) is a group of standards-based API specifications for software compiled to the W3C WebAssembly (Wasm) specification. Figure 2 shows the execution flow of WebAssembly System Interface (WASI) in a sandboxed portable environment in which common APIs handle file, and network operations without exposing the host to malicious code.

Figure 2: The execution flow of WASI
Figure 2: The execution flow of WASI

An Introduction to Blazor WebAssembly (WASM)

Blazor is an open-source framework developed by Microsoft for building interactive web applications using C# and .NET. Blazor supports the following hosting models:

  • Blazor WebAssembly (WASM): Allows applications to run completely in the browser since they utilize WebAssembly.
  • Blazor server: This model involves the applications running on the server and sending the updates to the client components using SignalR.
  • Blazor hybrid: This model combines the main elements of both technologies and lets developers use Blazor technology in applications built with .NET MAUI in order to create native apps for desktop or mobile devices.

Key Features

Here's a quick look at the key features of Blazor:

  • Support for component-based architecture
  • Support for authentication and authorization
  • Seamless integration with the .NET ecosystem
  • Platform independence
  • Support for multiple hosting models
  • Ability to use C# throughout—both in the server side and in the client side

Blazor WebAssembly (WASM) is a single-page application framework for creating state-of-the-art client-side web apps powered by .NET that run in any web browser. Blazor WASM loads the whole app from the app's core logic to the UI components, including its requirements and .NET Core engine, into the web browser. Hence, whenever you run an application or a web page, the script responsible for the app's client-side behavior is also retrieved.

How Does Blazor Work?

Blazor is a Microsoft framework that allows you to run C# in your web browser without the need to use any plugins. You can use Blazor to build modern-day applications using C# as a full-stack development tool. Applications developed using Blazor run inside the context of your web browser. Once you compile such an application, one or more files are loaded into the browser and executed. A standalone Blazor WebAssembly application does not need a server-side .NET backend component to run. Blazor applications can access web services using HTTP REST APIs.

A Blazor application includes optional reusable components containing a C#, HTML, and CSS conglomerate that can run both on the server and client sides. These components define the structure and behavior of the user interface. These are .NET C# classes enclosed in .NET assemblies supporting event handling logic and user events, which can be reused and be made available as Razor class libraries or NuGet packages. Components handle user interaction using events such as button clicks, which trigger updates to the state of the components.

Blazor Components

Blazor applications are built around the concept of components. These components take the form of self-contained, reusable units of UI and logic written in razor syntax, which combines HTML, CSS, and C#. These components specify your application's logic and structure, making multilevel web development possible.

  • Blazor server: Applications rendered in this mode run on the server
  • Blazor WebAssembly: Applications rendered in this mode run in the browser
  • Auto render mode: This mode is a combination of both the preceding modes

Real-World Architectural Patterns

Below are the architectural patterns that have been used repeatedly in many production systems, and each of them leverages a feature of the WASM-plus-.NET technology stack: in the hybrid model, one can reuse code, offline-first design gives the benefit of being independent, and in embedded modules and micro-frontends, safe composition is a key characteristic.

Hybrid Applications: Blazor WASM (Front End) and ASP.NET Core (Back End)

In this pattern, a Blazor WebAssembly front end pairs with an ASP.NET Core back end in a single solution. Do not confuse this pattern with Blazor Hybrid, which wraps Blazor components in a native shell via .NET MAUI for desktop and mobile apps. Hybrid applications allow you to write a single user interface logic just once and then reuse that code across all the different platforms that need to be supported (a browser, either Blazor WebAssembly or server, as well as a native desktop or mobile application.

The client application runs in the browser, whereas data access, authentication, and confidentiality are handled by an ASP.NET Core API. The benefit of this approach compared to JavaScript SPAs is that code is shared: both parties rely on the same data transfer objects, validation, and business logic from a common library.

Assume that you have a record type called OrderRequest used by both the client and the server as shown in the code snippet given below.

public record OrderRequest
{
    [Required, StringLength(64)]
    public string CustomerId { get; init; } = string.Empty;

    [Range(1, 10_000)]
    public int Quantity { get; init; }

    public bool IsApprovedForBulkDiscount() => Quantity >= 250;
}

The client will use this type in the EditForm to receive validation feedback without delay. At the same time, the API will validate the same type. There is only one type; thus, there are no inconsistencies.

As the contract is type-safe when a change is made on either side, its failure occurs at compile-time rather than when the application is deployed and running in production.

The following code example shows the client-side class responsible for sending the order request.

public class OrderClient(HttpClient http)
{
    public async Task<OrderResponse> SubmitAsync(OrderRequest request)
    {
        var response = await http.PostAsJsonAsync("api/orders", request);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<OrderResponse>();
    }
}

The ASP.NET Core Hosted template option was removed in .NET 8. In .NET 10, a Blazor Web App with the WebAssembly interactive render mode helps implement this architecture from scratch, and Blazor Web Apps allow using both server-side rendering and Blazor rendering for each component.

Micro Frontends

Micro frontend architecture is a methodology for structuring software applications by breaking up the functionality of a single-page application (SPA) into multiple components. Typical examples of micro frontends include a text box, a search box, and a shopping cart dashboard. Each component is implemented independently, allowing for rapid development, scalability, and testability. Each of these components (or micro frontends) can solve a specific business functionality and therefore can be created, tested, and deployed separately, resulting in faster development and facilitating more scalable, complex web applications.

Figure 3 below illustrates a typical micro frontend architecture.

Figure 3: A typical micro frontend architecture
Figure 3: A typical micro frontend architecture

What Are Progressive Web Applications?

Progressive web applications (PWAs) are web applications that provide users with a rich experience regardless of the platform on which they execute. PWAs are known for their ability to function while offline. Blazor facilitates the development of PWAs with offline support through features such as caching, service workers, and blob storage.

The following are the features of PWAs:

  • Progressive enhancement: PWAs can work on any device or platform, enabling an intuitive user experience on desktops and mobile devices alike.
  • Responsive design: PWAs provide an enriched user experience by following responsive web design principles across devices and platforms.
  • Offline support: PWAs can work offline or in areas with limited connectivity so that you can access the application even when there is no connectivity.

Key Benefits

Progressive web applications provide several benefits such as the following:

  • Reliability: An offline-first PWA ensures that your applications continue to function even in a scenario where there is no active internet connection.
  • Cross-platform compatibility: One codebase of PWA runs on any device, whether mobile phones or desktops and tablets, and doesn't require you to develop an app separately for every OS.
  • Offline accessibility: Thanks to service worker caching the app shell and its content, a user can continue to access the application even with poor or no connectivity.
  • Cost-effective: Developing a siloed single PWA is often less expensive than creating dedicated iOS, Android, and web apps; as such, they are easier to launch with just one codebase.
  • Improved performance: Offline-first caching can allow applications to load faster than the non-offline versions, as certain data is cached, thereby reducing the number of requests sent to the server.

Key Components

The key components of PWAs include the following:

  • Service worker: A service worker is basically a script that behaves like an intermediary service in the background and has the capability of caching. When you use Blazor, it comes with a default service worker that can be modified as you need.
  • Caching: Caching is a strategy of storing data so that the same data can be used by the application when the application does not have access to internet.
  • Data synchronization: When it is necessary to synchronize user data with the database, it becomes essential to store user data locally and later synchronize this data with the server when internet comes back.

Figure 4 below illustrates a typical progressive web application in action.

Figure 4: A typical progressive web application (PWA)
Figure 4: A typical progressive web application (PWA)

Consider the following piece of code that demonstrates how a note taking application can work even when it is offline.

public class NotesService(
    ILocalStore store,
    HttpClient http)
{
    public async Task SaveNoteAsync(Note note)
    {
        note.IsSynced = false;
        await store.SaveAsync(note);
        await TrySyncAsync();
    }

    public async Task TrySyncAsync()
    {
        foreach (var note in await store.GetUnsyncedAsync())
        {
            try
            {
                await http.PostAsJsonAsync("api/notes", note);
                note.IsSynced = true;
                await store.SaveAsync(note);
            }
            catch (HttpRequestException)
            {
                return;
            }
        }
    }
}

In the preceding piece of code, the SaveNoteAsync method is called when the user clicks on the Save button. Most importantly, this method can work even in the offline mode, i.e., when there is no internet connectivity. Since there is no connectivity, the IsSynced property of the Note object is set to false.

As soon as the connectivity is restored, the TrySyncAsync method can synchronize data with the server and set the IsSynced property of the Note instance to true. Note that for the sake of simplicity, this implementation is just a prototype.

Just in Time (JIT) Versus Ahead of Time (AOT) Compilation

With the initial release of the .NET Framework in 2002, the Just-In-Time (JIT) compiler was used to compile your source code. Since then, several versions of .NET have been released. Blazor WASM AOT was shipped in .NET 6; .NET 7 introduced Native AOT for console and server applications. In this section we'll explore each of these compilation techniques in detail.

JIT Compilation

JIT is the default compilation strategy used in .NET applications. Here, the Roslyn compiler converts your source code into Intermediate Language (IL) and metadata. Remember that any code targeting the .NET CLR is never converted directly to machine code. Once the IL is available, the CoreCLR runtime compiles the IL code into the corresponding native machine code. To reduce startup time and optimize performance, the CoreCLR uses tiered compilation. In this strategy, the CoreCLR runtime compiles methods at a lower optimization level (Tier 0). However, if a method is heavily used, it gets recompiled through Tier 1 with the necessary advanced optimizations.

Figure 5 illustrates how the entire JIT compilation process works.

Figure 5: The standard .NET execution pipeline (uses JIT compiler)
Figure 5: The standard .NET execution pipeline (uses JIT compiler)

Ahead-of-Time (AOT) Compilation

Ahead-of-time (AOT) compilation compiles your source code into machine code rather than IL code. Essentially, with AOT compilation, your managed source code is compiled into executable code (machine code) rather than IL code.

The key benefits of using AOT compilation are:

  • Creates standalone executables
  • Faster startup times
  • Consumes fewer memory resources
  • Suitable for large-scale cloud deployments

Figure 6 below demonstrates the AOT compilation process.

Figure 6: Ahead-of-time (AOT) compilation
Figure 6: Ahead-of-time (AOT) compilation

To enable AOT compilation in your Blazor WebAssembly application, you should first install the .NET WebAssembly Build Tools. To do this, you can either use the Visual Studio Installer, or execute the following command in a shell window:

dotnet workload install wasm-tools

Next, you should specify the following configuration in the project file of the Blazor WebAssembly project:

<PropertyGroup>
  <RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>

Enable / Disable Trimming

Note that when you publish your application, the Blazor WebAssembly trims any unused code from your assemblies by default. You can control this behavior by configuring the PublishTrimmed setting, as shown in the following code snippet.

<PublishTrimmed>true</PublishTrimmed>

Once AOT compilation is complete, the original IL code is stripped from your compiled methods to reduce the download size. However, you can control this by configuring the WasmStripILAfterAOT setting. If you would like to retain the IL, you should specify the following piece of code in the project file:

<PropertyGroup>
  <RunAOTCompilation>true</RunAOTCompilation>
  <WasmStripILAfterAOT>false</WasmStripILAfterAOT>
</PropertyGroup>

PublishAot

Here is a note of caution: The PublishAot property enables Native AOT for console and server applications only, i.e., it does not work for your Blazor WebAssembly projects. Instead, you should use the RunAOTCompilation setting for your Blazor WebAssembly projects.

Native AOT is a. NET compilation and publishing mode, which compiles your application directly into a self-contained native executable at build time. The resultant compiled code starts faster, uses less memory, and suits containers and CLI tools.

Additionally, you should know that AOT compilation in your WebAssembly projects will only work if you publish the application in the Release configuration mode using the following command:

dotnet publish -c Release

Interoperability Between .NET, JavaScript and Native APIs

Blazor WebAssembly executes managed .NET code in the web browser which in turn exposes DOM, local storage, and the clipboard capabilities through JavaScript APIs.

Calling JavaScript Through .NET

To call JavaScript through .NET, you should pass in the IJSRuntime abstraction into a component and then access your JavaScript functions by name. While the InvokeVoidAsync calls a method and does not return a value, the InvokeAsync<T> method returns an actual value that is deserialized to the right .NET type.

@inject IJSRuntime JS

private async Task CopyLinkAsync(string url)
{
    await JS.InvokeVoidAsync("navigator.clipboard.writeText", url);
}

Calling .NET Through JavaScript

To call a .NET method from JavaScript, use the [JSInvokable] attribute in a static method as shown in the code snippet given below.

[JSInvokable]
public static Task<int> GetCartCount()
{
    return Task.FromResult(CartState.Count);
}

You can then invoke the method from JavaScript as shown in the following snippet:

DotNet.invokeMethodAsync('MiniCommerce.Client', 'GetCartCount').then(count => updateBadge(count));

Accessing Browser APIs

Although each interop call does cross the serialization layer, this cost can add up when we are making calls on hot paths. Concatenate related calls to one JavaScript function, reduce payloads, and use IJSObjectReference to store JavaScript objects in memory without having to resolve the same module every time. For high-frequency operations, prefer [JSImport] and [JSExport], which were introduced in .NET 7. Note that these attributes can produce marshaling code at compile time and skip the reflection-based path.

Using WASI for System-level Capabilities

Although there is no JavaScript host outside the web browser environment, the WebAssembly System Interface (WASI) provides modules with restricted access to clocks, files, sockets, and environment variables

Implementing a Real-life E-commerce Application Using WASM and .NET 10

In this section, we'll implement a real-life e-commerce application using WASM and ASP.NET Core. MiniCommerce is a reference application that illustrates how to implement a high-performing, secure ASP.NET Core application with Blazor WebAssembly and EF Core on .NET 10 within a single code base. It shows all the techniques for reducing rendering overhead such as managing state on the client side, using lazy-loading, and reducing payload, using AOT compilation and caching, and implementing Brotli compression and optimizing the startup time of your ASP.NET Core application.

For security purposes, the MiniCommerce shows how to use the real boundaries of the WASM sandboxing model, how to perform server-authoritative pricing at checkout, how to securely authenticate your users using JWT and role-based authorization, how to implement rate limiting, and how to safely manage client-side state.

The MiniCommerce application is built using WASM and .NET 10 that demonstrates a sample product catalog with three separate categories: electronics, furniture, and automobile. The application provides a shopping cart, enables user registration and login and checkout. Additionally, the application includes a separate administrative area to add or manage any of the products in the catalog. Note that this application does not have all the features of a fully functional e-commerce site; therefore, it does not support payment processing and order history. Also, the product images used in this application are placeholder graphics instead of actual photos.

Architecture Overview

A hosted Blazor WebAssembly (WASM) app is an approach to developing web apps in which the ASP.NET Core back end sends a Blazor WASM app to the client's browser. It separates the code into two entities, the client and the server, allowing one to develop both the complete front end and back end of the application using C# without the need for JavaScript.

The application demonstrated in this example follows the traditional “hosted Blazor WebAssembly (WASM)” architecture. In other words, it is a WebAssembly client running in a browser. It consists of a client-side browser form that is compiled to WebAssembly plus a C# application back end that serves the compiled web application (static files) as well as creates the JSON-based interface the web application relies upon.

The MiniCommerce application includes the following projects:

  • MiniCommerce.Shared: This project contains the DTOs referenced by both client and server.
  • MiniCommerce.Client: This project is the Blazor WebAssembly app that compiles to WASM and runs entirely inside your web browser's script sandbox.
  • MiniCommerce.Client.AdminModule: This project is a Razor class library that contains the admin pages.
  • MiniCommerce.Server: This project represents the ASP.NET Core host that serves the compiled client, exposes the API endpoints, and contains the EF Core DbContext and all business logic methods.

Figure 7 shows a high-level architecture diagram.

Figure 7: A high-level architecture diagram
Figure 7: A high-level architecture diagram

Technology Stack

We'll use the following technologies to build this application:

  • Client: The user interface is created using Blazor WebAssembly (.NET 10), that is AOT-compiled and trimmed at publish.
  • Server: The server application is built using ASP.NET Core Web APIs (.NET 10).
  • Database: This application uses a PostgreSQL database to store data.
  • ORM: This application uses EF Core as the ORM.
  • Auth: The MiniCommerce application uses JWTs (HMAC-SHA256) for authentication.
  • Caching: To cache data, this application uses caching at both the client side and the server side. While it uses a service worker (Cache Storage API) at the client side, it uses ASP.NET Core output caching at the server.

You can use the dotnet publish command, which will automatically run the IL trimmer. The IL trimmer is a component that assesses the application's entry points and all accessible pathways and then removes all unused elements in the application from the final product.

Cross-Cutting Concerns

Cross-cutting concerns are features that provide functionality that cut across multiple components and layers of an application (e.g., authentication, logging, error handling, caching). The term “cross-cutting” is based on the idea that cross-cutting features typically affect multiple components of an application's layered architecture. Cross-cutting concerns address two primary issues: code redundancy and inconsistency.

Without a defined approach to managing cross-cutting concerns, individual controllers will implement their own version of try/catch statements, authentication, logging, etc. As these implementations evolve over time, it is easy for inconsistencies to arise between them. For example, in the case of MiniCommerce, each of the four controllers will be working with its own implementation of JWT validation and with its own version of limiting the number of requests, compression level, etc.

The following cross-cutting concerns have been used in the MiniCommerce application.

Authentication and Authorization

To implement authentication, this application uses JSON Web Tokens (JWT). Note that JWT bearer auth is configured once in Program.cs; enabling each controller to use authentication via attributes instead of custom code to validate authentication.

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));

[HttpPost]
[Authorize(Policy = "AdminOnly")]
public async Task<ActionResult> CreateProduct([FromBody] ProductDto dto)

Response Compression

The MiniCommerce application uses Brotli for implementing response compression. The following code snippet shows how the BrotliCompressionProvider is registered globally in the Program.cs file.

builder.Services.AddResponseCompression(options =>
{
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
});

Caching

The MiniCommerce application uses server output caching via a named policy that is applied per action. On the client side, caching is implemented via the service worker.

[HttpGet]
[OutputCache(PolicyName = "catalog")]
public async Task<ActionResult<List<ProductDto>>> GetProducts(...)

Rate Limiting

This application implements rate limiting using fixed-window policies.

options.AddFixedWindowLimiter("checkout",
    o =>
    {
        o.PermitLimit = 5;
        o.Window = TimeSpan.FromMinutes(1);
    });

[EnableRateLimiting("checkout")]
public async Task<ActionResult<OrderDto>> Checkout(...)

**Error Handling

In this application, error handling is implemented using a server-side global handler instead of implementing error handling for each endpoint using the try/catch blocks. The following code snippet shows how error handling is implemented globally in this application.

app.UseExceptionHandler("/error");

Cross Origin Resource Sharing (CORS)

In this application, cross origin resource sharing (CORS) is implemented using a named, origin-restricted policy rather than AllowAnyOrigin() as shown in the code snippet below.

options.AddPolicy(
    "DefaultClient",
    policy => policy
        .WithOrigins(builder.Configuration["AllowedOrigin"] ?? "https://localhost:5001")
        .AllowAnyHeader()
        .WithMethods("GET", "POST", "PUT", "DELETE"));

Configuration and Secrets Management

The following code snippet shows how fail fast has been implemented for every secret at startup instead of silently defaulting.

var jwtKey = jwtSection["Key"]
    ?? throw new InvalidOperationException("Jwt:Key is not configured...");

Client-side State Propagation

The following piece of code shows how CartState/AuthState uses a simple OnChange event, thereby enabling any component to subscribe the same way.

public class CartState
{
    public event Action? OnChange;

    public void AddItem(CartItemDto item)
    {
        /* ... */ NotifyStateChanged();
    }

    private void NotifyStateChanged() => OnChange?.Invoke();
}

Data Model

The MiniCommerce database design comprises a catalog part (categories and items), an identity part (users), and a transactional part (orders, ordered items).

  • The categories table is, as its name suggests, the catalog.
  • The products table has all the products for sale.
  • The users table is the identity store that stores user credentials.
  • The orders table stores all items purchased by the customer, including their purchase date, total cost, and status, i.e., whether the order is purchased, shipped or delivered.
  • The order items table holds information about items included in orders, i.e., it records the contents of a particular order including which products have been bought in that order, in what quantities, and at what prices.

The difference between the orders and the orders items tables is that the first one stores receipt data, while the other one stores a line of that receipt. Listing 1 shows the complete database script for each of these tables.

Listing 1: Database script

 BEGIN;
CREATE TABLE categories (
  id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 
  name text NOT NULL
);
CREATE TABLE products (
  id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 
  name text NOT NULL, 
  description text NOT NULL DEFAULT '', 
  price numeric(18, 2) NOT NULL, 
  image_url text NOT NULL DEFAULT '', 
  stock integer NOT NULL, 
  category_id integer NOT NULL, 
  CONSTRAINT fk_products_categories_category_id 
FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE
);
CREATE INDEX ix_products_category_id ON products (category_id);
AppDbContext.Users 
CREATE TABLE users (
  id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 
  email text NOT NULL, password_hash text NOT NULL DEFAULT '', 
  role text NOT NULL DEFAULT 'Customer'
);
CREATE UNIQUE INDEX ix_users_email ON users (email);
CREATE TABLE orders (
  id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 
  user_id text NOT NULL, 
  created_at_utc timestamptz NOT NULL, 
  total numeric(18, 2) NOT NULL
);
CREATE INDEX ix_orders_user_id ON orders (user_id);
CREATE TABLE order_items (
  id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 
  order_id integer NOT NULL, 
  product_id integer NOT NULL, 
  quantity integer NOT NULL, 
  unit_price_at_purchase numeric(18, 2) NOT NULL, 
  CONSTRAINT fk_order_items_orders_order_id 
FOREIGN KEY (order_id) 
REFERENCES orders (id) ON DELETE CASCADE, 
  CONSTRAINT fk_order_items_products_product_id 
FOREIGN KEY (product_id) 
REFERENCES products (id) ON DELETE RESTRICT
);
CREATE INDEX ix_order_items_order_id ON order_items (order_id);
CREATE INDEX ix_order_items_product_id ON order_items (product_id);
INSERT INTO categories (id, name) 
VALUES 
  (1, 'Electronics'), 
  (2, 'Furniture'), 
  (3, 'Automobile');
INSERT INTO products (
  id, name, description, price, image_url, 
  stock, category_id
) 
VALUES 
  (
    1, 'HP Laptop', '15.6-inch, 16GB RAM, 512GB SSD.', 
    999.99, 'img/laptop.png', 20, 1
  ), 
  (
    2, 'DELL Mouse', 'Wireless ergonomic mouse.', 
    24.99, 'img/mouse.png', 150, 1
  ), 
  (
    3, 'Keyboard', 'Mechanical, RGB backlit.', 
    79.99, 'img/keyboard.png', 80, 1
  ), 
  (
    4, 'Samsung Monitor', '27-inch 4K IPS display.', 
    349.00, 'img/monitor.png', 25, 1
  ), 
  (
    5, 'Office Chair', 'Ergonomic mesh back, adjustable height.', 
    189.99, 'img/office-chair.png', 
    40, 2
  ), 
  (
    6, 'Wooden Desk', 'Wooden Laminated Study Table.', 
    259.00, 'img/wooden-desk.png', 15, 
    2
  ), 
  (
    7, 'Bookshelf', 'Enigmatic Wooden Bookshelf.', 
    129.50, 'img/bookshelf.png', 22, 
    2
  ), 
  (
    8, 'Sofa', 'Rosebell Wooden Four Seater Sofa.', 
    599.00, 'img/sofa.png', 10, 2
  ), 
  (
    9, 'BMW', 'BMW 3 Series sedan.', 50000.00, 
    'img/bmw.png', 3, 3
  ), 
  (
    10, 'Mercedes', 'Mercedes-Benz C-Class.', 
    45000.00, 'img/mercedes.png', 2, 
    3
  ), 
  (
    11, 'Toyota', 'Toyota Corolla Sedan.', 
    30000.00, 'img/toyota-corolla.png', 
    5, 3
  ), 
  (
    12, 'Ford', ' Ford Bronco.', 45000.00, 
    'img/ford-bronco.png', 4, 3
  );
SELECT 
  setval(
    pg_get_serial_sequence('categories', 'id'), 
    (
      SELECT 
        MAX(id) 
      FROM 
        categories
    )
  );
SELECT 
  setval(
    pg_get_serial_sequence('products', 'id'), 
    (
      SELECT 
        MAX(id) 
      FROM 
        products
    )
  );
COMMIT;

Integrating Blazor WebAssembly Into an Existing ASP.NET Core Web Application

In this section, you'll examine how you can integrate a Blazor WebAssembly application with an existing ASP.NET Core Web Application.

Create a New Blazor Web Assembly Project in .NET 10 and Visual Studio 2026

You can create a project in Visual Studio 2026 in several ways, such as from the Visual Studio 2026 Developer Command Prompt or by launching the Visual Studio 2026 IDE. When you launch Visual Studio 2026, you'll see the Start window. You can choose “Continue without code” to launch the main screen.

Now that you know the basics, let's start setting up the project. To create a new Blazor Web Project in Visual Studio 2026:

  1. Start the Visual Studio 2026 IDE.
  2. In the Create a new project window, select Blazor Web App and click Next to move on.
  3. Specify the project name as Blazor_WebAssembly_Demo and the path where it should be created in the **Configure your new project **window.
  4. If you want the solution file and project to be created in the same directory, you can optionally check the Place solution and project in the same directory checkbox. Click Next to move on.
  5. In the next screen, specify the target framework and authentication type as well. Ensure that the Configure for HTTPS, and Do not use top-level statements checkboxes are unchecked because you won't use any of these in this example.
  6. Next, specify the Interactive render mode and Interactivity location.
  7. You should ensure that the Include sample pages checkbox is checked if you would like to have sample pages added to your project.
  8. Click Create to complete the process.

A new Blazor Web App project is created.

Install Entity Framework Core

So far, so good. The next step is to install the necessary NuGet Package(s) for working with Entity Framework Core and SQL Server. To install these packages into your project, right-click on the solution and then select Manage NuGet Packages for Solution….

Once the window pops up, search for the NuGet packages to add to your project. To do this, type in Microsoft.EntityFrameworkCore, Microsoft.EntityFrameworkCore.Design, Microsoft.EntityFrameworkCore.Tools, and Npgsql.EntityFrameworkCore.PostgreSQL in the search box and install them one after the other. Alternatively, you can type the commands shown below at the NuGet Package Manager Command Prompt:

PM> Install-Package Microsoft.EntityFrameworkCore
PM> Install-Package Microsoft.EntityFrameworkCore.Design
PM> Install-Package Microsoft.EntityFrameworkCore.Tools
PM> Install-Package Npgsql.EntityFrameworkCore.PostgreSQL

Alternatively, you can install these packages by executing the following commands at the Windows Shell:

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

Figure 8 illustrates the sequence diagram of the complete application.

Figure 8: The sequence diagram of the MiniCommerce application
Figure 8: The sequence diagram of the MiniCommerce application

Create the Model Classes

The following are the models used by the application:

  • Category: This table contains two fields: Id and Name. The Name field represents product categories which can have any of these values: electronics, furniture, and automobile. The category table includes the Id and Name fields.
  • Product: This table represents a single item in the catalog and contains fields that correspond to name, description, price, image URL, stock level and category to which this Product belongs.
  • ApplicationUser: This table represents a registered account, which has two fields: email and password (which contains a hashed password).
  • Order: This table represents order completion by user and consists of user id, UTC time, and the calculated total of an order.
  • OrderItem: This table represents one line of an order specifying Product and Quantity at a certain moment of time.

First off, create two solution folders; one of them named Models and the other Data. While the former would contain one or more model classes, the latter will have the data context and repository interfaces and classes. Note that you can always create multiple data context classes in the same project. If your data context class contains many entity references, it is a good practice to split the data context amongst multiple data context classes rather than have one large data context class.

Create new files named Category.cs, Product.cs, ApplicationUser.cs, Order.cs, and OrderItem.cs with each of them corresponding to the respective model classes inside the Models folder as shown below.

public class ApplicationUser
{
    public int Id { get; set; }
    public required string Email { get; set; }
    public string PasswordHash { get; set; } = string.Empty;
    public string Role { get; set; } = "Customer";
}

public class Product
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public string Description { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public string ImageUrl { get; set; } = string.Empty;
    public int Stock { get; set; }
    public int CategoryId { get; set; }
    public Category? Category { get; set; }
}

public class Category
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public List<Product> Products { get; set; } = new();
}

public class Order
{
    public int Id { get; set; }
    public required string UserId { get; set; }
    public DateTime CreatedAtUtc { get; set; }
    public decimal Total { get; set; }
    public List<OrderItem> Items { get; set; } = new();
}

public class OrderItem
{
    public int Id { get; set; }
    public int OrderId { get; set; }
    public int ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPriceAtPurchase { get; set; }
}

Create the Data Transfer Objects

A data transfer object (DTO) is a lightweight data structure used to pass data amongst various layers or components of an application. It contains only data—there is no business logic or validation logic and you cannot have any complex relationships in it either. You should use DTOs for data transport (i.e., transfer data across API boundaries, external communications) and domain models for writing your business logic, validation, rules, etc.

This application uses the following data transfer objects:

ProductDto: This represents a read-only view of Product that is being sent to the client over an HTTP Get endpoint.

CategoryDto: This represents a read-only view of Category used to display and filter the records of product categories in the user interface.

CartItemDto: This table is used to represent a shopping cart record. Note that unlike other data transfer objects, this data transfer object is mutable since it needs to be modified to add more products to the cart.

CheckoutLineDto: This represents the checkout data that will be sent to server by client, with two fields only: ProductId and Quantity.

CheckoutRequestDto: This data transfer object contains the request posted to the /api/checkout endpoint.

OrderDto: This is a data transfer object that is returned after a successful checkout completion and contains fields that comprise the order Id, total, and date.

RegisterRequestDto: This is a data transfer object that is used by the register endpoint to store data pertaining to the user to be registered in the application.

LoginRequestDto: This data transfer object is used as the request object for the login endpoint to store data pertaining to the user's login information.

AuthResponseDto: This is a data transfer object that is returned after a successful login process. It comprises information that includes a signed JWT, email, and role.

The following code snippet contains all data transfer objects (also known as DTOs) we'll be using in this application.

public record RegisterRequestDto(string Email, string Password);

public record LoginRequestDto(string Email, string Password);

public record AuthResponseDto(string Token, string Email, string Role);

public class CartItemDto
{
    public required int ProductId { get; set; }
    public required string Name { get; set; }
    public required decimal UnitPrice { get; set; }
    public int Quantity { get; set; }
}

public record CategoryDto(int Id, string Name);

public record CheckoutLineDto(int ProductId, int Quantity);

public record CheckoutRequestDto(List<CheckoutLineDto> Items);

public record OrderDto(int Id, decimal Total, DateTime CreatedAtUtc);

public record ProductDto(
    int Id,
    string Name,
    string Description,
    decimal Price,
    string ImageUrl,
    int Stock,
    int CategoryId
);

Create the Data Context

In Entity Framework Core (EF Core), a data context is a component used by an application to interact with the database and manage database connections, and to query and persist data in the database. Let us now create the data context class to enable our application to interact with the database to perform CRUD (Create, Read, Update, and Delete) operations.

To do this, create a new class named AppDbContext that extends the DbContext class of EF Core and write the following code in there.

public class AppDbContext : DbContext<AppDbContext>(options)
{
    public DbSet<Product> Products => Set<Product>();

    public DbSet<Category> Categories => Set<Category>();

    public DbSet<ApplicationUser> Users => Set<ApplicationUser>();

    public DbSet<Order> Orders => Set<Order>();

    public DbSet<OrderItem> OrderItems => Set<OrderItem>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    }
}

You can specify your database connection string in the OnConfiguring overloaded method of the AppDbContext class. However, in this implementation we will store the database connection settings in the appsettings.json file and read it in the Program.cs file to establish a database connection.

Note that your custom data context class (the AppDbContext class in this example), must expose a public constructor that accepts an instance of type DbContextOptions<AppDbContext> as an argument. This is needed to enable the runtime to pass the context configuration using a call to the AddDbContext() method to your custom DbContext class. The following code snippet illustrates how you can define a public constructor for your data context class.

public AppDbContext(
    DbContextOptions<CartDbContext> options,
    IConfiguration configuration) : base(options)
{
    _configuration = configuration;
}

However, the newer versions of C# support a parameterized constructor or a primary constructor. You can specify this in the class declaration itself as shown in the code snippet given below:

public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
}

We'll use a parameterized constructor in this example.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .Property(p => p.Price)
        .HasPrecision(18, 2);

    modelBuilder.Entity<OrderItem>()
        .Property(o => o.UnitPriceAtPurchase)
        .HasPrecision(18, 2);

    modelBuilder.Entity<Order>()
        .Property(o => o.Total)
        .HasPrecision(18, 2);

    modelBuilder.Entity<ApplicationUser>()
        .HasIndex(u => u.Email)
        .IsUnique();

    modelBuilder.Entity<Order>()
        .HasMany(o => o.Items)
        .WithOne()
        .HasForeignKey(i => i.OrderId);

    modelBuilder.Entity<OrderItem>()
        .HasOne<Product>()
        .WithMany()
        .HasForeignKey(oi => oi.ProductId)
        .OnDelete(DeleteBehavior.Restrict);
}

Listing 2 shows the complete source code of the AppDbContext class.

Listing 2: The AppDbContext class

public class AppDbContext(
    DbContextOptions<AppDbContext> options) : DbContext(options)
{
    public DbSet<Product> Products => Set<Product>();

    public DbSet<Category> Categories => Set<Category>();

    public DbSet<ApplicationUser> Users => Set<ApplicationUser>();

    public DbSet<Order> Orders => Set<Order>();

    public DbSet<OrderItem> OrderItems => Set<OrderItem>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>()
            .Property(p => p.Price)
            .HasPrecision(18, 2);

        modelBuilder.Entity<OrderItem>()
            .Property(o => o.UnitPriceAtPurchase)
            .HasPrecision(18, 2);

        modelBuilder.Entity<Order>()
            .Property(o => o.Total)
            .HasPrecision(18, 2);

        modelBuilder.Entity<ApplicationUser>()
            .HasIndex(u => u.Email)
            .IsUnique();

        modelBuilder.Entity<Order>()
            .HasMany(o => o.Items)
            .WithOne()
            .HasForeignKey(i => i.OrderId);

        modelBuilder.Entity<OrderItem>()
            .HasOne<Product>()
            .WithMany()
            .HasForeignKey(oi => oi.ProductId)
            .OnDelete(DeleteBehavior.Restrict);
    }
}

The SeedData class is responsible for populating an empty database with initial sample data, such as data pertaining to the categories (electronic devices, furniture, automobile), including several products across the three categories: HP laptop, DELL mouse, BMW, and Mercedes. This data will be used by EF Core entities and will be entered via the AddRange and SaveChanges methods.

The SeedData class contains a static method named EnsureSeeded that contains the necessary logic to seed data as shown in the following code snippet.

public static class SeedData
{
    public static void EnsureSeeded(AppDbContext db)
    {
        if (db.Categories.Any())
        {
            return;
        }

        var electronics = new Category { Name = "Electronics" };
        var furniture = new Category { Name = "Furniture" };
        var automobile = new Category { Name = "Automobile" };

        db.Categories.AddRange(electronics, furniture, automobile);

        db.Products.AddRange(
            /*Add your product records here*/
        );

        db.SaveChanges();
    }
}

Listing 3 shows the source code of SeedData.cs.

Listing 3: The SeedData class

public static class SeedData
{
    public static void EnsureSeeded(AppDbContext db)
    {
        if (db.Categories.Any())
        {
            return;
        }
        var electronics = new Category { Name = "Electronics" };
        var furniture = new Category { Name = "Furniture" };
        var automobile = new Category { Name = "Automobile" };
        db.Categories.AddRange(
        electronics, furniture, automobile);
        db.Products.AddRange(
            new Product
            {
                Name = "HP Laptop",
                Description = "15.6-inch, 16GB RAM, 512GB SSD.",
                Price = 999.99m,
                ImageUrl = "img/laptop.png",
                Stock = 20,
                Category = electronics
            },
            new Product
            {
                Name = "DELL Mouse",
                Description = "Wireless ergonomic mouse.",
                Price = 24.99m,
                ImageUrl = "img/mouse.png",
                Stock = 150,
                Category = electronics
            },
            new Product
            {
                Name = "Keyboard",
                Description = "Mechanical, RGB backlit.",
                Price = 79.99m,
                ImageUrl = "img/keyboard.png",
                Stock = 80,
                Category = electronics
            },
            new Product
            {
                Name = "Samsung Monitor",
                Description = "27-inch 4K IPS display.",
                Price = 349.00m,
                ImageUrl = "img/monitor.png",
                Stock = 25,
                Category = electronics
            },
            new Product
            {
                Name = "Office Chair",
                Description = "Ergonomic mesh back, adjustable height.",
                Price = 189.99m,
                ImageUrl = "img/office-chair.png",
                Stock = 40,
                Category = furniture
            },
            new Product
            {
                Name = "Wooden Desk",
                Description = "Wooden Laminated Study Table.",
                Price = 259.00m,
                ImageUrl = "img/wooden-desk.png",
                Stock = 15,
                Category = furniture
            },
            new Product
            {
                Name = "Bookshelf",
                Description = "Enigmatic Wooden Bookshelf.",
                Price = 129.50m,
                ImageUrl = "img/bookshelf.png",
                Stock = 22,
                Category = furniture
            },
            new Product
            {
                Name = "Sofa",
                Description = "Rosebell Wooden Four Seater Sofa.",
                Price = 599.00m,
                ImageUrl = "img/sofa.png",
                Stock = 10,
                Category = furniture
            },
            new Product
            {
                Name = "BMW",
                Description = "BMW 3 Series sedan.",
                Price = 50000.00m,
                ImageUrl = "img/bmw.png",
                Stock = 3,
                Category = automobile
            },
            new Product
            {
                Name = "Mercedes",
                Description = "Mercedes-Benz C-Class.",
                Price = 45000.00m,
                ImageUrl = "img/mercedes.png",
                Stock = 2,
                Category = automobile
            },
            new Product
            {
                Name = "Toyota",
                Description = "Toyota Corolla Sedan.",
                Price = 30000.00m,
                ImageUrl = "img/toyota-corolla.png",
                Stock = 5,
                Category = automobile
            },
            new Product
            {
                Name = "Ford",
                Description = "Ford Bronco.",
                Price = 45000.00m,
                ImageUrl = "img/ford-bronco.png",
                Stock = 4,
                Category = automobile
            }
        );
        db.SaveChanges();
    }
}

Create the Services

In this example, we'll create the following services:

  • ProductService: This class encapsulates all calls to the data context to read, write, filter and search product data.
  • CategoryService: This class contains only one method that uses the data context to return a list of the available categories.
  • CheckoutService: This class encapsulates calls to the data context to perform a checkout of items in the cart.
  • AuthService: This class encapsulates calls to the data context to register a new user, login an existing user, etc.

Create a solution folder in the server project and create four .cs files having the same name as the services, i.e., AuthService.cs, CategoryService.cs, CheckoutService.cs and ProductService.cs. Each of these classes should implement the following interfaces:

IProductService

ICategoryService

ICheckoutService

IAuthService

Note that for the sake of brevity, I've only included the implementation of ProductService class. You can look at the code repository for the complete source code of the application.

The following code snippet shows the IProductService interface:

public interface IProductService
{
    Task<List<ProductDto>> GetProductsAsync(int? categoryId, string? search);

    Task<ProductDto?> GetProductByIdAsync(int id);

    Task<int> CreateProductAsync(ProductDto dto);

    Task<bool> UpdateProductAsync(int id, ProductDto dto);

    Task<bool> DeleteProductAsync(int id);
}

The ProductService class implements all the methods of the IProductService interface as shown in Listing 4.

Listing 4: The ProductService class

public class ProductService(AppDbContext db) : IProductService
{
    public async Task<List<ProductDto>> GetProductsAsync(int? categoryId, string? search)
    {
        var query = db.Products.AsNoTracking().AsQueryable();

        if (categoryId is not null)
        {
            query = query.Where(p => p.CategoryId == categoryId);
        }

        if (!string.IsNullOrWhiteSpace(search))
        {
            query = query.Where(p => EF.Functions.Like(p.Name, $"%{search}%"));
        }

        return await query
            .Select(p => new ProductDto(p.Id, p.Name, p.Description, p.Price, p.ImageUrl, p.Stock, p.CategoryId))
            .ToListAsync();
    }

    public async Task<ProductDto?> GetProductByIdAsync(int id)
    {
        var product = await db.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id);

        return product is null
            ? null
            : new ProductDto(product.Id, product.Name, product.Description, product.Price, product.ImageUrl, product.Stock, product.CategoryId);
    }

    public async Task<int> CreateProductAsync(ProductDto dto)
    {
        var product = new Product
        {
            Name = dto.Name,
            Description = dto.Description,
            Price = dto.Price,
            ImageUrl = dto.ImageUrl,
            Stock = dto.Stock,
            CategoryId = dto.CategoryId
        };

        db.Products.Add(product);
        await db.SaveChangesAsync();
        return product.Id;
    }

    public async Task<bool> UpdateProductAsync(int id, ProductDto dto)
    {
        var product = await db.Products.FindAsync(id);

        if (product is null)
        {
            return false;
        }

        product.Name = dto.Name;
        product.Description = dto.Description;
        product.Price = dto.Price;
        product.ImageUrl = dto.ImageUrl;
        product.Stock = dto.Stock;
        product.CategoryId = dto.CategoryId;

        await db.SaveChangesAsync();
        return true;
    }

    public async Task<bool> DeleteProductAsync(int id)
    {
        var product = await db.Products.FindAsync(id);

        if (product is null)
        {
            return false;
        }

        db.Products.Remove(product);
        await db.SaveChangesAsync();
        return true;
    }
}

Create the Controllers

This application uses the following API Controllers:

AuthController

CategoriesController

ProductsController

CheckoutController

Listing 5 shows the ProductController class:

Listing 5: The ProductController class

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OutputCaching;
using MiniCommerce.Server.Services;
using MiniCommerce.Shared.DTOs;

namespace MiniCommerce.Server.Controllers;

[ApiController]
[Route("api/products")]
public class ProductsController(IProductService products) : ControllerBase
{
    [HttpGet]
    [OutputCache(PolicyName = "catalog")]
    public async Task<ActionResult<List<ProductDto>>> GetProducts(int? categoryId, string? search)
    {
        return Ok(await products.GetProductsAsync(categoryId, search));
    }

    [HttpGet("{id:int}")]
    [OutputCache(PolicyName = "catalog")]
    public async Task<ActionResult<ProductDto>> GetProduct(int id)
    {
        var product = await products.GetProductByIdAsync(id);
        return product is null ? NotFound() : Ok(product);
    }

    [HttpPost]
    [Authorize(Policy = "AdminOnly")]
    public async Task<ActionResult> CreateProduct([FromBody] ProductDto dto)
    {
        var id = await products.CreateProductAsync(dto);
        return Created($"/api/products/{id}", id);
    }

    [HttpPut("{id:int}")]
    [Authorize(Policy = "AdminOnly")]
    public async Task<ActionResult> UpdateProduct(int id, [FromBody] ProductDto dto)
    {
        var updated = await products.UpdateProductAsync(id, dto);
        return updated ? NoContent() : NotFound();
    }

    [HttpDelete("{id:int}")]
    [Authorize(Policy = "AdminOnly")]
    public async Task<ActionResult> DeleteProduct(int id)
    {
        var deleted = await products.DeleteProductAsync(id);
        return deleted ? NoContent() : NotFound();
    }
}

Create the User Interface

In this section, we'll focus on using Blazor to create an interface for interacting with the previously developed APIs. Blazor is a Microsoft open-source framework that allows developers to build interactive web applications in C# and .NET, making it one of the most versatile, elegant, and productive ways to build modern web applications today. Here are a few important points you should know:

  • The MiniCommerce user interface is built using a few Razor components shared between Client and AdminModule projects.
  • The root user interface component is App.razor that contains the routing information and the OnNavigateAsync hook for the lazy-loading of the Admin module.
  • MainLayout.razor and NavMenu.razor are two user interface components that provide the common layout and navigation functionality in the application.
  • Catalog.razor implements displaying products using a solution that renders only those rows that may be seen.
  • ProductCard.razor creates a tile for a product. It has an override of ShouldRender() that prevents it from repainting when there is no data change.
  • The Cart.razor component shows what is already in the cart as indicated by CartState.
  • Checkout.razor is a user interface component that is used to check out the items that have been added to the cart so that you can later submit those cart items for payment.
  • The Login.razor component is used to register a new user or login an existing user to the application.
  • The AdminDashboard.razor and ProductManagement.razor components contain admin pages that implement product management functionality.

Listing 6 illustrates the Catalog.razor user interface component. You can find the source code of all other user interface components in the code bundle.

Listing 6: Create the Catalog UI Component

@page "/"
@inject ProductApiService ProductApi
@inject CartState Cart

<PageTitle>Catalog</PageTitle>

<h1>Products</h1>

@if (products is null)
{
    <p>Loading...</p>
}
else
{
    <div class="catalog-grid">
        <Virtualize Items="products" Context="product" ItemSize="260">
            <ProductCard @key="product.Id" Product="product" OnAddToCart="HandleAddToCart" />
        </Virtualize>
    </div>
}

@code {
    private List<ProductDto>? products;

    protected override async Task OnInitializedAsync()
    {
        products = await ProductApi.GetProductsAsync();
    }

    private void HandleAddToCart(ProductDto product)
    {
        Cart.AddItem(new CartItemDto
        {
            ProductId = product.Id,
            Name = product.Name,
            UnitPrice = product.Price,
            Quantity = 1
        });
    }
}

When you execute the application, the available products will be displayed in the web browser as shown in Figure 9.

Figure 9: Displaying products in the web browser
Figure 9: Displaying products in the web browser

Performance Optimization Strategies

To optimize performance, MiniCommerce uses the techniques discussed in this section.

WASM Versus JavaScript Performance

In CPU-bound work like image processing, compression, and simulation, WASM outperforms JavaScript for most workloads by 1.5x to 3x, but for DOM-heavy code, JavaScript is a better choice because every update of a DOM through WASM requires an interop boundary crossing.

Reducing the Payload Size

By using IL linker, the unused code is removed from your assemblies at publish time. However, you should verify the actual output published as the trimmer will drop members that were referenced using reflection. You should prefer compact serialization using System.Text.Json over reflection-based serialization.

Lazy Loading and Code splitting

You should load assemblies that are seldom used only when they are needed, i.e., on demand. The MiniCommerce application marks the admin module as “lazy load” inside of the project file and will lazy load the module in the OnNavigateAsync callback.

<BlazorWebAssemblyLazyLoad Include="MiniCommerce.Client.AdminModule.wasm" />

Caching

The Blazor PWA template includes a service worker. In the initial visit, the service worker saves all the published assets in the Cache Storage API. Subsequent visits load the application from the cache, which reduces network latency.

Compression

When we publish the application, the framework files are pre-compressed with Brotli. The ASP.NET Core host serves the .br variants when the web browsers need them. Compared to the uncompressed assemblies, Brotli reduces the size of a Blazor payload by around 60 to 70 percent.

Startup Time

To reduce the startup time, you should keep OnInitializedAsync as slim as possible and delay any requests until after the first render. You should use a combination of trimming, compression, and lazy loading to reduce the startup time considerably.

Security Considerations

The WASM module runs inside the sandbox environment of the web browser. This means a module reads and writes only from the block of memory allocated to the module. By restricting the module to run inside a protected environment, the sandbox prevents your code from doing damage to the machine. You should not embed secrets, connection strings, API keys or proprietary business rules in a Blazor WebAssembly project.

You should validate the signed JWT on every request. You should also implement role or policy-based authorization for your endpoints. It is also good practice to protect endpoints against brute-force attacks on critical functionality like login or checkout using rate limiting. Also, you should restrict CORS to known origins only.

Beyond the Web Browser

WebAssembly is emerging as a new-generation technology that enables the development of applications that run cross-platform, are highly efficient, scalable, and secure, and do not depend on vendor-specific infrastructure (a.k.a., vendor-neutral). The WebAssembly System Interface (WASI) and runtimes such as Wasmtime and Wasmer enable WASM to be used in serverless computing, IoT devices, and microservices that run on the back end of an application. The combination of high performance, small size, and small footprint means that WASM applications can run with great performance on devices that normally would not be able to support them, such as running on a web browser on a mobile phone.

Key Use Cases

Here are key use cases of WASM:

  • Edge computing: WASM modules can be initialized in microseconds and take up very small amounts of space i.e., just a few megabytes. This allows you to deploy large numbers of WASM modules to locations where the size of the module does not permit the use of containers.
  • Serverless workloads: A container cold-starts in seconds; a WASM module cold-starts in milliseconds. This makes it an ideal choice for per-request billing models because of short bursts of usage charged on a per-request basis.
  • **Plugin architectures: **A host application has the capability to load a third-party wasm plugin and execute the plugin within a secured sandboxed environment. Hence, you can use the plug-in to extend the capability of a product without giving the plugin access to the host application process.

Challenges and Trade-Offs

If you're using either Blazor WebAssembly or WASM in your application, you should consider the potential costs and trade-offs discussed in this section.

Download size: On the initial visit, the .NET Runtime and the assemblies are downloaded. The typical combined size of the download will be on the order of 2 to 4 MB (after trimming and compression). However, if you use Ahead-of-Time (AOT) compilation, this total size is likely to double or get even worse.

Throughput without AOT: The Blazor WebAssembly interpreter executes IL code at a slow rate compared to the execution speed of JIT-compiled server-side code. Hence, if your application requires a significant amount of complex computation, either AOT compilation or a round-trip to the server will be required.

Interop overhead: The development model does not have direct access to the Document Object Model (DOM) so a large number of interop calls will reduce the performance benefits associated with the use of WASM.

Single-threaded UI: At present, Blazor WebAssembly runs as a single-threaded application, so unless you offload computational work to an additional web worker or worker thread, all long-running computations will block.

Debugging: Blazor WebAssembly produced assemblies generated as a result of AOT compilation and trimming will be difficult to debug. When possible, it is best to replicate a reported problem using Debug builds that were not previously trimmed.

SEO and first paint: The end user of a client-side rendered application will not experience the benefit of seeing any content until the initial rendering of the application has been completed. Blazor Web Apps provide a workaround for this issue through pre-rendering and static server-side rendering capabilities.

The Future of WASM in the .NET Ecosystem

Performance, portability, and scalability constitute a competitive advantage in today's web development. WebAssembly (WASM) enables you to leverage the best of both worlds, i.e., combining the flexibility of JavaScript with the performance of native code. WASM is a binary instruction set that provides near-native performance in all environments, i.e., browsers, servers, and edge devices. As WASM matures, we'll see many advances in performance. Here are a few trends that will drive the future growth of WASM technologies on the .NET platform.

  • .NET 11 previews a CoreCLR-based WebAssembly runtime, a step toward removing the threading constraints on WASM applications.
  • Smaller binaries and near-native performance
  • Hybrid execution and traced interpreters
  • Parallel processing via SIMD

Takeaways

Here are the key takeaways:

  • You can use WASM to run the same C# code in the browser, at the edge, or on an embedded device—you don't need a separate codebase for each platform you want your application to be deployed on.
  • AOT compilation trades a larger download for faster, more consistent execution. You should enable AOT compilation only for CPU-intensive workloads.
  • The sandboxing provided by the browser gives protection to the end user's physical computer. It does not protect the developer's sensitive data; anything sent to the browser is subject to viewing and potential tampering. Therefore, any sensitive business logic should be run on the server side.
  • Always trim the application, make use of compact serialization, cache aggressively, and measure before optimizing.
  • Tools for working with WASI continue to evolve, and tools will likely evolve significantly across .NET releases.
  • If you need high-compute workloads or your applications to work offline, WASM is a good choice. However, it is not a good fit for static applications or for applications that hardly have any CPU-intensive workloads.