From d5946fb7a4edea0f2a074c25ec6d0eaad3b543e7 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 8 Apr 2026 09:11:47 +0200 Subject: [PATCH 1/2] Update web worker template docs. --- .../blazor-with-dotnet-on-web-workers.md | 66 +++--- .../client-side/dotnet-on-webworkers.md | 209 +++++++++++++++++- .../aspnetcore-11/includes/blazor.md | 4 +- 3 files changed, 242 insertions(+), 37 deletions(-) diff --git a/aspnetcore/blazor/blazor-with-dotnet-on-web-workers.md b/aspnetcore/blazor/blazor-with-dotnet-on-web-workers.md index 67f363002a6f..4fb9f919303a 100644 --- a/aspnetcore/blazor/blazor-with-dotnet-on-web-workers.md +++ b/aspnetcore/blazor/blazor-with-dotnet-on-web-workers.md @@ -6,7 +6,7 @@ description: Learn how to use Web Workers to enable JavaScript to run on separat monikerRange: '>= aspnetcore-8.0' ms.author: wpickett ms.custom: mvc -ms.date: 03/13/2026 +ms.date: 04/07/2026 uid: blazor/blazor-web-workers --- # ASP.NET Core Blazor with .NET on Web Workers @@ -21,10 +21,10 @@ Modern Blazor WebAssembly apps often handle CPU-intensive work alongside rich UI :::moniker range=">= aspnetcore-11.0" -The `webworker` project template provides built-in scaffolding for running .NET code in a Web Worker. The template generates the required JavaScript worker scripts and a C# `WebWorkerClient` class, which removes the need to write the interop layer manually. To learn about Web Workers with React, see . +The Blazor Web Worker project template (`dotnet new blazorwebworker`) provides built-in scaffolding for running .NET code in a Web Worker in a Blazor WebAssembly app. The template generates the required JavaScript worker scripts, a C# `WebWorkerClient` class, and a starter `WorkerMethods.cs` file, which removes the need to write the interop layer manually. To learn about Web Workers with React, see . > [!NOTE] -> The `webworker` template isn't limited to Blazor. The template works with any .NET WebAssembly host, including standalone `wasmbrowser` apps and custom JavaScript frontends, such as React or vanilla JS. In non-Blazor scenarios, import the template's JavaScript client (`dotnet-web-worker-client.js`) directly from your entry point and call `[JSExport]` methods without the Blazor-specific C# `WebWorkerClient` class. +> In .NET 11 and later, the Blazor Web Worker template (`blazorwebworker`) is intended for Blazor WebAssembly scenarios. For React or other custom JavaScript frontends, use the manual approach in . ## Create the projects @@ -32,7 +32,7 @@ Create a Blazor WebAssembly app and a .NET Web Worker class library: ```dotnetcli dotnet new blazorwasm -n SampleApp -dotnet new webworker -n WebWorker +dotnet new blazorwebworker -n WebWorker ``` Add a project reference from the app to the worker library: @@ -42,34 +42,25 @@ cd SampleApp dotnet add reference ../WebWorker/WebWorker.csproj ``` -## Enable `AllowUnsafeBlocks` +## Customize the generated worker methods -Enable the property in the app's project file (`SampleApp.csproj`), which is required for [`[JSExport]` attribute](xref:System.Runtime.InteropServices.JavaScript.JSExportAttribute) usage: +The template already enables the property in the worker project and includes a starter `WorkerMethods.cs` file with a [`[JSExport]` attribute](xref:System.Runtime.InteropServices.JavaScript.JSExportAttribute) example. Add or update methods in the worker project as needed. -```xml - - true - -``` - -> [!WARNING] -> The JS interop API requires enabling . Be careful when implementing your own unsafe code in .NET apps, which can introduce security and stability risks. For more information, see [Unsafe code, pointer types, and function pointers](/dotnet/csharp/language-reference/unsafe-code). +Worker methods are `static` methods marked with [`[JSExport]`](xref:System.Runtime.InteropServices.JavaScript.JSExportAttribute) in a `static partial class`. -## Define worker methods +Due to `[JSExport]` limitations, worker methods should use JS interop-friendly types such as primitives and strings. For complex data, serialize to JSON in the worker and deserialize it in the Blazor app. -Worker methods are `static` methods marked with [`[JSExport]`](xref:System.Runtime.InteropServices.JavaScript.JSExportAttribute) in a `static partial class`. Define them in the main application project because the assembly name must match the one used by the worker runtime. - -Due to `[JSExport]` limitations, worker methods can only return primitives or strings. For complex types, serialize to JSON before returning. The `WebWorkerClient` automatically deserializes JSON results. - -`Worker.cs`: +`WebWorker\WorkerMethods.cs`: ```csharp using System.Runtime.InteropServices.JavaScript; using System.Runtime.Versioning; using System.Text.Json; +namespace WebWorker; + [SupportedOSPlatform("browser")] -public static partial class Worker +public static partial class WorkerMethods { [JSExport] public static string Greet(string name) => $"Hello, {name}!"; @@ -124,6 +115,7 @@ Inject `IJSRuntime` and use `WebWorkerClient.CreateAsync` to create a worker ins `Pages/Home.razor.cs`: ```csharp +using System.Text.Json; using System.Runtime.Versioning; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; @@ -155,10 +147,12 @@ public partial class Home : ComponentBase, IAsyncDisposable } greeting = await worker.InvokeAsync( - "SampleApp.Worker.Greet", ["World"]); + "WebWorker.WorkerMethods.Greet", ["World"]); - users = await worker.InvokeAsync>( - "SampleApp.Worker.GetUsers", []); + var usersJson = await worker.InvokeAsync( + "WebWorker.WorkerMethods.GetUsers", []); + + users = JsonSerializer.Deserialize>(usersJson) ?? []; } public async ValueTask DisposeAsync() @@ -173,18 +167,20 @@ public partial class Home : ComponentBase, IAsyncDisposable ## Template output -The `dotnet new webworker` template generates a class library with the following structure: +The `dotnet new blazorwebworker` template generates a class library with the following structure: ``` WebWorker/ ├── WebWorker.csproj ├── WebWorkerClient.cs +├── WorkerMethods.cs └── wwwroot/ ├── dotnet-web-worker-client.js └── dotnet-web-worker.js ``` * `WebWorkerClient.cs`: C# client that manages worker lifecycle and communication. +* `WorkerMethods.cs`: Starter file for adding `[JSExport]` methods that run inside the worker. * `dotnet-web-worker-client.js`: JavaScript class that creates the worker, dispatches messages, and resolves pending requests. * `dotnet-web-worker.js`: Worker entry point that boots the .NET WebAssembly runtime and dynamically resolves `[JSExport]` methods by name. @@ -196,18 +192,30 @@ The `WebWorkerClient` class exposes an async API for communicating with a Web Wo public sealed class WebWorkerClient : IAsyncDisposable { public static async Task CreateAsync( - IJSRuntime jsRuntime); + IJSRuntime jsRuntime, + int timeoutMs = 60000, + string? assemblyName = null, + CancellationToken cancellationToken = default); public async Task InvokeAsync( - string method, object[] args, + string method, + object[] args, + int timeoutMs = 60000, + CancellationToken cancellationToken = default); + + public async Task InvokeVoidAsync( + string method, + object[] args, + int timeoutMs = 60000, CancellationToken cancellationToken = default); public async ValueTask DisposeAsync(); } ``` -* `CreateAsync`: Initializes the worker and waits for the .NET runtime to be ready inside the worker thread. -* `InvokeAsync`: Calls a `[JSExport]` method on the worker by its full name (`Namespace.ClassName.MethodName`) and returns the deserialized result. JSON string results are automatically parsed into `TResult`. +* `CreateAsync`: Initializes the worker and waits for the .NET runtime to be ready inside the worker thread. The `assemblyName` parameter defaults to the worker project's assembly. +* `InvokeAsync`: Calls a `[JSExport]` method on the worker by its full name (`AssemblyName.ClassName.MethodName`) and returns the result. +* `InvokeVoidAsync`: Calls a `[JSExport]` method that doesn't return a value. * `DisposeAsync`: Terminates the worker and releases resources. Use `await using` or call explicitly. :::moniker-end diff --git a/aspnetcore/client-side/dotnet-on-webworkers.md b/aspnetcore/client-side/dotnet-on-webworkers.md index 26b903d601d4..c180afed9ee7 100644 --- a/aspnetcore/client-side/dotnet-on-webworkers.md +++ b/aspnetcore/client-side/dotnet-on-webworkers.md @@ -6,7 +6,7 @@ description: Learn how to use Web Workers to enable JavaScript to run on separat monikerRange: '>= aspnetcore-8.0' ms.author: wpickett ms.custom: mvc -ms.date: 03/13/2026 +ms.date: 04/07/2026 uid: client-side/dotnet-on-webworkers --- # .NET on Web Workers @@ -21,10 +21,21 @@ Modern web apps often require intensive computational tasks that can block the m This approach is particularly valuable when you need to perform complex calculations, data processing, or business logic without requiring direct DOM manipulation. Instead of rewriting algorithms in JS, you can maintain your existing .NET codebase and execute it efficiently in the background while your React.js frontend remains responsive. +:::moniker range=">= aspnetcore-11.0" + +> [!TIP] +> In .NET 11 or later, `dotnet new blazorwebworker` generates the worker scripts and starter `[JSExport]` code used by the Blazor integration. For the Blazor-specific walkthrough, see . + +:::moniker-end + +:::moniker range="< aspnetcore-11.0" + > [!TIP] -> Starting with .NET 11, the `webworker` project template (`dotnet new webworker`) scaffolds the JavaScript worker scripts and interop boilerplate for you. The template works with any .NET WebAssembly host—Blazor, standalone `wasmbrowser` apps, and custom JavaScript frontends like React. Import the template's JavaScript client (`dotnet-web-worker-client.js`) directly from your app's entry point to get started. For the Blazor-specific integration that includes a C# `WebWorkerClient` class, see . +> For earlier versions, follow the manual `wasmbrowser` steps in this article. For the Blazor-specific walkthrough, see . + +:::moniker-end -This article demonstrates the manual approach for React.js frontends using a standalone .NET WebAssembly project. +This article demonstrates the React approach using a standalone .NET WebAssembly project. ## Sample app @@ -32,15 +43,43 @@ Explore a complete working implementation in the [Blazor samples GitHub reposito ## Prerequisites and setup -Before diving into the implementation, ensure the necessary tools are installed. The [.NET SDK 8.0 or later](https://dotnet.microsoft.com/download) is required and the WebAssembly workloads: +Before diving into the implementation, ensure the necessary tools are installed. + +:::moniker range=">= aspnetcore-11.0" + +The [.NET 11 SDK or later](https://dotnet.microsoft.com/download) is required for the `blazorwebworker` template approach. + +:::moniker-end + +:::moniker range="< aspnetcore-11.0" + +The [.NET SDK 8.0 or later](https://dotnet.microsoft.com/download) is required. If the WebAssembly build tools aren't already installed, run: ```bash dotnet workload install wasm-tools dotnet workload install wasm-experimental ``` +:::moniker-end + For the React.js frontend, [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com) must be installed. +:::moniker range=">= aspnetcore-11.0" + +Create a new React app with Vite: + +```bash +npm create vite@latest react-app +cd react-app +npm install +``` + +When prompted, select `React` and `JavaScript`. + +:::moniker-end + +:::moniker range="< aspnetcore-11.0" + Create a new React app: ```bash @@ -48,13 +87,80 @@ npx create-react-app react-app cd react-app ``` +:::moniker-end + ## Create the .NET WebAssembly project +:::moniker range=">= aspnetcore-11.0" + +In .NET 11 or later, you can start from the Blazor Web Worker template and adapt it for a React host: + +```bash +dotnet new blazorwebworker -o WebWorkersOnReact +cd WebWorkersOnReact +dotnet add package QRCoder +``` + +Update `WebWorkersOnReact.csproj` to use the WebAssembly SDK and build as a library. The template uses the Razor SDK by default because, in the Blazor scenario, the host app already provides the .NET WebAssembly runtime assets. In a React app, there isn't a Blazor host to provide that runtime bundle, so the worker project must produce its own `_framework` output. + +Setting `Library` enables WebAssembly library mode. In this mode, the project produces the runtime files needed by the worker scripts but doesn't require a standalone app entry point: + +```xml + + + net11.0 + enable + enable + true + Library + + +``` + +Delete `WebWorkerClient.cs` because it's specific to the Blazor integration. + +Update `WorkerMethods.cs` with the methods that the React app should call: + +```csharp +using System.Runtime.InteropServices.JavaScript; +using System.Runtime.Versioning; +using QRCoder; + +namespace WebWorkersOnReact; + +[SupportedOSPlatform("browser")] +public static partial class WorkerMethods +{ + private static readonly int MaxQrSize = 20; + + [JSExport] + public static byte[] Generate(string text, int qrSize) + { + if (qrSize >= MaxQrSize) + { + throw new ArgumentOutOfRangeException( + nameof(qrSize), + $"QR code size must be less than {MaxQrSize}."); + } + + using var qrGenerator = new QRCodeGenerator(); + using var qrCodeData = qrGenerator.CreateQrCode(text, QRCodeGenerator.ECCLevel.Q); + var qrCode = new BitmapByteQRCode(qrCodeData); + return qrCode.GetGraphic(qrSize); + } +} +``` + +:::moniker-end + +:::moniker range="< aspnetcore-11.0" + Create a new WebAssembly browser project to serve as the Web Worker: ```bash dotnet new wasmbrowser -o WebWorkersOnReact cd WebWorkersOnReact +dotnet add package QRCoder ``` Modify the `Program.cs` file to set up the Web Worker entry point and message handling: @@ -136,7 +242,9 @@ self.addEventListener('message', async function(e) { }, false); ``` -Build the WebAssembly project to generate the necessary files: +:::moniker-end + +Build the worker project: ```bash dotnet build @@ -144,7 +252,82 @@ dotnet build ## Set up the React app -In the React app, create a Web Worker to host the .NET WebAssembly runtime. Use an npm script defined in the `package.json` to automate copying the WebAssembly build artifacts from the .NET project to the React directory. See the [sample app](#sample-app) for reference. +For a quick test, copy the worker output into the React app's static files manually. For a real app, automate these copies with an npm script or another build step. + +:::moniker range=">= aspnetcore-11.0" + +Use the generated JavaScript client to host the .NET WebAssembly runtime in a Web Worker. For example, copy: + +* Copy `WebWorkersOnReact/bin/Debug/net11.0/wwwroot/_framework` to `react-app/public/_framework`. +* Copy `WebWorkersOnReact/wwwroot/dotnet-web-worker.js` to `react-app/public/_content/WebWorkersOnReact/dotnet-web-worker.js`. +* Copy `WebWorkersOnReact/wwwroot/dotnet-web-worker-client.js` to `react-app/public/_content/WebWorkersOnReact/dotnet-web-worker-client.js`. + +Then create a client helper, such as `src/client.js`, to load the generated client and call the worker: + +```javascript +let worker; + +async function getWorker() { + if (!worker) { + const { create } = await import( + /* @vite-ignore */ '/_content/WebWorkersOnReact/dotnet-web-worker-client.js'); + worker = await create(60000, { assemblyName: 'WebWorkersOnReact' }); + } + + return worker; +} + +export async function generateQR(text, size) { + const worker = await getWorker(); + const response = await worker.invoke( + 'WebWorkersOnReact.WorkerMethods.Generate', + [text, size], + 60000); + const blob = new Blob([response], { type: 'image/png' }); + return URL.createObjectURL(blob); +} +``` + +Replace the starter content in `src/App.jsx` with a button that calls `generateQR`: + +```javascript +import { useState } from 'react'; +import './App.css'; +import { generateQR } from './client'; + +function App() { + const [qrUrl, setQrUrl] = useState(''); + const [status, setStatus] = useState('Ready'); + + async function handleGenerate() { + try { + setStatus('Generating QR code...'); + const url = await generateQR('Hello from docs', 10); + setQrUrl(url); + setStatus('Done'); + } catch (error) { + setStatus(error.message); + } + } + + return ( +
+

.NET on Web Workers

+

{status}

+ + {qrUrl ? Generated QR code : null} +
+ ); +} + +export default App; +``` + +:::moniker-end + +:::moniker range="< aspnetcore-11.0" + +Create a Web Worker to host the .NET WebAssembly runtime. The sample app copies the entire output folder into `public/qr`, which preserves both `wwwroot/worker.js` and the `_framework` assets required by the runtime. See the [sample app](#sample-app) for reference. Create a Web Worker file `client.js` to receive messages from dotnet: @@ -197,6 +380,20 @@ function sendRequestToWorker(request) { } ``` +:::moniker-end + +:::moniker range=">= aspnetcore-11.0" + +Run the app: + +```bash +npm run dev +``` + +Open the local URL shown by the development server and select **Generate QR**. If everything is set up correctly, the page displays the generated image. + +:::moniker-end + ## Performance considerations and optimization When working with .NET on Web Workers, consider these key optimization strategies: diff --git a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md index 88786c902a89..9bef2832cf6f 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md @@ -114,6 +114,6 @@ public ITempData? TempData { get; set; } For more information, see . -### New Web Worker template (`webworker`) +### New Blazor Web Worker template (`blazorwebworker`) -Blazor WebAssembly apps can perform heavy computing on the client, but doing so on the UI thread interferes with UI rendering and negatively affects the user experience. In .NET 10, we added an article with a sample app to make offloading heavy work from the UI thread to a Web Worker easier. For .NET 11, we've added the .NET Web Worker project template (`webworker`), which provides infrastructure for running .NET code in a Web Worker. For more information, see . +Blazor WebAssembly apps can perform heavy computing on the client, but doing so on the UI thread interferes with UI rendering and negatively affects the user experience. In .NET 10, we added an article with a sample app to make offloading heavy work from the UI thread to a Web Worker easier. For .NET 11, we've added the Blazor Web Worker project template (`blazorwebworker`), which provides infrastructure for running .NET code in a Web Worker in Blazor WebAssembly apps. For more information, see . From 1cf49789a743891cc2b27d04f5024c206f83b116 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:44:04 -0400 Subject: [PATCH 2/2] Update aspnetcore/client-side/dotnet-on-webworkers.md --- aspnetcore/client-side/dotnet-on-webworkers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/client-side/dotnet-on-webworkers.md b/aspnetcore/client-side/dotnet-on-webworkers.md index c180afed9ee7..97f757389100 100644 --- a/aspnetcore/client-side/dotnet-on-webworkers.md +++ b/aspnetcore/client-side/dotnet-on-webworkers.md @@ -256,7 +256,7 @@ For a quick test, copy the worker output into the React app's static files manua :::moniker range=">= aspnetcore-11.0" -Use the generated JavaScript client to host the .NET WebAssembly runtime in a Web Worker. For example, copy: +Use the generated JavaScript client to host the .NET WebAssembly runtime in a Web Worker. For example: * Copy `WebWorkersOnReact/bin/Debug/net11.0/wwwroot/_framework` to `react-app/public/_framework`. * Copy `WebWorkersOnReact/wwwroot/dotnet-web-worker.js` to `react-app/public/_content/WebWorkersOnReact/dotnet-web-worker.js`.