Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions aspnetcore/blazor/forms/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Learn how to use validation in Blazor forms.
monikerRange: '>= aspnetcore-3.1'
ms.author: wpickett
ms.custom: mvc
ms.date: 11/12/2024
ms.date: 09/08/2025
uid: blazor/forms/validation
---
# ASP.NET Core Blazor forms validation
Expand Down Expand Up @@ -134,6 +134,12 @@ The <xref:Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator> compon
* [`DataAnnotationsValidator`](https://github.com/dotnet/AspNetCore/blob/main/src/Components/Forms/src/DataAnnotationsValidator.cs)
* [`EnableDataAnnotationsValidation`](https://github.com/dotnet/AspNetCore/blob/main/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs)

:::moniker range=">= aspnetcore-10.0"

For details on validation behavior, see the [`DataAnnotationsValidator` validation behavior](#dataannotationsvalidator-validation-behavior) section.

:::moniker-end

If you need to enable data annotations validation support for an <xref:Microsoft.AspNetCore.Components.Forms.EditContext> in code, call <xref:Microsoft.AspNetCore.Components.Forms.EditContextDataAnnotationsExtensions.EnableDataAnnotationsValidation%2A> with an injected <xref:System.IServiceProvider> (`@inject IServiceProvider ServiceProvider`) on the <xref:Microsoft.AspNetCore.Components.Forms.EditContext>. For an advanced example, see the [`NotifyPropertyChangedValidationComponent` component in the ASP.NET Core Blazor framework's `BasicTestApp` (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/blob/main/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor). In a production version of the example, replace the `new TestServiceProvider()` argument for the service provider with an injected <xref:System.IServiceProvider>.

[!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)]
Expand Down Expand Up @@ -1638,26 +1644,18 @@ In the following `OrderPage` component, the <xref:Microsoft.AspNetCore.Component

The requirement to declare the model types outside of Razor components (`.razor` files) is due to the fact that both the new validation feature and the Razor compiler itself are using a source generator. Currently, output of one source generator can't be used as an input for another source generator.

## Complex types

Blazor provides support for validating form input using data annotations with the built-in <xref:Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator>. However, the <xref:Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator> only validates top-level properties of the model bound to the form that aren't complex-type properties.

To validate the bound model's entire object graph, including complex-type properties, use the `ObjectGraphDataAnnotationsValidator` provided by the *experimental* [`Microsoft.AspNetCore.Components.DataAnnotations.Validation`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) package.

> [!NOTE]
> The `ObjectGraphDataAnnotationsValidator` isn't compatible with [nested objects and collection types validation](#nested-objects-and-collection-types), but it's capable of validating nested objects and collection types on its own.

:::moniker-end

:::moniker range="< aspnetcore-10.0"

## Nested objects, collection types, and complex types

Blazor provides support for validating form input using data annotations with the built-in <xref:Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator>. However, the <xref:Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator> only validates top-level properties of the model bound to the form that aren't collection- or complex-type properties.
> [!NOTE]
> For apps targeting .NET 10 or later, we no longer recommend using the *experimental* [`Microsoft.AspNetCore.Components.DataAnnotations.Validation`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) package and approach described in this section. We recommend using the built-in validation features of the the <xref:Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator> component.

To validate the bound model's entire object graph, including collection- and complex-type properties, use the `ObjectGraphDataAnnotationsValidator` provided by the *experimental* [`Microsoft.AspNetCore.Components.DataAnnotations.Validation`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) package:
Blazor provides support for validating form input using data annotations with the built-in <xref:Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator>. However, the <xref:Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator> in .NET 9 or earlier only validates top-level properties of the model bound to the form that aren't collection- or complex-type properties.

:::moniker-end
To validate the bound model's entire object graph, including collection- and complex-type properties, use the `ObjectGraphDataAnnotationsValidator` provided by the *experimental* [`Microsoft.AspNetCore.Components.DataAnnotations.Validation`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) package in .NET 9 or earlier:

```razor
<EditForm ...>
Expand Down Expand Up @@ -1703,6 +1701,8 @@ public class ShipDescription
}
```

:::moniker-end

## Enable the submit button based on form validation

To enable and disable the submit button based on form validation, the following example:
Expand Down Expand Up @@ -1829,3 +1829,17 @@ A side effect of the preceding approach is that a validation summary (<xref:Micr
}
}
```

:::moniker range=">= aspnetcore-10.0"

## `DataAnnotationsValidator` validation behavior

The <xref:Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator> component has the same validation order and short-circuiting behavior as <xref:System.ComponentModel.DataAnnotations.Validator?displayProperty=nameWithType>. The following rules are applied when validating an instance of type `T`:

1. Member properties of `T` are validated, including recursively validating nested objects.
1. Type-level attributes of `T` are validated.
1. The <xref:System.ComponentModel.DataAnnotations.IValidatableObject.Validate%2A?displayProperty=nameWithType> method is executed, if `T` implements it.

If one of the preceding steps produces a validation error, the remaining steps are skipped.

:::moniker-end
27 changes: 26 additions & 1 deletion aspnetcore/blazor/fundamentals/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Learn how to manage Blazor app request routing and how to use the N
monikerRange: '>= aspnetcore-3.1'
ms.author: wpickett
ms.custom: mvc
ms.date: 11/12/2024
ms.date: 09/08/2025
uid: blazor/fundamentals/routing
---
# ASP.NET Core Blazor routing and navigation
Expand Down Expand Up @@ -704,6 +704,31 @@ The following component:

For more information on component disposal, see <xref:blazor/components/component-disposal>.

:::moniker range=">= aspnetcore-9.0"

## Navigation Manager redirect behavior during static server-side rendering (static SSR)

For a redirect during static server-side rendering (static SSR), <xref:Microsoft.AspNetCore.Components.NavigationManager> relies on throwing a <xref:Microsoft.AspNetCore.Components.NavigationException> that gets captured by the framework, which converts the error into a redirect. Code that exists after the call to <xref:Microsoft.AspNetCore.Components.NavigationManager.NavigateTo%2A> isn't called. When using Visual Studio, the debugger breaks on the exception, requiring you to deselect the checkbox for **Break when this exception type is user-handled** in the Visual Studio UI to avoid the debugger stopping for future redirects.

:::moniker-end

:::moniker range=">= aspnetcore-10.0"

You can use the `<BlazorDisableThrowNavigationException>` MSBuild property set to `true` in the app's project file to opt-in to no longer throwing a <xref:Microsoft.AspNetCore.Components.NavigationException>. This behavior is enabled by default in the .NET 10 or later Blazor Web App project template:

```xml
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
```

:::moniker-end

:::moniker range=">= aspnetcore-9.0 < aspnetcore-10.0"

> [!NOTE]
> In .NET 10 or later, you can opt-in to not throwing a <xref:Microsoft.AspNetCore.Components.NavigationException> by setting the `<BlazorDisableThrowNavigationException>` MSBuild property to `true` in the app's project file. To take advantage of the new MSBuild property and behavior, upgrade the app to .NET 10 or later.

:::moniker-end

:::moniker range=">= aspnetcore-10.0"

## Not Found responses
Expand Down
143 changes: 85 additions & 58 deletions aspnetcore/blazor/host-and-deploy/configure-trimmer.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Learn how to control the Intermediate Language (IL) Trimmer when bu
monikerRange: '>= aspnetcore-5.0'
ms.author: wpickett
ms.custom: mvc
ms.date: 11/12/2024
ms.date: 09/08/2025
uid: blazor/host-and-deploy/configure-trimmer
---
# Configure the Trimmer for ASP.NET Core Blazor
Expand Down Expand Up @@ -42,73 +42,123 @@ For more information, see [Trimming options (.NET documentation)](/dotnet/core/d

## Failure to preserve types used by a published app
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javiercn Can you review the recommendations made in this section for dealing with type preservation for JS interop related scenarios?


Trimming may have detrimental effects for a published app leading to runtime errors. In apps that use [reflection](/dotnet/csharp/advanced-topics/reflection-and-attributes/), the IL Trimmer often can't determine the required types for runtime reflection and trims them away or trims away parameter names from methods. This can happen with complex framework types used for JS interop, JSON serialization/deserialization, and other operations.
Trimming may have detrimental effects for a published app leading to runtime errors, even in spite of setting the [`<PublishTrimmed>` property](#configuration) to `false` in the project file. In apps that use [reflection](/dotnet/csharp/advanced-topics/reflection-and-attributes/), the IL Trimmer often can't determine the required types for runtime reflection and trims them away or trims away parameter names from methods. This can happen with complex framework types used for JS interop, JSON serialization/deserialization, and other operations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even in spite of setting the <PublishTrimmed> property to false

This doesn't sound right. Setting PublishTrimmed to false should disable trimming for the entire app. Does this not work? If it doesn't, I think it's a bug that we should fix, not document.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll double check this in the morning and report back.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danroth27 ... Confirmed!

image image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gaurdrex Can you create an issue for this in the dotnet/aspnetcore repo and share your project with us?

Copy link
Collaborator Author

@guardrex guardrex Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danroth27 ... Isn't this the same as (or at least has the elements of) dotnet/aspnetcore#52947? It's just not KeyValuePair ... it's a Tuple ... but same situation/error. I thought @javiercn was well aware of this, and I placed the content that he requested to cover it. I just want to make sure I'm opening an issue that @javiercn won't close saying that it can never be fixed outside of the approaches provided ... in order: use a custom type (any .NET), Dynamic Dependency (.NET 5+), Root Descriptor (.NET 10+), <_ExtraTrimmerArgs> (.NET 8 workaround).

Copy link
Collaborator Author

@guardrex guardrex Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and @danroth27 ... are we holding this PR past the RC1 release if @javiercn doesn't respond by then?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the same as (or at least has the elements of) dotnet/aspnetcore#52947? It's just not KeyValuePair ... it's a Tuple ... but same situation/error. I thought @javiercn was well aware of this, and I placed the content that he requested to cover it.

Right, that's why I thought @javiercn might want to review this content.

are we holding this PR past the RC1 release if @javiercn doesn't respond by then?

Nope, no need to block on this. The content looks fine to me.

Copy link
Collaborator Author

@guardrex guardrex Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just asked because you were calling for his review. I'll react with a patch PR later if he has any changes.

... and I thought Javier would just re-open the existing PU issue on that subject, but I'm still 👂 if you/he want a new issue on ConstructorContainsNullParameterNames.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We did KeyValuePair because people use that.

If get reports for other types, we might choose to add those too. The cost of preserving a type is small compared to the benefit of not having to force people into doing this type of stuff if enough people use the type.


The IL Trimmer is also unable to react to an app's dynamic behavior at runtime. To ensure the trimmed app works correctly once deployed, test published output frequently while developing.

Consider the following client-side component in a Blazor Web App (.NET 8 or later) that deserializes a <xref:System.Collections.Generic.KeyValuePair> collection (`List<KeyValuePair<string, string>>`):
Consider the following example that performs JSON deserialization into a <xref:System.Tuple%602> collection (`List<Tuple<string, string>>`).

`TrimExample.razor`:

```razor
@rendermode @(new InteractiveWebAssemblyRenderMode(false))
@page "/trim-example"
@using System.Diagnostics.CodeAnalysis
@using System.Text.Json

<dl>
<h1>Trim Example</h1>

<ul>
@foreach (var item in @items)
{
<dt>@item.Key</dt>
<dd>@item.Value</dd>
<li>@item.Item1, @item.Item2</li>
}
</dl>
</ul>

@code {
private List<KeyValuePair<string, string>> items = [];
private List<Tuple<string, string>> items = [];

[StringSyntax(StringSyntaxAttribute.Json)]
private const string data =
"""[{"key":"key 1","value":"value 1"},{"key":"key 2","value":"value 2"}]""";
"""[{"item1":"1:T1","item2":"1:T2"},{"item1":"2:T1","item2":"2:T2"}]""";

protected override void OnInitialized()
{
JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true };

items = JsonSerializer
.Deserialize<List<KeyValuePair<string, string>>>(data, options)!;
.Deserialize<List<Tuple<string, string>>>(data, options)!;
}
}
```

The preceding component executes normally when the app is run locally and produces the following rendered definition list (`<dl>`):
The preceding component executes normally when the app is run locally and produces the following rendered list:

> • 1:T1, 1:T2
> • 2:T2, 2:T2

When the app is published, <xref:System.Tuple%602> is trimmed from the app, even in spite of setting the `<PublishTrimmed>` property to `false` in the project file. Accessing the component throws the following exception:

> :::no-loc text="crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]":::
> :::no-loc text="Unhandled exception rendering component: ConstructorContainsNullParameterNames, System.Tuple`2[System.String,System.String]":::
> :::no-loc text="System.NotSupportedException: ConstructorContainsNullParameterNames, System.Tuple`2[System.String,System.String]":::

To address lost types, consider adopting one of the following approaches.

### Custom types

Custom types aren't trimmed by Blazor when an app is published, so we recommend using custom types for JS interop, JSON serialization/deserialization, and other operations that rely on reflection.

The following modifications create a `StringTuple` type for use by the component.

`StringTuple.cs`:

```csharp
[method: SetsRequiredMembers]
public sealed class StringTuple(string item1, string item2)
{
public required string Item1 { get; init; } = item1;
public required string Item2 { get; init; } = item2;
}
```

The component is modified to use the `StringTuple` type:

```diff
- private List<Tuple<string, string>> items = [];
+ private List<StringTuple> items = [];
```

```diff
- items = JsonSerializer.Deserialize<List<Tuple<string, string>>>(data, options)!;
+ items = JsonSerializer.Deserialize<List<StringTuple>>(data, options)!;
```

Because custom types are never trimmed by Blazor when an app is published, the component works as designed after the app is published.

:::moniker range=">= aspnetcore-10.0"

> **:::no-loc text="key 1":::**
> :::no-loc text="value 1":::
> **:::no-loc text="key 2":::**
> :::no-loc text="value 2":::
If you prefer to use framework types in spite of our recommendation, use either of the following approaches:

When the app is published, <xref:System.Collections.Generic.KeyValuePair> is trimmed from the app, even in spite of setting the [`<PublishTrimmed>` property](#configuration) to `false` in the project file. Accessing the component throws the following exception:
* [Preserve the type as a dynamic dependency](#preserve-the-type-as-a-dynamic-dependency)
* [Use a Root Descriptor](#use-a-root-descriptor)

> :::no-loc text="Unhandled exception rendering component: ConstructorContainsNullParameterNames, System.Collections.Generic.KeyValuePair`2[System.String,System.String]":::
:::moniker-end

To address lost types, consider the following approaches.
:::moniker range="< aspnetcore-10.0"

If you prefer to use framework types in spite of our recommendation, [preserve the type as a dynamic dependency](#preserve-the-type-as-a-dynamic-dependency).

:::moniker-end

### Preserve the type as a dynamic dependency

We recommend creating a dynamic dependency to preserve the type with the [`[DynamicDependency]` attribute](xref:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute).
Create a dynamic dependency to preserve the type with the [`[DynamicDependency]` attribute](xref:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute).

If not already present, add an `@using` directive for <xref:System.Diagnostics.CodeAnalysis?displayProperty=fullName>:

```razor
@using System.Diagnostics.CodeAnalysis
```

Add a [`[DynamicDependency]` attribute](xref:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute) to preserve the <xref:System.Collections.Generic.KeyValuePair>:
Add a [`[DynamicDependency]` attribute](xref:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute) to preserve the <xref:System.Tuple%602>:

```diff
+ [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(KeyValuePair<string, string>))]
private List<KeyValuePair<string, string>> items = [];
+ [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors,
+ typeof(Tuple<string, string>))]
private List<Tuple<string, string>> items = [];
```

<!-- UPDATE 10.0 - Hold this for https://github.com/dotnet/aspnetcore/issues/52947
:::moniker range=">= aspnetcore-10.0"

### Use a Root Descriptor

Expand All @@ -118,10 +168,8 @@ Add an `ILLink.Descriptors.xml` file to the root of the app&dagger; with the typ

```xml
<linker>
<assembly fullname="System.Runtime">
<type fullname="System.Collections.Generic.KeyValuePair`2">
<method signature="System.Void .ctor(TKey,TValue)" />
</type>
<assembly fullname="System.Private.CoreLib">
<type fullname="System.Tuple`2" preserve="all" />
</assembly>
</linker>
```
Expand All @@ -138,42 +186,21 @@ Add a `TrimmerRootDescriptor` item to the app's project file&Dagger; referencing

&Dagger;The project file is either the project file of the Blazor WebAssembly app or the project file of the `.Client` project of a Blazor Web App (.NET 8 or later).

-->

### Custom types

<!-- UPDATE 10.0 - We'll hold this for when the file descriptor approach comes back.

Custom types aren't trimmed by Blazor when an app is published, but we recommend [preserving types as dynamic dependencies](#preserve-the-type-as-a-dynamic-dependency) instead of creating custom types.

-->

The following modifications create a `StringKeyValuePair` type for use by the component.

`StringKeyValuePair.cs`:
:::moniker-end

```csharp
[method: SetsRequiredMembers]
public sealed class StringKeyValuePair(string key, string value)
{
public required string Key { get; init; } = key;
public required string Value { get; init; } = value;
}
```
:::moniker range="= aspnetcore-8.0"

The component is modified to use the `StringKeyValuePair` type:
### Workaround in .NET 8

```diff
- private List<KeyValuePair<string, string>> items = [];
+ private List<StringKeyValuePair> items = [];
```
As a workaround in .NET 8, you can add the `_ExtraTrimmerArgs` MSBuild property set to `--keep-metadata parametername` in the app's project file to preserve parameter names during trimming:

```diff
- items = JsonSerializer.Deserialize<List<KeyValuePair<string, string>>>(data, options)!;
+ items = JsonSerializer.Deserialize<List<StringKeyValuePair>>(data, options)!;
```xml
<PropertyGroup>
<_ExtraTrimmerArgs>--keep-metadata parametername</_ExtraTrimmerArgs>
</PropertyGroup>
```

Because custom types are never trimmed by Blazor when an app is published, the component works as designed after the app is published.
:::moniker-end

## Additional resources

Expand Down
Loading