diff --git a/.github/workflows/blazor-issue-processing.yml b/.github/workflows/blazor-issue-processing.yml index 650a645b59de..5dc1ec2a8a13 100644 --- a/.github/workflows/blazor-issue-processing.yml +++ b/.github/workflows/blazor-issue-processing.yml @@ -9,6 +9,7 @@ jobs: && !contains(github.event.issue.body, 'https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/hybrid')) || contains(github.event.issue.body, 'https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/dotnet-interop/index.md') || contains(github.event.issue.body, 'https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/dotnet-interop/wasm-browser-app.md') + || contains(github.event.issue.body, 'https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/dotnet-on-webworkers.md') || contains(github.event.issue.body, 'https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/built-in/component-tag-helper.md') || contains(github.event.issue.body, 'https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/built-in/persist-component-state.md') runs-on: ubuntu-latest @@ -22,7 +23,7 @@ jobs: issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: `### 🏖️🌞 **_Summertime!! Woot!!_** 🏐⛵ + body: `### 🧟💀 *Happy Halloween!!_** 🎃🧛 *Stand-by!* ... A green dinosaur 🦖 will be along shortly to assist.` }) diff --git a/aspnetcore/blazor/globalization-localization.md b/aspnetcore/blazor/globalization-localization.md index dd9bd5654806..cf1133ed69db 100644 --- a/aspnetcore/blazor/globalization-localization.md +++ b/aspnetcore/blazor/globalization-localization.md @@ -5,7 +5,7 @@ description: Learn how to render globalized and localized content to users in di monikerRange: '>= aspnetcore-3.1' ms.author: wpickett ms.custom: mvc -ms.date: 11/12/2024 +ms.date: 10/02/2025 uid: blazor/globalization-localization --- # ASP.NET Core Blazor globalization and localization @@ -75,15 +75,40 @@ The following field types have specific formatting requirements and aren't suppo For current browser support of the preceding types, see [Can I use](https://caniuse.com). +By default, Blazor loads a subset of globalization data that contains the app's culture. To load all globalization data, set `` to `true` in the app's project file (`.csproj`): + +```xml + + true + +``` + ## .NET globalization and International Components for Unicode (ICU) support (Blazor WebAssembly) :::moniker range=">= aspnetcore-8.0" -Blazor WebAssembly uses a reduced globalization API and set of built-in International Components for Unicode (ICU) locales. For more information, see [.NET globalization and ICU: ICU on WebAssembly](/dotnet/core/extensions/globalization-icu#icu-on-webassembly). +Blazor WebAssembly uses a reduced globalization API and set of built-in International Components for Unicode (ICU) locales. + +In WebAssembly (Wasm) apps, when globalization invariant mode is disabled, an ICU data file is loaded. There are four basic types of these files: + +* `icudt.dat`: Full data +* `icudt_EFIGS.dat`: Data for locales: `en-*`, `fr-FR`, `es-ES`, `it-IT`, and `de-DE`. +* `icudt_CJK.dat`: Data for locales: `en-*`, `ja`, `ko`, and `zh-*`. +* `icudt_no_CJK.dat`: Data for all locales from `icudt.dat`, excluding `ja`, `ko`, and `zh-*`. + +Specify one file to load with the `` MSBuild property in the app's project file (`.csproj`). The following example loads the `icudt_no_CJK.dat` file: + +```xml + + icudt_no_CJK.dat + +``` + +`` only accepts a single file. The file can be a custom file created by the developer. To create a custom ICU file, see [WASM Globalization Icu: Custom ICU](https://github.com/dotnet/runtime/blob/main/docs/design/features/globalization-icu-wasm.md#custom-icu). - +If a file isn't specified with ``, the app's culture is checked, and the corresponding ICU file is loaded for its culture. For example, the `en-US` culture results in loading the `icudt_EFIGS.dat` file. For `zh-CN`, the `icudt_CJK.dat` file is used. -To load a custom ICU data file to control the app's locales, see [WASM Globalization Icu](https://github.com/dotnet/runtime/blob/main/docs/design/features/globalization-icu-wasm.md). Currently, manually building the custom ICU data file is required. .NET tooling to ease the process of creating the file is planned for .NET 10 in November, 2025. +For more information, see [.NET globalization and ICU: ICU on WebAssembly](/dotnet/core/extensions/globalization-icu#icu-on-webassembly). :::moniker-end diff --git a/aspnetcore/client-side/dotnet-interop.md b/aspnetcore/client-side/dotnet-interop.md deleted file mode 100644 index 2b3e0060bce1..000000000000 --- a/aspnetcore/client-side/dotnet-interop.md +++ /dev/null @@ -1,497 +0,0 @@ ---- -title: JavaScript `[JSImport]`/`[JSExport]` interop -author: pavelsavara -description: Learn how to run .NET from JS with [JSImport]/[JSExport] interop in a WebAssembly Browser App project. -monikerRange: '>= aspnetcore-7.0' -ms.author: wpickett -ms.custom: mvc -ms.date: 07/25/2024 -uid: client-side/dotnet-interop ---- -# JavaScript `[JSImport]`/`[JSExport]` interop - -[!INCLUDE[](~/includes/not-latest-version.md)] - -This article explains how to run .NET from JavaScript (JS) using JS `[JSImport]`/`[JSExport]` interop. - -For additional guidance, see the [Configuring and hosting .NET WebAssembly applications](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/features.md) guidance in the .NET Runtime (`dotnet/runtime`) GitHub repository. - -Existing JS apps can use the expanded client-side WebAssembly support to reuse .NET libraries from JS or to build novel .NET-based apps and frameworks. - -> [!NOTE] -> This article focuses on running .NET from JS apps without any dependency on [Blazor](xref:blazor/index). For guidance on using `[JSImport]`/`[JSExport]` interop in Blazor WebAssembly apps, see . - -These approaches are appropriate when you only expect to run on WebAssembly (:::no-loc text="WASM":::). Libraries can make a runtime check to determine if the app is running on :::no-loc text="WASM"::: by calling . - -## Prerequisites - -[.NET SDK (latest version)](https://dotnet.microsoft.com/download/dotnet/) - -Install the `wasm-tools` workload in an administrative command shell, which brings in the related MSBuild targets: - -```dotnetcli -dotnet workload install wasm-tools -``` - -The tools can also be installed via Visual Studio's installer under the **ASP.NET and web development** workload in the Visual Studio installer. Select the **.NET WebAssembly build tools** option from the list of optional components. - -Optionally, install the `wasm-experimental` workload, which contains experimental project templates for getting started with .NET on WebAssembly in a browser app (WebAssembly Browser App) or in a Node.js-based console app (WebAssembly Console App). This workload isn't required if you plan to integrate JS `[JSImport]`/`[JSExport]` interop into an existing JS app. - -```dotnetcli -dotnet workload install wasm-experimental -``` - -The templates can also be installed from the [`Microsoft.NET.Runtime.WebAssembly.Templates`](https://www.nuget.org/packages/Microsoft.NET.Runtime.WebAssembly.Templates) NuGet package with the following command: - -```dotnetcli -dotnet new install Microsoft.NET.Runtime.WebAssembly.Templates -``` - -For more information, see the [Experimental workload and project templates](#experimental-workload-and-project-templates) section. - -## Namespace - -The JS interop API described in this article is controlled by attributes in the namespace. - -## Project configuration - -To configure a project (`.csproj`) to enable JS interop: - -:::moniker range=">= aspnetcore-8.0" - -* Set the [target framework moniker](/dotnet/standard/frameworks) (`{TARGET FRAMEWORK}` placeholder): - - ```xml - {TARGET FRAMEWORK} - ``` - - .NET 7 (`net7.0`) or later is supported. - -* Enable the property, which permits the code generator in the Roslyn compiler to use pointers for JS interop: - - ```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). - -The following is an example project file (`.csproj`) after configuration. The `{TARGET FRAMEWORK}` placeholder is the target framework: - -```xml - - - - {TARGET FRAMEWORK} - true - - - -``` - -:::moniker-end - -:::moniker range="< aspnetcore-8.0" - -* Set the [target framework moniker](/dotnet/standard/frameworks): - - ```xml - net7.0 - ``` - - .NET 7 (`net7.0`) or later is supported. - -* Specify `browser-wasm` for the runtime identifier: - - ```xml - browser-wasm - ``` - -* Specify an executable output type: - - ```xml - Exe - ``` - -* Enable the property, which permits the code generator in the Roslyn compiler to use pointers for JS interop: - - ```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). - -* Specify `WasmMainJSPath` to point to a file on disk. This file is published with the app, but use of the file isn't required if you're integrating .NET into an existing JS app. - - In the following example, the JS file on disk is `main.js`, but any JS filename is permissible: - - ```xml - main.js - ``` - -Example project file (`.csproj`) after configuration: - -```xml - - - - net7.0 - browser-wasm - Exe - true - main.js - enable - - - -``` - -:::moniker-end - -## JavaScript interop on :::no-loc text="WASM"::: - -APIs in the following example are imported from `dotnet.js`. These APIs enable you to set up named modules that can be imported into your C# code and call into methods exposed by your .NET code, including `Program.Main`. - -> [!IMPORTANT] -> "Import" and "export" throughout this article are defined from the perspective of .NET: -> -> * An app imports JS methods so that they can be called from .NET. -> * The app exports .NET methods so that they can be called from JS. - -In the following example: - -* The `dotnet.js` file is used to create and start the .NET WebAssembly runtime. `dotnet.js` is generated as part of the app's build output. - - > [!IMPORTANT] - > To integrate with an existing app, copy the contents of the publish output folder† to the existing app's deployment assets so that it can be served along with the rest of the app. For production deployments, publish the app with the `dotnet publish -c Release` command in a command shell and deploy the output folder's contents with the app. - > - > †The publish output folder is the target location of your publish profile. The default for a **:::no-loc text="Release":::** profile in .NET 8 or later is `bin/Release/{TARGET FRAMEWORK}/publish`, where the `{TARGET FRAMEWORK}` placeholder is the target framework (for example, `net8.0`). - -* `dotnet.create()` sets up the .NET WebAssembly runtime. - -:::moniker range=">= aspnetcore-9.0" - -* `setModuleImports` associates a name with a module of JS functions for import into .NET. The JS module contains a `dom.setInnerText` function, which accepts and element selector and time to display the current stopwatch time in the UI. The name of the module can be any string (it doesn't need to be a file name), but it must match the name used with the `JSImportAttribute` (explained later in this article). The `dom.setInnerText` function is imported into C# and called by the C# method `SetInnerText`. The `SetInnerText` method is shown later in this section. - -* `exports.StopwatchSample.Reset()` calls into .NET (`StopwatchSample.Reset`) from JS. The `Reset` C# method restarts the stopwatch if it's running or resets it if it isn't running. The `Reset` method is shown later in this section. - -* `exports.StopwatchSample.Toggle()` calls into .NET (`StopwatchSample.Toggle`) from JS. The `Toggle` C# method starts or stops the stopwatch depending on if it's currently running or not. The `Toggle` method is shown later in this section. - -* `runMain()` runs `Program.Main`. - -:::moniker-end - -:::moniker range="< aspnetcore-9.0" - -* `setModuleImports` associates a name with a module of JS functions for import into .NET. The JS module contains a `window.location.href` function, which returns the current page address (URL). The name of the module can be any string (it doesn't need to be a file name), but it must match the name used with the `JSImportAttribute` (explained later in this article). The `window.location.href` function is imported into C# and called by the C# method `GetHRef`. The `GetHRef` method is shown later in this section. - -* `exports.MyClass.Greeting()` calls into .NET (`MyClass.Greeting`) from JS. The `Greeting` C# method returns a string that includes the result of calling the `window.location.href` function. The `Greeting` method is shown later in this section. - -* `dotnet.run()` runs `Program.Main`. - -:::moniker-end - -JS module: - -:::moniker range=">= aspnetcore-9.0" - -```javascript -import { dotnet } from './_framework/dotnet.js' - -const { setModuleImports, getAssemblyExports, getConfig, runMain } = await dotnet - .withApplicationArguments("start") - .create(); - -setModuleImports('main.js', { - dom: { - setInnerText: (selector, time) => - document.querySelector(selector).innerText = time - } -}); - -const config = getConfig(); -const exports = await getAssemblyExports(config.mainAssemblyName); - -document.getElementById('reset').addEventListener('click', e => { - exports.StopwatchSample.Reset(); - e.preventDefault(); -}); - -const pauseButton = document.getElementById('pause'); -pauseButton.addEventListener('click', e => { - const isRunning = exports.StopwatchSample.Toggle(); - pauseButton.innerText = isRunning ? 'Pause' : 'Start'; - e.preventDefault(); -}); - -await runMain(); -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" - -```javascript -import { dotnet } from './_framework/dotnet.js' - -const { setModuleImports, getAssemblyExports, getConfig } = await dotnet - .withDiagnosticTracing(false) - .withApplicationArgumentsFromQuery() - .create(); - -setModuleImports('main.js', { - window: { - location: { - href: () => globalThis.window.location.href - } - } -}); - -const config = getConfig(); -const exports = await getAssemblyExports(config.mainAssemblyName); -const text = exports.MyClass.Greeting(); -console.log(text); - -document.getElementById('out').innerHTML = text; -await dotnet.run(); -``` - -:::moniker-end - -:::moniker range="< aspnetcore-8.0" - -```javascript -import { dotnet } from './dotnet.js' - -const is_browser = typeof window != "undefined"; -if (!is_browser) throw new Error(`Expected to be running in a browser`); - -const { setModuleImports, getAssemblyExports, getConfig } = - await dotnet.create(); - -setModuleImports("main.js", { - window: { - location: { - href: () => globalThis.window.location.href - } - } -}); - -const config = getConfig(); -const exports = await getAssemblyExports(config.mainAssemblyName); -const text = exports.MyClass.Greeting(); -console.log(text); - -document.getElementById("out").innerHTML = text; -await dotnet.run(); -``` - -:::moniker-end - -To import a JS function so it can be called from C#, use the new on a matching method signature. The first parameter to the is the name of the JS function to import and the second parameter is the name of the module. - -:::moniker range=">= aspnetcore-9.0" - -In the following example, the `dom.setInnerText` function is called from the `main.js` module when `SetInnerText` method is called: - -```csharp -[JSImport("dom.setInnerText", "main.js")] -internal static partial void SetInnerText(string selector, string content); -``` - -:::moniker-end - -:::moniker range="< aspnetcore-9.0" - -In the following example, the `window.location.href` function is called from the `main.js` module when `GetHRef` method is called: - -```csharp -[JSImport("window.location.href", "main.js")] -internal static partial string GetHRef(); -``` - -:::moniker-end - -In the imported method signature, you can use .NET types for parameters and return values, which are marshalled automatically by the runtime. Use to control how the imported method parameters are marshalled. For example, you might choose to marshal a `long` as or . You can pass / callbacks as parameters, which are marshalled as callable JS functions. You can pass both JS and managed object references, and they are marshaled as proxy objects, keeping the object alive across the boundary until the proxy is garbage collected. You can also import and export asynchronous methods with a result, which are marshaled as [JS promises](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise). Most of the marshalled types work in both directions, as parameters and as return values, on both imported and exported methods. - -Functions accessible on the global namespace can be imported by using the [`globalThis`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/globalThis) prefix in the function name and by using the `[JSImport]` attribute without providing a module name. In the following example, [`console.log`](https://developer.mozilla.org/docs/Web/API/console/log) is prefixed with `globalThis`. The imported function is called by the C# `Log` method, which accepts a C# string message (`message`) and marshalls the C# string to a JS [`String`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) for `console.log`: - -```csharp -[JSImport("globalThis.console.log")] -internal static partial void Log([JSMarshalAs] string message); -``` - -To export a .NET method so it can be called from JS, use the . - -:::moniker range=">= aspnetcore-9.0" - -In the following example, each method is exported to JS and can be called from JS functions: - -* The `Toggle` method starts or stops the stopwatch depending on its running state. -* The `Reset` method restarts the stopwatch if it's running or resets it if it isn't running. -* The `IsRunning` method indicates if the stopwatch is running. - -```csharp -[JSExport] -internal static bool Toggle() -{ - if (stopwatch.IsRunning) - { - stopwatch.Stop(); - return false; - } - else - { - stopwatch.Start(); - return true; - } -} - -[JSExport] -internal static void Reset() -{ - if (stopwatch.IsRunning) - stopwatch.Restart(); - else - stopwatch.Reset(); - - Render(); -} - -[JSExport] -internal static bool IsRunning() => stopwatch.IsRunning; -``` - -:::moniker-end - -:::moniker range="< aspnetcore-9.0" - -In the following example, the `Greeting` method returns a string that includes the result of calling the `GetHRef` method. As shown earlier, the `GetHref` C# method calls into JS for the `window.location.href` function from the `main.js` module. `window.location.href` returns the current page address (URL): - -```csharp -[JSExport] -internal static string Greeting() -{ - var text = $"Hello, World! Greetings from {GetHRef()}"; - Console.WriteLine(text); - return text; -} -``` - -:::moniker-end - -## Experimental workload and project templates - -To demonstrate the JS interop functionality and obtain JS interop project templates, install the `wasm-experimental` workload: - -```dotnetcli -dotnet workload install wasm-experimental -``` - -The `wasm-experimental` workload contains two project templates: `wasmbrowser` and `wasmconsole`. These templates are experimental at this time, which means the developer workflow for the templates is evolving. However, the .NET and JS APIs used in the templates are supported in .NET 8 and provide a foundation for using .NET on :::no-loc text="WASM"::: from JS. - -The templates can also be installed from the [`Microsoft.NET.Runtime.WebAssembly.Templates`](https://www.nuget.org/packages/Microsoft.NET.Runtime.WebAssembly.Templates) NuGet package with the following command: - -```dotnetcli -dotnet new install Microsoft.NET.Runtime.WebAssembly.Templates -``` - -### Browser app - -You can create a browser app with the `wasmbrowser` template from the command line, which creates a web app that demonstrates using .NET and JS together in a browser: - -```dotnetcli -dotnet new wasmbrowser -``` - -Alternatively in Visual Studio, you can create the app using the **:::no-loc text="WebAssembly Browser App":::** project template. - -Build the app from Visual Studio or by using the .NET CLI: - -```dotnetcli -dotnet build -``` - -Build and run the app from Visual Studio or by using the .NET CLI: - -```dotnetcli -dotnet run -``` - -Alternatively, install and use the [`dotnet serve` command](https://github.com/natemcmaster/dotnet-serve): - -```dotnetcli -dotnet serve -d:bin/$(Configuration)/{TARGET FRAMEWORK}/publish -``` - -In the preceding example, the `{TARGET FRAMEWORK}` placeholder is the [target framework moniker](/dotnet/standard/frameworks). - -### Node.js console app - -You can create a console app with the `wasmconsole` template, which creates an app that runs under :::no-loc text="WASM"::: as a [Node.js](https://nodejs.org/) or [V8](https://developers.google.com/apps-script/guides/v8-runtime) console app: - -```dotnetcli -dotnet new wasmconsole -``` - -Alternatively in Visual Studio, you can create the app using the **:::no-loc text="WebAssembly Console App":::** project template. - -Build the app from Visual Studio or by using the .NET CLI: - -```dotnetcli -dotnet build -``` - -Build and run the app from Visual Studio or by using the .NET CLI: - -```dotnetcli -dotnet run -``` - -Alternatively, start any static file server from the publish output directory that contains the `main.mjs` file: - -``` -node bin/$(Configuration)/{TARGET FRAMEWORK}/{PATH}/main.mjs -``` - -In the preceding example, the `{TARGET FRAMEWORK}` placeholder is the [target framework moniker](/dotnet/standard/frameworks), and the `{PATH}` placeholder is the path to the `main.mjs` file. - -:::moniker range=">= aspnetcore-10.0" - -## Control Hot Reload - -The `WasmEnableHotReload` MSBuild property enables [Hot Reload](xref:test/hot-reload) and is set to `true` by default when building in the `Debug` configuration. Hot Reload isn't enabled (set to `false`) when building in any other configuration. - -To use a custom configuration name when debugging, for example, `DebugWebAssembly`, set the property to `true` to enable Hot Reload: - -```xml - - true - -``` - -To disable Hot Reload for the `Debug` configuration, set the value to `false`: - -```xml - - false - -``` - -:::moniker-end - -## Additional resources - -* [Configuring and hosting .NET WebAssembly applications](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/features.md) -* API documentation - * [`[JSImport]` attribute](xref:System.Runtime.InteropServices.JavaScript.JSImportAttribute) - * [`[JSExport]` attribute](xref:System.Runtime.InteropServices.JavaScript.JSExportAttribute) -* -* In the `dotnet/runtime` GitHub repository: - * [.NET WebAssembly runtime](https://github.com/dotnet/runtime/tree/main/src/mono/wasm) - * [`dotnet.d.ts` file (.NET WebAssembly runtime configuration)](https://github.com/dotnet/runtime/blob/main/src/mono/browser/runtime/dotnet.d.ts) -* [Use .NET from any JavaScript app in .NET 7](https://devblogs.microsoft.com/dotnet/use-net-7-from-any-javascript-app-in-net-7/) -* Including static assets from a Razor class library - * (Blazor documentation) - * (Razor Pages documentation) diff --git a/aspnetcore/client-side/dotnet-on-webworkers.md b/aspnetcore/client-side/dotnet-on-webworkers.md new file mode 100644 index 000000000000..409f9677726f --- /dev/null +++ b/aspnetcore/client-side/dotnet-on-webworkers.md @@ -0,0 +1,203 @@ +--- +title: .NET on Web Workers +author: guardrex +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. +monikerRange: '>= aspnetcore-8.0' +ms.author: wpickett +ms.custom: mvc +ms.date: 10/03/2025 +uid: client-side/dotnet-on-webworkers +--- +# .NET on Web Workers + + + +Modern web apps often require intensive computational tasks that can block the main UI thread, leading to poor user experience. [Web Workers](https://developer.mozilla.org/docs/Web/API/Web_Workers_API) provide a solution to this problem by enabling JavaScript (JS) to run on separate threads. With .NET WebAssembly (Wasm), you can run C# code in Web Workers, combining the performance benefits of compiled code with the non-blocking execution model of background threads. + +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. + +## Sample app + +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`. + +## 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: + +```bash +dotnet workload install wasm-tools +dotnet workload install wasm-experimental +``` + +For the React.js frontend, [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com) must be installed. + +Create a new React app: + +```bash +npx create-react-app react-app +cd react-app +``` + +## Create the .NET WebAssembly project + +Create a new WebAssembly browser project to serve as the Web Worker: + +```bash +dotnet new wasmbrowser -n DotNetOnWebWorkers +cd DotNetOnWebWorkers +``` + +Modify the `Program.cs` file to set up the Web Worker entry point and message handling: + +```csharp +using System; +using System.Runtime.InteropServices.JavaScript; +using QRCoder; +using System.Linq; + +public partial class QRGenerator +{ + private static readonly int MAX_QR_SIZE = 20; + + [JSExport] + internal static byte[] Generate(string text, int qrSize) + { + if (qrSize >= MAX_QR_SIZE) + { + throw new Exception( + $"QR code size must be less than {MAX_QR_SIZE}. Try again."); + } + QRCodeGenerator qrGenerator = new QRCodeGenerator(); + QRCodeData qrCodeData = qrGenerator.CreateQrCode( + text, QRCodeGenerator.ECCLevel.Q); + BitmapByteQRCode qrCode = new BitmapByteQRCode(qrCodeData); + return qrCode.GetGraphic(qrSize); + } +} +``` + +Add a `wwwroot/worker.js` file with code that interops between C# and JS: + +```javascript +import { dotnet } from './_framework/dotnet.js' + +let assemblyExports = null; +let startupError = undefined; + +try { + const { getAssemblyExports, getConfig } = await dotnet.create(); + const config = getConfig(); + assemblyExports = await getAssemblyExports(config.mainAssemblyName); +} +catch (err) { + startupError = err.message; +} + +self.addEventListener('message', async function(e) { + try { + if (!assemblyExports) { + throw new Error(startupError || "worker exports not loaded"); + } + let result = null; + switch (e.data.command) { + case "generateQR": + const size = Number(e.data.size); + const text = e.data.text; + if (size === undefined || text === undefined) + new Error("Inner error, got empty QR generation data from React"); + result = assemblyExports.QRGenerator.Generate(text, size); + break; + default: + throw new Error("Unknown command: " + e.data.command); + } + self.postMessage({ + command: "response", + requestId: e.data.requestId, + result, + }); + } + catch (err) { + self.postMessage({ + command: "response", + requestId: e.data.requestId, + error: err.message, + }); + } +}, false); +``` + +Build the WebAssembly project to generate the necessary files: + +```bash +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. + +Create a Web Worker file `client.js` to receive messages from dotnet: + +```javascript +const dotnetWorker = new Worker('../../qr/wwwroot/worker.js', { type: "module" } ); + +dotnetWorker.addEventListener('message', async function (e) { + switch (e.data.command) { + case "response": + if (!e.data.requestId) { + console.error("No requestId in response from worker"); + } + const request = pendingRequests[e.data.requestId]; + delete pendingRequests[e.data.requestId]; + if (e.data.error) { + request.reject(new Error(e.data.error)); + } + request.resolve(e.data.result); + break; + default: + console.log('Worker said: ', e.data); + break; + } +}, false); +``` + +Connect this functionality with UI and add a button that triggers `generateQR`: + +```javascript +export async function generateQR(text, size) { + const response = await sendRequestToWorker({ + command: "generateQR", + text: text, + size: size + }); + const blob = new Blob([response], { type: 'image/png' }); + return URL.createObjectURL(blob); +} + +function sendRequestToWorker(request) { + pendingRequestId++; + const promise = new Promise((resolve, reject) => { + pendingRequests[pendingRequestId] = { resolve, reject }; + }); + dotnetWorker.postMessage({ + ...request, + requestId: pendingRequestId + }); + return promise; +} +``` + +## Performance considerations and optimization + +When working with .NET on Web Workers, consider these key optimization strategies: + +* **Minimize data transfer**: Serialize only essential data between the main thread and worker to reduce communication overhead. +* **Batch operations**: Group multiple calculations together rather than sending individual requests. +* **Memory management**: Be mindful of memory usage in the WebAssembly environment, especially for long-running workers. +* **Startup cost**: WebAssembly initialization has overhead, so prefer persistent workers over frequent creation/destruction. + +See the [sample app](#sample-app) for a demonstration of the preceding concepts. diff --git a/aspnetcore/fundamentals/error-handling.md b/aspnetcore/fundamentals/error-handling.md index c1844501a9ed..885e6da37752 100644 --- a/aspnetcore/fundamentals/error-handling.md +++ b/aspnetcore/fundamentals/error-handling.md @@ -1,11 +1,12 @@ --- title: Handle errors in ASP.NET Core +ai-usage: ai-assisted author: tdykstra description: Discover how to handle errors in ASP.NET Core apps. monikerRange: '>= aspnetcore-3.1' ms.author: tdykstra ms.custom: mvc -ms.date: 01/15/2025 +ms.date: 09/25/2025 uid: fundamentals/error-handling --- # Handle errors in ASP.NET Core @@ -79,11 +80,13 @@ Another way to use a lambda is to set the status code based on the exception typ ## IExceptionHandler -[IExceptionHandler](/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandler) is an interface that gives the developer a callback for handling known exceptions in a central location. +[IExceptionHandler](/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandler) is an interface that gives the developer a callback for handling known exceptions in a central location. The interface contains a single method, [`TryHandleAsync`](/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandler.tryhandleasync), which receives an `HttpContext` and an `Exception` parameter. `IExceptionHandler` implementations are registered by calling [`IServiceCollection.AddExceptionHandler`](/dotnet/api/microsoft.extensions.dependencyinjection.exceptionhandlerservicecollectionextensions.addexceptionhandler). The lifetime of an `IExceptionHandler` instance is singleton. Multiple implementations can be added, and they're called in the order registered. -If an exception handler handles a request, it can return `true` to stop processing. If an exception isn't handled by any exception handler, then control falls back to the default behavior and options from the middleware. Different metrics and logs are emitted for handled versus unhandled exceptions. +Exception handling middleware iterates through registered exception handlers in order until one returns `true` from `TryHandleAsync`, indicating that the exception has been handled. If an exception handler handles an exception, it can return `true` to stop processing. If an exception isn't handled by any exception handler, then control falls back to the default behavior and options from the middleware. + +Starting in .NET 10, the default behavior is to suppress emission of diagnostics such as logs and metrics for handled exceptions (when `TryHandleAsync` returns `true`). This differs from earlier versions (.NET 8 and 9) where diagnostics were always emitted regardless of whether the exception was handled. The default behavior can be changed by setting [SuppressDiagnosticsCallback](#suppressdiagnosticscallback). The following example shows an `IExceptionHandler` implementation: @@ -103,6 +106,30 @@ In other environments: * The `CustomExceptionHandler` is called first to handle an exception. * After logging the exception, the `TryHandleAsync` method returns `false`, so the [`/Error` page](#exception-handler-page) is shown. +### SuppressDiagnosticsCallback + +Starting in .NET 10, you can control whether the exception handling middleware writes diagnostics for handled exceptions by configuring the `SuppressDiagnosticsCallback` property on `ExceptionHandlerOptions`. This callback receives the exception context and allows you to determine whether diagnostics should be suppressed based on the specific exception or request. + +To revert to the .NET 8 and 9 behavior where diagnostics are always emitted for handled exceptions, set the callback to always return `false`: + +```csharp +app.UseExceptionHandler(new ExceptionHandlerOptions +{ + SuppressDiagnosticsCallback = context => false +}); +``` + +You can also conditionally suppress diagnostics based on the exception type or other context: + +```csharp +app.UseExceptionHandler(new ExceptionHandlerOptions +{ + SuppressDiagnosticsCallback = context => context.Exception is ArgumentException +}); +``` + +When an exception isn't handled by any `IExceptionHandler` implementation (all handlers return `false` from `TryHandleAsync`), control falls back to the default behavior and options from the middleware, and diagnostics are emitted according to the middleware's standard behavior. + diff --git a/aspnetcore/fundamentals/minimal-apis/responses.md b/aspnetcore/fundamentals/minimal-apis/responses.md index 522cfa0b4537..68fc3420970c 100644 --- a/aspnetcore/fundamentals/minimal-apis/responses.md +++ b/aspnetcore/fundamentals/minimal-apis/responses.md @@ -84,13 +84,7 @@ In order to document this endpoint correctly the extensions method `Produces` is For more information about describing a response type, see [OpenAPI support in minimal APIs](/aspnet/core/fundamentals/openapi/aspnetcore-openapi#describe-response-types-1). -As mentioned previously, when using `TypedResults`, a conversion is not needed. Consider the following minimal API which returns a `TypedResults` class - -:::code language="csharp" source="~/../AspNetCore.Docs.Samples/fundamentals/minimal-apis/samples/MinApiTestsSample/WebMinRouteGroup/TodoEndpointsV1.cs" id="snippet_1"::: - -The following test checks for the full concrete type: - -:::code language="csharp" source="~/../AspNetCore.Docs.Samples/fundamentals/minimal-apis/samples/MinApiTestsSample/UnitTests/TodoInMemoryTests.cs" id="snippet_11" highlight="26"::: +For examples on testing result types, see the [Test documentation](/aspnet/core/fundamentals/minimal-apis/test-min-api#unit-test-iresult-implementation-types). Because all methods on `Results` return `IResult` in their signature, the compiler automatically infers that as the request delegate return type when returning different results from a single endpoint. `TypedResults` requires the use of `Results` from such delegates. diff --git a/aspnetcore/fundamentals/minimal-apis/test-min-api.md b/aspnetcore/fundamentals/minimal-apis/test-min-api.md index f181be4e66c2..04f08e966fb2 100644 --- a/aspnetcore/fundamentals/minimal-apis/test-min-api.md +++ b/aspnetcore/fundamentals/minimal-apis/test-min-api.md @@ -34,6 +34,13 @@ The following code uses the :::code language="csharp" source="~/../AspNetCore.Docs.Samples/fundamentals/minimal-apis/samples/MinApiTestsSample/UnitTests/TodoInMemoryTests.cs" id="snippet_1" highlight="18"::: +In the previous examples, the result is cast to a concrete type because the endpoint under test can return multiple types (a or ) result. +However, if the endpoint returns a single type, then the result is automatically inferred to that type and no casting is required. + +The following code uses the class, and the value's type is a collection of `Todo`: + +:::code language="csharp" source="~/../AspNetCore.Docs.Samples/fundamentals/minimal-apis/samples/MinApiTestsSample/UnitTests/TodoInMemoryTests.cs" id="snippet_11" highlight="26"::: + ## Additional Resources * [Basic authentication tests](https://github.com/blowdart/idunno.Authentication/tree/dev/test/idunno.Authentication.Basic.Test) is not a .NET repository but was written by a member of the .NET team. It provides examples of basic authentication testing. diff --git a/aspnetcore/security/authentication/configure-jwt-bearer-authentication.md b/aspnetcore/security/authentication/configure-jwt-bearer-authentication.md index 82ffa1c784ec..61252a1a47e8 100644 --- a/aspnetcore/security/authentication/configure-jwt-bearer-authentication.md +++ b/aspnetcore/security/authentication/configure-jwt-bearer-authentication.md @@ -5,7 +5,7 @@ description: Learn how to set up JWT bearer authentication in an ASP.NET Core ap monikerRange: '>= aspnetcore-8.0' ms.author: tdykstra ms.custom: mvc -ms.date: 12/7/2024 +ms.date: 09/29/2025 uid: security/authentication/configure-jwt-bearer-authentication --- # Configure JWT bearer authentication in ASP.NET Core @@ -111,7 +111,7 @@ When an API uses JWT access tokens for authorization, the API only validates the OpenID Connect (OIDC) and OAuth 2.0 provide standardized, secure frameworks for token acquisition. Token acquisition varies depending on the type of app. Due to the complexity of secure token acquisition, it's highly recommended to rely on these standards: * For apps acting on behalf of a user and an application: OIDC is the preferred choice, enabling delegated user access. In web apps, the confidential code flow with [Proof Key for Code Exchange](https://oauth.net/2/pkce/) (PKCE) is recommended for enhanced security. - * If the calling app is an ASP.NET Core app with server-side [OIDC authentication](/aspnet/core/security/authentication/configure-oidc-web-authentication), you can use the [SaveTokens](/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.savetokens) option to store access token in a cookie for later use via [`HttpContext.GetTokenAsync("access_token")`](/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.gettokenasync). + * If the calling app is an ASP.NET Core app with server-side [OIDC authentication](/aspnet/core/security/authentication/configure-oidc-web-authentication), you can use the property to store access token in a cookie for later use via [`HttpContext.GetTokenAsync("access_token")`](xref:Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.GetTokenAsync%2A). * If the app has no user: The OAuth 2.0 client credentials flow is suitable for obtaining application access tokens. ## Implementing JWT bearer token authentication @@ -131,7 +131,7 @@ If any of these claims or values are incorrect, the API should return a 401 resp ### JWT bearer token basic validation -A basic implementation of the [AddJwtBearer](/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer) can validate just the audience and the issuer. The signature must be validated so that the token can be trusted and that it hasn't been tampered with. +A basic implementation of the can validate just the audience and the issuer. The signature must be validated so that the token can be trusted and that it hasn't been tampered with. ```csharp builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) @@ -144,7 +144,7 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) ### JWT bearer token explicit validation -The [AddJwtBearer](/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer) method provides multiple configurations. Some secure token providers use a non-standard metadata address and the parameter can be setup explicitly. The API can accept multiple issuers or audiences. +The method provides multiple configurations. Some secure token providers use a non-standard metadata address and the parameter can be setup explicitly. The API can accept multiple issuers or audiences. Explicitly defining the parameters is not required. The definitions depends on the access token claim values and the secure token server used to validate the access token. You should use the default values if possible. @@ -191,7 +191,7 @@ builder.Services.AddAuthorizationBuilder() .SetDefaultPolicy(requireAuthPolicy); ``` -The [Authorize](/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute can also be used to force the authentication. If multiple schemes are used, the bearer scheme generally needs to be set as the default authentication scheme or specified via `[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme])`. +The attribute can also be used to force the authentication. If multiple schemes are used, the bearer scheme generally needs to be set as the default authentication scheme or specified via `[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme])`. Authorization in controllers: @@ -263,9 +263,9 @@ This is easy to implement but the client application has full application access ## Handling access tokens -When using access tokens in a client application, the access tokens need to be rotated, persisted and stored somewhere on the server. In a web application, cookies are used to secure the session and can be used to store tokens via [SaveTokens](/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.savetokens) option. +When using access tokens in a client application, the access tokens must be rotated, persisted, and stored on the server. In a web app, cookies are used to secure the session and can be used to store tokens via the property. -`SaveTokens` will not currently refresh access tokens automatically, but this functionality is planned for .NET 10. Follow https://github.com/dotnet/aspnetcore/issues/8175 for updates. In the meantime, you can manually refresh the access token as [demonstrated in the Blazor Web App with OIDC documentation](/aspnet/core/blazor/security/blazor-web-app-with-oidc?pivots=with-bff-pattern#token-refresh) or use a third-party NuGet package like [Duende.AccessTokenManagement.OpenIdConnect](https://www.nuget.org/packages/Duende.AccessTokenManagement.OpenIdConnect) for handling and managing access tokens in the client app. For more information, see [Duende token management](https://docs.duendesoftware.com/identityserver/v7/quickstarts/3a_token_management/). + doesn't refresh access tokens automatically, but this functionality is planned for a future release. In the meantime, you can manually refresh the access token as [demonstrated in the Blazor Web App with OIDC documentation](/aspnet/core/blazor/security/blazor-web-app-with-oidc?pivots=with-bff-pattern#token-refresh) or use a third-party NuGet package, such as [`Duende.AccessTokenManagement.OpenIdConnect`](https://www.nuget.org/packages/Duende.AccessTokenManagement.OpenIdConnect). For more information, see [Duende token management](https://docs.duendesoftware.com/identityserver/v7/quickstarts/3a_token_management/). > [!NOTE] > If deploying to production, the cache should work in a multi-instance deployment. A persistent cache is normally required. diff --git a/aspnetcore/security/cors.md b/aspnetcore/security/cors.md index 271c4a5f51ed..be7931ff921b 100644 --- a/aspnetcore/security/cors.md +++ b/aspnetcore/security/cors.md @@ -4,7 +4,7 @@ author: tdykstra description: Learn how CORS as a standard for allowing or rejecting cross-origin requests in an ASP.NET Core app. ms.author: tdykstra ms.custom: mvc -ms.date: 9/02/2024 +ms.date: 09/29/2025 uid: security/cors --- # Enable Cross-Origin Requests (CORS) in ASP.NET Core @@ -211,6 +211,8 @@ This section describes the various options that can be set in a CORS policy: [!code-csharp[](~/security/cors/8.0sample/Cors/Web2API/Program.cs?name=snippet_aa)] +In the preceding code, `SetIsOriginAllowedToAllowWildcardSubdomains` is called with the base origin `"https://example.com"`. This configuration allows CORS requests from any subdomain of `example.com`, such as `https://subdomain.example.com` or `https://api.example.com`. The wildcard matching is handled by the method, so the origin should be specified without the `*` wildcard character. + ### Set the allowed HTTP methods : diff --git a/aspnetcore/security/cors/3.1sample/Cors/WebAPI/StartupAllowSubdomain.cs b/aspnetcore/security/cors/3.1sample/Cors/WebAPI/StartupAllowSubdomain.cs index 802bdb85cc81..e552c2d6cf17 100644 --- a/aspnetcore/security/cors/3.1sample/Cors/WebAPI/StartupAllowSubdomain.cs +++ b/aspnetcore/security/cors/3.1sample/Cors/WebAPI/StartupAllowSubdomain.cs @@ -27,7 +27,7 @@ public void ConfigureServices(IServiceCollection services) options.AddPolicy("MyAllowSubdomainPolicy", policy => { - policy.WithOrigins("https://*.example.com") + policy.WithOrigins("https://example.com") .SetIsOriginAllowedToAllowWildcardSubdomains(); }); #endregion diff --git a/aspnetcore/security/cors/6.0sample/Cors/WebAPI/Program.cs b/aspnetcore/security/cors/6.0sample/Cors/WebAPI/Program.cs index e809f1d456f2..afd8007d4cbd 100644 --- a/aspnetcore/security/cors/6.0sample/Cors/WebAPI/Program.cs +++ b/aspnetcore/security/cors/6.0sample/Cors/WebAPI/Program.cs @@ -259,7 +259,7 @@ options.AddPolicy(name: MyAllowSpecificOrigins, policy => { - policy.WithOrigins("https://*.example.com") + policy.WithOrigins("https://example.com") .SetIsOriginAllowedToAllowWildcardSubdomains(); }); }); diff --git a/aspnetcore/security/cors/8.0sample/Cors/Web2API/Program.cs b/aspnetcore/security/cors/8.0sample/Cors/Web2API/Program.cs index 01ad4447f691..0c7ac891ee62 100644 --- a/aspnetcore/security/cors/8.0sample/Cors/Web2API/Program.cs +++ b/aspnetcore/security/cors/8.0sample/Cors/Web2API/Program.cs @@ -261,7 +261,7 @@ options.AddPolicy(name: MyAllowSpecificOrigins, policy => { - policy.WithOrigins("https://*.example.com") + policy.WithOrigins("https://example.com") .SetIsOriginAllowedToAllowWildcardSubdomains(); }); }); diff --git a/aspnetcore/security/cors/includes/cors56.md b/aspnetcore/security/cors/includes/cors56.md index d419d2e64010..20917f7c1520 100644 --- a/aspnetcore/security/cors/includes/cors56.md +++ b/aspnetcore/security/cors/includes/cors56.md @@ -206,6 +206,8 @@ This section describes the various options that can be set in a CORS policy: [!code-csharp[](~/security/cors/6.0sample/Cors/WebAPI/Program.cs?name=snippet_aa)] +In the preceding code, `SetIsOriginAllowedToAllowWildcardSubdomains` is called with the base origin `"https://example.com"`. This configuration allows CORS requests from any subdomain of `example.com`, such as `https://subdomain.example.com` or `https://api.example.com`. The wildcard matching is handled by the method, so the origin should be specified without the `*` wildcard character. + ### Set the allowed HTTP methods : @@ -821,6 +823,8 @@ This section describes the various options that can be set in a CORS policy: [!code-csharp[](~/security/cors/3.1sample/Cors/WebAPI/StartupAllowSubdomain.cs?name=snippet)] +In the preceding code, `SetIsOriginAllowedToAllowWildcardSubdomains` is called with the base origin `"https://example.com"`. This configuration allows CORS requests from any subdomain of `example.com`, such as `https://subdomain.example.com` or `https://api.example.com`. The wildcard matching is handled by the method, so the origin should be specified without the `*` wildcard character. + ### Set the allowed HTTP methods : diff --git a/aspnetcore/security/cors/includes/cors7.md b/aspnetcore/security/cors/includes/cors7.md index 299bb7e767cd..e81a91f566ea 100644 --- a/aspnetcore/security/cors/includes/cors7.md +++ b/aspnetcore/security/cors/includes/cors7.md @@ -207,6 +207,8 @@ This section describes the various options that can be set in a CORS policy: [!code-csharp[](~/security/cors/8.0sample/Cors/Web2API/Program.cs?name=snippet_aa)] +In the preceding code, `SetIsOriginAllowedToAllowWildcardSubdomains` is called with the base origin `"https://example.com"`. This configuration allows CORS requests from any subdomain of `example.com`, such as `https://subdomain.example.com` or `https://api.example.com`. The wildcard matching is handled by the method, so the origin should be specified without the `*` wildcard character. + ### Set the allowed HTTP methods : diff --git a/aspnetcore/security/cors/sample/CorsExample4/Startup.cs b/aspnetcore/security/cors/sample/CorsExample4/Startup.cs index ec657bcb4b02..04cadbc80553 100644 --- a/aspnetcore/security/cors/sample/CorsExample4/Startup.cs +++ b/aspnetcore/security/cors/sample/CorsExample4/Startup.cs @@ -100,7 +100,7 @@ public void ConfigureServices(IServiceCollection services) options.AddPolicy("AllowSubdomain", policy => { - policy.WithOrigins("https://*.example.com") + policy.WithOrigins("https://example.com") .SetIsOriginAllowedToAllowWildcardSubdomains(); }); // END11 diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index 6fa58763fc74..4660100223ff 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -776,6 +776,8 @@ items: uid: client-side/dotnet-interop/index - name: JS interop (WebAssembly Browser App) uid: client-side/dotnet-interop/wasm-browser-app + - name: .NET on Web Workers + uid: client-side/dotnet-on-webworkers - name: Grunt uid: client-side/using-grunt - name: Bundle and minify