Skip to content

Commit 768feea

Browse files
authored
Merge pull request #36389 from dotnet/main
2 parents f5b9c18 + 0d9479a commit 768feea

File tree

12 files changed

+343
-14
lines changed

12 files changed

+343
-14
lines changed

.github/workflows/quest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
env:
3434
REASON: ${{ github.event.inputs.reason }}
3535
ISSUENUMBER: ${{ github.event.inputs.issue }}
36-
I
36+
3737
- name: Azure OpenID Connect
3838
id: azure-oidc-auth
3939
uses: dotnet/docs-tools/.github/actions/oidc-auth-flow@main
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
---
2+
title: ASP.NET Core Blazor with .NET on Web Workers
3+
author: guardrex
4+
description: Learn how to use Web Workers to enable JavaScript to run on separate threads that don't block the main UI thread for improved app performance in a Blazor WebAssembly app.
5+
monikerRange: '>= aspnetcore-8.0'
6+
ms.author: wpickett
7+
ms.custom: mvc
8+
ms.date: 11/20/2025
9+
uid: blazor/blazor-web-workers
10+
---
11+
# ASP.NET Core Blazor with .NET on Web Workers
12+
13+
<!-- UPDATE 11.0 - Activate
14+
15+
[!INCLUDE[](~/includes/not-latest-version.md)]
16+
17+
-->
18+
19+
Modern Blazor WebAssembly apps often handle CPU-intensive work alongside rich UI updates. Tasks such as image processing, document parsing, or data crunching can easily freeze the browser's main thread. Web Workers let you push that work to a background thread. Combined with the .NET WebAssembly runtime, you can keep writing C# while the UI stays responsive.
20+
21+
The guidance in this article mirrors the concepts from the React-focused *.NET on Web Workers* walkthrough, but adapts every step to a Blazor frontend. It highlights the same QR-code generation scenario implemented in this repository. To learn about Web Workers with React, see <xref:client-side/dotnet-on-webworkers>.
22+
23+
## Sample app
24+
25+
Explore a complete working implementation in the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples). The sample is available for .NET 10 or later and named `DotNetOnWebWorkersBlazorWebAssembly`.
26+
27+
## Prerequisites
28+
29+
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.
30+
31+
## Create the Blazor WebAssembly project
32+
33+
Create a Blazor WebAssembly app:
34+
35+
```bash
36+
dotnet new blazorwasm -o WebWorkersOnBlazor
37+
cd WebWorkersOnBlazor
38+
```
39+
40+
Add a package reference for [`QRCoder`](https://www.nuget.org/packages/QRCoder) to simulate heavy computations.
41+
42+
[!INCLUDE[](~/includes/package-reference.md)]
43+
44+
> [!WARNING]
45+
> [`Shane32/QRCoder`](https://github.com/Shane32/QRCoder)/[`QRCoder` NuGet package](https://www.nuget.org/packages/QRCoder) isn't owned or maintained by Microsoft and isn't covered by any Microsoft Support Agreement or license. Use caution when adopting a third-party library, especially for security features. Confirm that the library follows official specifications and adopts security best practices. Keep the library's version current to obtain the latest bug fixes.
46+
47+
Enable the <xref:Microsoft.Build.Tasks.Csc.AllowUnsafeBlocks> property in app's project file, which is required whenever you use [`[JSImport]` attribute](xref:System.Runtime.InteropServices.JavaScript.JSImportAttribute) or [`[JSExport]` attribute](xref:System.Runtime.InteropServices.JavaScript.JSExportAttribute) in WebAssembly projects:
48+
49+
```xml
50+
<PropertyGroup>
51+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
52+
</PropertyGroup>
53+
```
54+
55+
> [!WARNING]
56+
> The JS interop API requires enabling <xref:Microsoft.Build.Tasks.Csc.AllowUnsafeBlocks>. 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).
57+
58+
## Add the Web Worker code
59+
60+
Create the following file to expose .NET code to JavaScript using the [`[JSExport]` attribute](xref:System.Runtime.InteropServices.JavaScript.JSExportAttribute):
61+
62+
`Workers/QRGenerator.razor.cs`:
63+
64+
```csharp
65+
using System.Runtime.InteropServices.JavaScript;
66+
using System.Runtime.Versioning;
67+
using QRCoder;
68+
69+
[SupportedOSPlatform("browser")]
70+
public partial class QRGenerator
71+
{
72+
private static readonly int MaxQrSize = 20;
73+
74+
[JSExport]
75+
internal static byte[] Generate(string text, int qrSize)
76+
{
77+
if (qrSize >= MaxQrSize)
78+
{
79+
throw new Exception($"QR code size must be less than {MaxQrSize}.");
80+
}
81+
82+
var generator = new QRCodeGenerator();
83+
QRCodeData data = generator.CreateQrCode(text, QRCodeGenerator.ECCLevel.Q);
84+
var qrCode = new BitmapByteQRCode(data);
85+
86+
return qrCode.GetGraphic(qrSize);
87+
}
88+
}
89+
```
90+
91+
Create a matching Razor component file (`.razor`) to act as an empty stub so that the build packs the worker script alongside the component assets:
92+
93+
`Workers/QRGenerator.razor`:
94+
95+
```razor
96+
// dummy file to let blazor handle Worker.razor.js file loading
97+
```
98+
99+
Add the following JavaScript file. The script boots the .NET runtime in the worker, then listens for messages from the main thread. `postMessage` is used to send either a `result` or an `error` payload.
100+
101+
`Workers/QRGenerator.razor.js`:
102+
103+
```javascript
104+
import { dotnet } from '../_framework/dotnet.js';
105+
106+
let assemblyExports;
107+
let startupError;
108+
109+
try {
110+
const { getAssemblyExports, getConfig } = await dotnet.create();
111+
const config = getConfig();
112+
assemblyExports = await getAssemblyExports(config.mainAssemblyName);
113+
} catch (err) {
114+
startupError = err.message;
115+
}
116+
117+
self.addEventListener('message', async e => {
118+
try {
119+
if (!assemblyExports) {
120+
throw new Error(startupError || 'worker exports not loaded');
121+
}
122+
123+
let result;
124+
switch (e.data.command) {
125+
case 'generateQR':
126+
result = assemblyExports.QRGenerator.Generate(e.data.text, e.data.size);
127+
break;
128+
default:
129+
throw new Error(`Unknown command: ${e.data.command}`);
130+
}
131+
132+
self.postMessage({ command: 'response',
133+
requestId: e.data.requestId, result });
134+
} catch (err) {
135+
self.postMessage({ command: 'response',
136+
requestId: e.data.requestId, error: err.message });
137+
}
138+
});
139+
```
140+
141+
## Bridge the worker to the Blazor UI
142+
143+
Create the following JavaScript file that manages the worker instance and exposes helper functions to Blazor.
144+
145+
`Clients/Client.razor.js`:
146+
147+
```javascript
148+
const pendingRequests = {};
149+
let pendingRequestId = 0;
150+
151+
const dotnetWorker =
152+
new Worker('./Workers/QRGenerator.razor.js', { type: 'module' });
153+
154+
dotnetWorker.addEventListener('message', e => {
155+
switch (e.data.command) {
156+
case 'response':
157+
const request = pendingRequests[e.data.requestId];
158+
delete pendingRequests[e.data.requestId];
159+
if (e.data.error) {
160+
request.reject(new Error(e.data.error));
161+
}
162+
request.resolve(e.data.result);
163+
break;
164+
default:
165+
console.log('Worker said:', e.data);
166+
}
167+
});
168+
169+
function sendRequestToWorker(request) {
170+
pendingRequestId++;
171+
const promise = new Promise((resolve, reject) => {
172+
pendingRequests[pendingRequestId] = { resolve, reject };
173+
});
174+
175+
dotnetWorker.postMessage({ ...request, requestId: pendingRequestId });
176+
return promise;
177+
}
178+
179+
export async function generateQR(text, size) {
180+
const response = await sendRequestToWorker({ command: 'generateQR', text, size });
181+
const blob = new Blob([response], { type: 'image/png' });
182+
return URL.createObjectURL(blob);
183+
}
184+
```
185+
186+
Similarly as the worker, the `Client` script requires a matching `.razor` file with an empty stub to assure that the JS file is considered a part of the component.
187+
188+
`Clients/Client.razor`:
189+
190+
```razor
191+
// dummy file to let blazor handle Client.razor.js file loading
192+
```
193+
194+
Add the following `Client`, which exposes the JavaScript module to Blazor components using the [`[JSImport]` attribute](xref:System.Runtime.InteropServices.JavaScript.JSImportAttribute). `InitClient` ensures the worker JS module is only loaded once per browser session.
195+
196+
`Clients/Client.razor.cs`:
197+
198+
```csharp
199+
using System.Runtime.InteropServices.JavaScript;
200+
using System.Runtime.Versioning;
201+
202+
[SupportedOSPlatform("browser")]
203+
public partial class Client
204+
{
205+
private static bool _workerStarted;
206+
207+
public static async Task InitClient()
208+
{
209+
if (_workerStarted)
210+
{
211+
return;
212+
}
213+
214+
_workerStarted = true;
215+
216+
await JSHost.ImportAsync(
217+
moduleName: nameof(Client),
218+
moduleUrl: "../Clients/Client.razor.js");
219+
}
220+
221+
[JSImport("generateQR", nameof(Client))]
222+
public static partial Task<string> GenerateQR(string text, int size);
223+
}
224+
```
225+
226+
## Demonstrate the flow
227+
228+
You can use the app's Home page to demonstrate the flow.
229+
230+
`Pages/Home.razor`:
231+
232+
```razor
233+
@page "/"
234+
@using Components
235+
@namespace Pages
236+
237+
<PageTitle>Home</PageTitle>
238+
239+
<h1>Hello, world!</h1>
240+
241+
<Popup @ref="popup" />
242+
243+
<div class="input-container">
244+
<div class="form-group">
245+
<label for="textInput">Generate a QR from text:</label>
246+
<input type="text" class="form-control" id="textInput" @bind="text" placeholder="Text" />
247+
</div>
248+
249+
<div class="form-group">
250+
<label for="numberInput">Set size of QR (in pixels):</label>
251+
<input type="number" class="form-control" id="numberInput" @bind="size" />
252+
</div>
253+
254+
<div class="form-group">
255+
<button class="btn btn-primary" @onclick="GenerateQR">Generate QR</button>
256+
</div>
257+
258+
@if (!string.IsNullOrWhiteSpace(imageUrl))
259+
{
260+
<div class="form-group">
261+
<img class="image" src="@imageUrl" id="qrImage" alt="Image" />
262+
</div>
263+
}
264+
</div>
265+
```
266+
267+
The following code-behind file initializes the client and generates the QR code. The [`OnAfterRenderAsync` lifecycle method](xref:blazor/components/lifecycle#after-component-render-onafterrenderasync) code guarantees that the JavaScript module is loaded before the user clicks the button, while the `GenerateQR` handler makes a single asynchronous worker request.
268+
269+
`Home.razor.cs`:
270+
271+
```csharp
272+
using Microsoft.AspNetCore.Components;
273+
using System.Runtime.Versioning;
274+
using Components;
275+
276+
namespace Pages;
277+
278+
[SupportedOSPlatform("browser")]
279+
public partial class Home : ComponentBase
280+
{
281+
private string imageUrl = string.Empty;
282+
private string? text;
283+
private int size = 5;
284+
private Popup popup = new();
285+
286+
protected override async Task OnAfterRenderAsync(bool firstRender)
287+
{
288+
await Client.InitClient();
289+
}
290+
291+
private async Task GenerateQR()
292+
{
293+
try
294+
{
295+
if (text is not null)
296+
{
297+
imageUrl = await Client.GenerateQR(text, size);
298+
}
299+
}
300+
catch(Exception ex)
301+
{
302+
imageUrl = string.Empty;
303+
popup.Show(title: "Error", message: ex.Message);
304+
}
305+
306+
await InvokeAsync(StateHasChanged);
307+
}
308+
}
309+
```
310+
311+
## Next steps
312+
313+
* Swap the QR code sample for your own CPU-intensive domain logic.
314+
* Move long-running workflows into dedicated worker instances per feature area.
315+
* Explore shared array buffers or Atomics when you need higher-throughput synchronization between Blazor and workers.
316+
317+
## Additional resources
318+
319+
<xref:client-side/dotnet-on-webworkers>

aspnetcore/blazor/security/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1201,7 +1201,7 @@ Although the <xref:Microsoft.AspNetCore.Components.Authorization.AuthorizeView>
12011201

12021202
:::moniker range=">= aspnetcore-8.0"
12031203

1204-
Razor components of Blazor Web Apps never display `<NotAuthorized>` content when authorization fails server-side during static server-side rendering (static SSR). The server-side ASP.NET Core pipeline processes authorization on the server. Use server-side techniques to handle unauthorized requests. For more information, see <xref:blazor/components/render-modes#static-server-side-rendering-static-ssr>.
1204+
Razor components of Blazor Web Apps never display `<NotAuthorized>` content when authorization fails server-side during static server-side rendering (static SSR). The server-side ASP.NET Core pipeline processes authorization on the server. Use server-side techniques, such as configuring <xref:Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions.LoginPath%2A> to handle unauthorized requests. For more information, see <xref:blazor/components/render-modes#static-server-side-rendering-static-ssr>.
12051205

12061206
:::moniker-end
12071207

aspnetcore/client-side/dotnet-on-webworkers.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
---
22
title: .NET on Web Workers
33
author: guardrex
4-
description: Learn how to use Web Workers to enable JavaScript to run on separate threads that don't block the main UI thread for improved app performance.
4+
description: Learn how to use Web Workers to enable JavaScript to run on separate threads that don't block the main UI thread for improved app performance in a React app.
55
monikerRange: '>= aspnetcore-8.0'
66
ms.author: wpickett
77
ms.custom: mvc
8-
ms.date: 10/03/2025
8+
ms.date: 11/20/2025
99
uid: client-side/dotnet-on-webworkers
1010
---
1111
# .NET on Web Workers
@@ -22,7 +22,7 @@ This approach is particularly valuable when you need to perform complex calculat
2222

2323
## Sample app
2424

25-
Explore a complete working implementation in the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples). The sample is available for .NET 10 or later and named `DotNetOnWebWorkers`.
25+
Explore a complete working implementation in the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples). The sample is available for .NET 10 or later and named `DotNetOnWebWorkersReact`.
2626

2727
## Prerequisites and setup
2828

@@ -47,8 +47,8 @@ cd react-app
4747
Create a new WebAssembly browser project to serve as the Web Worker:
4848

4949
```bash
50-
dotnet new wasmbrowser -n DotNetOnWebWorkers
51-
cd DotNetOnWebWorkers
50+
dotnet new wasmbrowser -o WebWorkersOnReact
51+
cd WebWorkersOnReact
5252
```
5353

5454
Modify the `Program.cs` file to set up the Web Worker entry point and message handling:
@@ -201,3 +201,7 @@ When working with .NET on Web Workers, consider these key optimization strategie
201201
* **Startup cost**: WebAssembly initialization has overhead, so prefer persistent workers over frequent creation/destruction.
202202

203203
See the [sample app](#sample-app) for a demonstration of the preceding concepts.
204+
205+
## Additional resources
206+
207+
<xref:blazor/blazor-web-workers>

0 commit comments

Comments
 (0)