diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index c9c1db6ab3a9..fe5447ce5578 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -41,288 +41,288 @@
https://github.com/dotnet/efcore
8f52c86d7a31810711e3fa4c56a970bc10a1b026
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
https://github.com/dotnet/source-build-externals
1b64d3c0fad8af67da8f42927ce7306730224c15
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
https://github.com/dotnet/xdt
@@ -352,16 +352,16 @@
https://github.com/dotnet/roslyn
1aa759af23d2a29043ea44fcef5bd6823dafa5d0
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
-
+
https://github.com/dotnet/runtime
- 1222f14d02818cfab597fa2a17c8664fe6cb39be
+ da3500bb02343b1d0424c74ccdddbc592b5b3f4f
https://github.com/dotnet/winforms
@@ -388,9 +388,9 @@
https://github.com/dotnet/arcade
4665b3d04e1da3796b965c3c3e3b97f55c449a6e
-
+
https://github.com/dotnet/extensions
- 3a758c142bdf9ad3ace1361b8bde9404e6cced57
+ 7dc81f36785709a147b10b9f0a23f152a2582608
https://github.com/nuget/nuget.client
diff --git a/eng/Versions.props b/eng/Versions.props
index c83a9a8fe09d..39b53f6ef2be 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -64,80 +64,80 @@
-->
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
9.0.0-alpha.1.23454.1
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
- 8.0.0-rc.2.23456.8
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
+ 8.0.0-rc.2.23457.7
- 9.0.0-alpha.1.23453.3
+ 8.0.0-rc.2.23457.7
9.0.0-alpha.1.23421.9
9.0.0-alpha.1.23421.9
@@ -168,7 +168,7 @@
2.1.0-beta.23409.1
- 8.0.0-rc.2.23456.8
+ 8.0.0-rc.2.23457.7
7.0.0-preview.22423.2
diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
index 833a41540b37..2d13b0ccb7fc 100644
--- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
+++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
@@ -16,6 +16,7 @@
+
diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs
index 4adad2caf3e6..934a41613096 100644
--- a/src/Components/Components/src/Routing/Router.cs
+++ b/src/Components/Components/src/Routing/Router.cs
@@ -92,6 +92,7 @@ static readonly IReadOnlyDictionary _emptyParametersDictionary
/// over wildcards.
/// This property is obsolete and configuring it does nothing.
///
+ [Obsolete("This property is obsolete and configuring it has not effect.")]
[Parameter] public bool PreferExactMatches { get; set; }
private RouteTable Routes { get; set; }
diff --git a/src/Components/Endpoints/src/DependencyInjection/DefaultRazorComponentsServiceOptionsConfiguration.cs b/src/Components/Endpoints/src/DependencyInjection/DefaultRazorComponentsServiceOptionsConfiguration.cs
new file mode 100644
index 000000000000..6161cf770c7c
--- /dev/null
+++ b/src/Components/Endpoints/src/DependencyInjection/DefaultRazorComponentsServiceOptionsConfiguration.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Components.Endpoints;
+using Microsoft.AspNetCore.Components.Endpoints.FormMapping;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+internal class DefaultRazorComponentsServiceOptionsConfiguration(IConfiguration configuration, ILoggerFactory loggerFactory)
+ : IPostConfigureOptions
+{
+ public IConfiguration Configuration { get; } = configuration;
+
+ public void PostConfigure(string? name, RazorComponentsServiceOptions options)
+ {
+ var value = Configuration[WebHostDefaults.DetailedErrorsKey];
+ options.DetailedErrors = string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(value, "1", StringComparison.OrdinalIgnoreCase);
+
+ options._formMappingOptions = new FormDataMapperOptions(loggerFactory)
+ {
+ MaxRecursionDepth = options.MaxFormMappingRecursionDepth,
+ MaxErrorCount = options.MaxFormMappingErrorCount,
+ MaxCollectionSize = options.MaxFormMappingCollectionSize,
+ MaxKeyBufferSize = options.MaxFormMappingKeySize
+ };
+ }
+}
diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsEndpointOptions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsEndpointOptions.cs
deleted file mode 100644
index f2d00f2bf498..000000000000
--- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsEndpointOptions.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.AspNetCore.Components.Endpoints;
-
-internal class RazorComponentsEndpointOptions
-{
- public bool DetailedErrors { get; set; }
-}
diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsEndpointsDetailedErrorsConfiguration.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsEndpointsDetailedErrorsConfiguration.cs
deleted file mode 100644
index 63a10ad6c9a6..000000000000
--- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsEndpointsDetailedErrorsConfiguration.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Microsoft.AspNetCore.Components.Endpoints;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Options;
-
-namespace Microsoft.Extensions.DependencyInjection;
-
-internal class RazorComponentsEndpointsDetailedErrorsConfiguration : IConfigureOptions
-{
- public RazorComponentsEndpointsDetailedErrorsConfiguration(IConfiguration configuration)
- {
- Configuration = configuration;
- }
-
- public IConfiguration Configuration { get; }
-
- public void Configure(RazorComponentsEndpointOptions options)
- {
- var value = Configuration[WebHostDefaults.DetailedErrorsKey];
- options.DetailedErrors = string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(value, "1", StringComparison.OrdinalIgnoreCase);
- }
-}
diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs
index d361c17687c4..f695735195d2 100644
--- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs
+++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs
@@ -28,14 +28,15 @@ public static class RazorComponentsServiceCollectionExtensions
/// Registers services required for server-side rendering of Razor Components.
///
/// The service collection.
- /// An to configure the provided .
+ /// An to configure the provided .
/// An that can be used to further configure the Razor component services.
[RequiresUnreferencedCode("Razor Components does not currently support trimming or native AOT.", Url = "https://aka.ms/aspnet/nativeaot")]
- public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services, Action? configure = null)
+ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services, Action? configure = null)
{
ArgumentNullException.ThrowIfNull(services);
// Dependencies
+ services.AddLogging();
services.AddAntiforgery();
services.TryAddSingleton();
@@ -61,7 +62,8 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
services.TryAddScoped();
services.TryAddScoped(sp => sp.GetRequiredService().State);
services.TryAddScoped();
- services.TryAddEnumerable(ServiceDescriptor.Singleton, RazorComponentsEndpointsDetailedErrorsConfiguration>());
+ services.TryAddEnumerable(
+ ServiceDescriptor.Singleton, DefaultRazorComponentsServiceOptionsConfiguration>());
services.TryAddScoped();
services.TryAddScoped(sp => sp.GetRequiredService());
services.AddSupplyValueFromQueryProvider();
diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentOptions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceOptions.cs
similarity index 85%
rename from src/Components/Endpoints/src/DependencyInjection/RazorComponentOptions.cs
rename to src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceOptions.cs
index 7d79b4b9b802..7d0fda142c1d 100644
--- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentOptions.cs
+++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceOptions.cs
@@ -8,9 +8,14 @@ namespace Microsoft.AspNetCore.Components.Endpoints;
///
/// Provides options for configuring server-side rendering of Razor Components.
///
-public sealed class RazorComponentsOptions
+public sealed class RazorComponentsServiceOptions
{
- internal readonly FormDataMapperOptions _formMappingOptions = new();
+ internal FormDataMapperOptions _formMappingOptions = new();
+
+ ///
+ /// Gets or sets a value that determines whether to include detailed information on errors.
+ ///
+ public bool DetailedErrors { get; set; }
///
/// Gets or sets the maximum number of elements allowed in a form collection.
diff --git a/src/Components/Endpoints/src/FormMapping/Converters/CollectionConverter.cs b/src/Components/Endpoints/src/FormMapping/Converters/CollectionConverter.cs
index 6b5852833a97..18ef85a46c80 100644
--- a/src/Components/Endpoints/src/FormMapping/Converters/CollectionConverter.cs
+++ b/src/Components/Endpoints/src/FormMapping/Converters/CollectionConverter.cs
@@ -58,7 +58,7 @@ internal override bool TryRead(
out bool found)
{
TElement currentElement;
- TBuffer buffer;
+ TBuffer? buffer = default;
bool foundCurrentElement;
bool currentElementSuccess;
bool succeded;
@@ -73,10 +73,10 @@ internal override bool TryRead(
{
context.PopPrefix("[0]");
}
+
if (!found)
{
- result = default;
- return succeded;
+ return TryReadSingleValueCollection(ref context, out result, ref found, ref buffer, ref succeded);
}
// We already know we found an element;
@@ -92,6 +92,15 @@ internal override bool TryRead(
currentElementSuccess = _elementConverter.TryRead(ref context, _elementType, options, out currentElement!, out foundCurrentElement);
succeded = succeded && currentElementSuccess;
}
+ catch
+ {
+ if (buffer != null)
+ {
+ // Ensure the buffer is cleaned up if we fail.
+ result = TCollectionPolicy.ToResult(buffer);
+ }
+ throw;
+ }
finally
{
context.PopPrefix("[1]");
@@ -120,6 +129,15 @@ internal override bool TryRead(
currentElementSuccess = _elementConverter.TryRead(ref context, _elementType, options, out currentElement!, out foundCurrentElement);
succeded = succeded && currentElementSuccess;
}
+ catch
+ {
+ if (buffer != null)
+ {
+ // Ensure the buffer is cleaned up if we fail.
+ result = TCollectionPolicy.ToResult(buffer);
+ }
+ throw;
+ }
finally
{
context.PopPrefix(prefix);
@@ -163,6 +181,15 @@ internal override bool TryRead(
currentElementSuccess = _elementConverter.TryRead(ref context, _elementType, options, out currentElement!, out foundCurrentElement);
succeded = succeded && currentElementSuccess;
}
+ catch
+ {
+ if (buffer != null)
+ {
+ // Ensure the buffer is cleaned up if we fail.
+ result = TCollectionPolicy.ToResult(buffer);
+ }
+ throw;
+ }
finally
{
context.PopPrefix(computedPrefix[..(charsWritten + 2)]);
@@ -189,4 +216,44 @@ internal override bool TryRead(
return succeded;
}
}
+
+ private bool TryReadSingleValueCollection(ref FormDataReader context, out TCollection? result, ref bool found, ref TBuffer? buffer, ref bool succeded)
+ {
+ if (_elementConverter is ISingleValueConverter singleValueConverter &&
+ singleValueConverter.CanConvertSingleValue() &&
+ context.TryGetValues(out var values))
+ {
+ found = true;
+ buffer = TCollectionPolicy.CreateBuffer();
+
+ for (var i = 0; i < values.Count; i++)
+ {
+ var value = values[i];
+ try
+ {
+ if (!singleValueConverter.TryConvertValue(ref context, value!, out var elementValue))
+ {
+ succeded = false;
+ }
+ else
+ {
+ buffer = TCollectionPolicy.Add(ref buffer, elementValue);
+ }
+ }
+ catch (Exception ex)
+ {
+ succeded = false;
+ context.AddMappingError(ex, value);
+ }
+ };
+
+ result = TCollectionPolicy.ToResult(buffer);
+ }
+ else
+ {
+ result = default;
+ }
+
+ return succeded;
+ }
}
diff --git a/src/Components/Endpoints/src/FormMapping/Converters/EnumConverter.cs b/src/Components/Endpoints/src/FormMapping/Converters/EnumConverter.cs
index aae1a63995ba..aa7991671bce 100644
--- a/src/Components/Endpoints/src/FormMapping/Converters/EnumConverter.cs
+++ b/src/Components/Endpoints/src/FormMapping/Converters/EnumConverter.cs
@@ -6,18 +6,12 @@
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
-internal class EnumConverter : FormDataConverter where TEnum : struct, Enum
+internal class EnumConverter : FormDataConverter, ISingleValueConverter where TEnum : struct, Enum
{
- [RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
- [RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
- internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMapperOptions options, out TEnum result, out bool found)
+ public bool CanConvertSingleValue() => true;
+
+ public bool TryConvertValue(ref FormDataReader reader, string value, out TEnum result)
{
- found = reader.TryGetValue(out var value);
- if (!found)
- {
- result = default;
- return true;
- }
if (Enum.TryParse(value, ignoreCase: true, out result))
{
return true;
@@ -30,4 +24,18 @@ internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMap
return false;
}
}
+
+ [RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
+ [RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
+ internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMapperOptions options, out TEnum result, out bool found)
+ {
+ found = reader.TryGetValue(out var value);
+ if (!found)
+ {
+ result = default;
+ return true;
+ }
+
+ return TryConvertValue(ref reader, value!, out result);
+ }
}
diff --git a/src/Components/Endpoints/src/FormMapping/Converters/NullableConverter.cs b/src/Components/Endpoints/src/FormMapping/Converters/NullableConverter.cs
index d588754f7767..58bd47c60396 100644
--- a/src/Components/Endpoints/src/FormMapping/Converters/NullableConverter.cs
+++ b/src/Components/Endpoints/src/FormMapping/Converters/NullableConverter.cs
@@ -5,13 +5,27 @@
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
-internal sealed class NullableConverter : FormDataConverter where T : struct
+internal sealed class NullableConverter(FormDataConverter nonNullableConverter) : FormDataConverter, ISingleValueConverter where T : struct
{
- private readonly FormDataConverter _nonNullableConverter;
+ private readonly FormDataConverter _nonNullableConverter = nonNullableConverter;
- public NullableConverter(FormDataConverter nonNullableConverter)
+ public bool CanConvertSingleValue() => _nonNullableConverter is ISingleValueConverter singleValueConverter &&
+ singleValueConverter.CanConvertSingleValue();
+
+ public bool TryConvertValue(ref FormDataReader reader, string value, out T? result)
{
- _nonNullableConverter = nonNullableConverter;
+ var converter = (ISingleValueConverter)_nonNullableConverter;
+
+ if (converter.TryConvertValue(ref reader, value, out var converted))
+ {
+ result = converted;
+ return true;
+ }
+ else
+ {
+ result = null;
+ return false;
+ }
}
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
diff --git a/src/Components/Endpoints/src/FormMapping/Converters/ParsableConverter.cs b/src/Components/Endpoints/src/FormMapping/Converters/ParsableConverter.cs
index 7eb1d0cf857d..3e7cd7adc083 100644
--- a/src/Components/Endpoints/src/FormMapping/Converters/ParsableConverter.cs
+++ b/src/Components/Endpoints/src/FormMapping/Converters/ParsableConverter.cs
@@ -6,8 +6,25 @@
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
-internal sealed class ParsableConverter : FormDataConverter, ISingleValueConverter where T : IParsable
+internal sealed class ParsableConverter : FormDataConverter, ISingleValueConverter where T : IParsable
{
+ public bool CanConvertSingleValue() => true;
+
+ public bool TryConvertValue(ref FormDataReader reader, string value, out T result)
+ {
+ if (T.TryParse(value, reader.Culture, out result!))
+ {
+ return true;
+ }
+ else
+ {
+ var segment = reader.GetLastPrefixSegment();
+ reader.AddMappingError(FormattableStringFactory.Create(FormDataResources.ParsableMappingError, value, segment), value);
+ result = default!;
+ return false;
+ }
+ }
+
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMapperOptions options, out T? result, out bool found)
@@ -18,17 +35,9 @@ internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMap
result = default;
return true;
}
-
- if (T.TryParse(value, reader.Culture, out result))
- {
- return true;
- }
else
{
- var segment = reader.GetLastPrefixSegment();
- reader.AddMappingError(FormattableStringFactory.Create(FormDataResources.ParsableMappingError, value, segment), value);
- result = default;
- return false;
+ return TryConvertValue(ref reader, value!, out result!);
}
}
}
diff --git a/src/Components/Endpoints/src/FormMapping/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs b/src/Components/Endpoints/src/FormMapping/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs
index 70e419a17aba..96146387c11c 100644
--- a/src/Components/Endpoints/src/FormMapping/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs
+++ b/src/Components/Endpoints/src/FormMapping/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs
@@ -23,7 +23,9 @@ internal override CompiledComplexTypeConverter CreateConverter(Type type, For
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
private CompiledComplexTypeConverter.ConverterDelegate CreateConverterBody(Type type, FormDataMapperOptions options)
{
- var metadata = factory.GetOrCreateMetadataFor(type, options);
+ var metadata = factory.GetOrCreateMetadataFor(type, options) ??
+ throw new InvalidOperationException($"Could not resolve metadata for type '{type.FullName}'.");
+
var properties = metadata.Properties;
var constructorParameters = metadata.ConstructorParameters;
diff --git a/src/Components/Endpoints/src/FormMapping/Factories/ComplexTypeConverterFactory.cs b/src/Components/Endpoints/src/FormMapping/Factories/ComplexTypeConverterFactory.cs
index e90061d2d2d4..09df61c18329 100644
--- a/src/Components/Endpoints/src/FormMapping/Factories/ComplexTypeConverterFactory.cs
+++ b/src/Components/Endpoints/src/FormMapping/Factories/ComplexTypeConverterFactory.cs
@@ -3,52 +3,26 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components.Endpoints.FormMapping.Metadata;
-using Microsoft.Extensions.Internal;
+using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
// This factory is registered last, which means, dictionaries and collections, have already
// been processed by the time we get here.
-internal class ComplexTypeConverterFactory(FormDataMapperOptions options) : IFormDataConverterFactory
+internal class ComplexTypeConverterFactory(FormDataMapperOptions options, ILoggerFactory loggerFactory) : IFormDataConverterFactory
{
- internal FormDataMetadataFactory MetadataFactory { get; } = new FormDataMetadataFactory(options.Factories);
+ internal FormDataMetadataFactory MetadataFactory { get; } = new FormDataMetadataFactory(options.Factories, loggerFactory);
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
public bool CanConvert(Type type, FormDataMapperOptions options)
{
- if (type.IsGenericTypeDefinition)
- {
- return false;
- }
-
- var constructors = type.GetConstructors();
- if (constructors.Length > 1 || (constructors.Length == 0 && !type.IsValueType))
- {
- // We can't select the constructor when there are multiple of them.
- return false;
- }
-
- if (MetadataFactory.HasMetadataFor(type))
- {
- return true;
- }
-
// Create the metadata for the type. This walks the graph and creates metadata for all the types
// in the reference graph, detecting and identifying recursive types.
- MetadataFactory.GetOrCreateMetadataFor(type, options);
-
- // Check that all properties have a valid converter.
- var propertyHelper = PropertyHelper.GetVisibleProperties(type);
- foreach (var helper in propertyHelper)
- {
- if (!options.CanConvert(helper.Property.PropertyType))
- {
- return false;
- }
- }
+ var metadata = MetadataFactory.GetOrCreateMetadataFor(type, options);
- return true;
+ // If we can create metadata for the type, then we can convert it.
+ return metadata != null;
}
// We are going to compile a function that maps all the properties for the type.
diff --git a/src/Components/Endpoints/src/FormMapping/FormDataMapper.cs b/src/Components/Endpoints/src/FormMapping/FormDataMapper.cs
index 578f1a53de80..00873d0f9baf 100644
--- a/src/Components/Endpoints/src/FormMapping/FormDataMapper.cs
+++ b/src/Components/Endpoints/src/FormMapping/FormDataMapper.cs
@@ -2,10 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
-internal static class FormDataMapper
+internal static partial class FormDataMapper
{
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
@@ -13,20 +14,35 @@ internal static class FormDataMapper
FormDataReader reader,
FormDataMapperOptions options)
{
+ FormDataConverter? converter;
try
{
- var converter = options.ResolveConverter();
- if (converter.TryRead(ref reader, typeof(T), options, out var result, out _))
+ converter = options.ResolveConverter();
+ if (converter == null)
{
- return result;
+ Log.CannotResolveConverter(options.Logger, typeof(T), null);
+ return default;
}
-
- // Always return the result, even if it has failures. This is because we do not want
- // to loose the data that we were able to deserialize.
- return result;
}
- finally
+ catch (Exception ex)
+ {
+ Log.CannotResolveConverter(options.Logger, typeof(T), ex);
+ return default;
+ }
+
+ if (converter.TryRead(ref reader, typeof(T), options, out var result, out _))
{
+ return result;
}
+
+ // Always return the result, even if it has failures. This is because we do not want
+ // to loose the data that we were able to deserialize.
+ return result;
+ }
+
+ private static partial class Log
+ {
+ [LoggerMessage(1, LogLevel.Warning, "Cannot resolve converter for type '{Type}'.", EventName = "CannotResolveConverter")]
+ public static partial void CannotResolveConverter(ILogger logger, Type type, Exception? ex);
}
}
diff --git a/src/Components/Endpoints/src/FormMapping/FormDataMapperOptions.cs b/src/Components/Endpoints/src/FormMapping/FormDataMapperOptions.cs
index 26dca21b8159..25cbc47cf2f0 100644
--- a/src/Components/Endpoints/src/FormMapping/FormDataMapperOptions.cs
+++ b/src/Components/Endpoints/src/FormMapping/FormDataMapperOptions.cs
@@ -4,6 +4,8 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
@@ -14,7 +16,13 @@ internal sealed class FormDataMapperOptions
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
- public FormDataMapperOptions()
+ public FormDataMapperOptions() : this(NullLoggerFactory.Instance)
+ {
+ }
+
+ [RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
+ [RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
+ public FormDataMapperOptions(ILoggerFactory loggerFactory)
{
_converters = new(WellKnownConverters.Converters);
_factories.Add(new ParsableConverterFactory());
@@ -22,40 +30,26 @@ public FormDataMapperOptions()
_factories.Add(new NullableConverterFactory());
_factories.Add(new DictionaryConverterFactory());
_factories.Add(new CollectionConverterFactory());
- _factories.Add(new ComplexTypeConverterFactory(this));
+ _factories.Add(new ComplexTypeConverterFactory(this, loggerFactory));
+ Logger = loggerFactory.CreateLogger();
}
- // Not configurable for now, this is the max number of elements we will bind. This is important for
- // security reasons, as we don't want to bind a huge collection and cause perf issues.
- // Some examples of this are:
- // Binding to collection using hashes, where the payload can be crafted to force the worst case on insertion
- // which is O(n).
- internal int MaxCollectionSize = FormReader.DefaultValueCountLimit;
-
- // MVC uses 32, JSON uses 64. Let's stick to STJ default.
- internal int MaxRecursionDepth = 64;
-
- // This is normally 200 (similar to ModelStateDictionary.DefaultMaxAllowedErrors in MVC)
- internal int MaxErrorCount = 200;
+ // For testing purposes only.
+ internal List Factories => _factories;
- internal int MaxKeyBufferSize = FormReader.DefaultKeyLengthLimit;
+ internal ILogger Logger { get; }
- internal bool UseCurrentCulture;
+ internal int MaxCollectionSize { get; set; } = FormReader.DefaultValueCountLimit;
- // For testing purposes only.
- internal List Factories => _factories;
+ internal int MaxRecursionDepth { get; set; } = 64;
- internal bool HasConverter(Type valueType) => _converters.ContainsKey(valueType);
+ internal int MaxErrorCount { get; set; } = 200;
- internal bool IsSingleValueConverter(Type type)
- {
- return _converters.TryGetValue(type, out var converter) &&
- converter is ISingleValueConverter;
- }
+ internal int MaxKeyBufferSize { get; set; } = FormReader.DefaultKeyLengthLimit;
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
- internal FormDataConverter ResolveConverter()
+ internal FormDataConverter? ResolveConverter()
{
return (FormDataConverter)_converters.GetOrAdd(typeof(T), CreateConverter, this);
}
diff --git a/src/Components/Endpoints/src/FormMapping/FormDataReader.cs b/src/Components/Endpoints/src/FormMapping/FormDataReader.cs
index 78c94bdab641..1e062dd6bdba 100644
--- a/src/Components/Endpoints/src/FormMapping/FormDataReader.cs
+++ b/src/Components/Endpoints/src/FormMapping/FormDataReader.cs
@@ -10,13 +10,14 @@
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
+[DebuggerDisplay($"{{{nameof(DebuggerDisplay)}(),nq}}")]
internal struct FormDataReader : IDisposable
{
private readonly IReadOnlyDictionary _readOnlyMemoryKeys;
private readonly Memory _prefixBuffer;
private Memory _currentPrefixBuffer;
- private int _currentDepth = 0;
- private int _errorCount = 0;
+ private int _currentDepth;
+ private int _errorCount;
// As an implementation detail, reuse FormKey for the values.
// It's just a thin wrapper over ReadOnlyMemory that caches
@@ -41,7 +42,8 @@ public FormDataReader(IReadOnlyDictionary formCollection,
public Action? ErrorHandler { get; set; }
public Action? AttachInstanceToErrorsHandler { get; set; }
- public int MaxErrorCount { get; set; } = 100;
+
+ public int MaxErrorCount { get; set; } = 200;
public void AddMappingError(FormattableString errorMessage, string? attemptedValue)
{
@@ -245,6 +247,9 @@ internal readonly bool TryGetValue([NotNullWhen(true)] out string? value)
return foundSingleValue;
}
+ internal readonly bool TryGetValues(out StringValues values) =>
+ _readOnlyMemoryKeys.TryGetValue(new FormKey(_currentPrefixBuffer), out values);
+
internal string GetPrefix() => _currentPrefixBuffer.ToString();
internal string GetLastPrefixSegment()
@@ -305,4 +310,7 @@ public Enumerator(HashSet.Enumerator enumerator)
void IEnumerator.Reset() { }
}
}
+
+ private readonly string DebuggerDisplay =>
+ $"Key count = {_readOnlyMemoryKeys.Count}, Prefix = {_currentPrefixBuffer}, Error count = {_errorCount}, Current depth = {_currentDepth}";
}
diff --git a/src/Components/Endpoints/src/FormMapping/HttpContextFormValueMapper.cs b/src/Components/Endpoints/src/FormMapping/HttpContextFormValueMapper.cs
index 083d492183c5..cac78e732636 100644
--- a/src/Components/Endpoints/src/FormMapping/HttpContextFormValueMapper.cs
+++ b/src/Components/Endpoints/src/FormMapping/HttpContextFormValueMapper.cs
@@ -21,7 +21,7 @@ internal sealed class HttpContextFormValueMapper : IFormValueMapper
public HttpContextFormValueMapper(
HttpContextFormDataProvider formData,
- IOptions options)
+ IOptions options)
{
_formData = formData;
_options = options.Value._formMappingOptions;
@@ -128,7 +128,7 @@ public override void Deserialize(
using var reader = new FormDataReader(
dictionary,
- options.UseCurrentCulture ? CultureInfo.CurrentCulture : CultureInfo.InvariantCulture,
+ CultureInfo.InvariantCulture,
buffer.AsMemory(0, options.MaxKeyBufferSize))
{
ErrorHandler = context.OnError,
diff --git a/src/Components/Endpoints/src/FormMapping/ISingleValueConverter.cs b/src/Components/Endpoints/src/FormMapping/ISingleValueConverter.cs
index b8b28500b113..56f240f9708a 100644
--- a/src/Components/Endpoints/src/FormMapping/ISingleValueConverter.cs
+++ b/src/Components/Endpoints/src/FormMapping/ISingleValueConverter.cs
@@ -1,8 +1,11 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
-internal interface ISingleValueConverter
+internal interface ISingleValueConverter
{
+ bool CanConvertSingleValue();
+
+ bool TryConvertValue(ref FormDataReader reader, string value, out T result);
}
diff --git a/src/Components/Endpoints/src/FormMapping/Metadata/FormDataMetadataFactory.cs b/src/Components/Endpoints/src/FormMapping/Metadata/FormDataMetadataFactory.cs
index 7439fccce728..bba5501e347d 100644
--- a/src/Components/Endpoints/src/FormMapping/Metadata/FormDataMetadataFactory.cs
+++ b/src/Components/Endpoints/src/FormMapping/Metadata/FormDataMetadataFactory.cs
@@ -7,162 +7,246 @@
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using Microsoft.Extensions.Internal;
+using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping.Metadata;
-internal class FormDataMetadataFactory(List factories)
+internal partial class FormDataMetadataFactory(List factories, ILoggerFactory loggerFactory)
{
+ private readonly object _lock = new object();
private readonly FormMetadataContext _context = new();
private readonly ParsableConverterFactory _parsableFactory = factories.OfType().Single();
private readonly DictionaryConverterFactory _dictionaryFactory = factories.OfType().Single();
private readonly CollectionConverterFactory _collectionFactory = factories.OfType().Single();
+ private readonly ILogger _logger = loggerFactory.CreateLogger();
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
- public FormDataTypeMetadata GetOrCreateMetadataFor(Type type, FormDataMapperOptions options)
+ public FormDataTypeMetadata? GetOrCreateMetadataFor(Type type, FormDataMapperOptions options)
{
var shouldClearContext = !_context.ResolutionInProgress;
- try
+ lock (_lock)
{
- // We are walking the graph in order to detect recursive types.
- // We evaluate whether a type is:
- // 1. Primitive
- // 2. Dictionary
- // 3. Collection
- // 4. Complex
- // Only complex types can be recursive.
- // We only compute metadata when we are dealing with objects, other classes of
- // types are handled directly by the appropriate converters.
- // We keep track of the metadata for the types because it is useful when we generate
- // the specific object converter for a type.
- // The code generation varies depending on whether there is recursion or not within
- // the type graph.
- if (shouldClearContext)
+ try
{
- _context.BeginResolveGraph();
- }
-
- // Try to get the metadata for the type or create and add a new instance.
- var result = _context.TypeMetadata.TryGetValue(type, out var value) ? value : new FormDataTypeMetadata(type);
- if (value == null)
- {
- _context.TypeMetadata[type] = result;
- }
-
- // Check for cycles and mark any type as recursive if needed.
- DetectCyclesAndMarkMetadataTypesAsRecursive(type, result);
-
- // We found the value on the existing metadata, we can return it.
- if (value != null)
- {
- return result;
- }
+ // We are walking the graph in order to detect recursive types.
+ // We evaluate whether a type is:
+ // 1. Primitive
+ // 2. Dictionary
+ // 3. Collection
+ // 4. Complex
+ // Only complex types can be recursive.
+ // We only compute metadata when we are dealing with objects, other classes of
+ // types are handled directly by the appropriate converters.
+ // We keep track of the metadata for the types because it is useful when we generate
+ // the specific object converter for a type.
+ // The code generation varies depending on whether there is recursion or not within
+ // the type graph.
+ if (shouldClearContext)
+ {
+ Log.StartResolveMetadataGraph(_logger, type);
+ _context.BeginResolveGraph();
+ }
- // These blocks are evaluated in a specific order.
- if (_parsableFactory.CanConvert(type, options) || type.IsEnum ||
- (Nullable.GetUnderlyingType(type) is { } underlyingType &&
- _parsableFactory.CanConvert(underlyingType, options)))
- {
- result.Kind = FormDataTypeKind.Primitive;
- return result;
- }
+ // Try to get the metadata for the type or create and add a new instance.
+ var result = _context.TypeMetadata.TryGetValue(type, out var value) ? value : new FormDataTypeMetadata(type);
+ if (value == null)
+ {
+ Log.NoMetadataFound(_logger, type);
+ _context.TypeMetadata[type] = result;
+ }
+ else
+ {
+ Log.MetadataFound(_logger, type);
+ }
- if (_dictionaryFactory.CanConvert(type, options))
- {
- result.Kind = FormDataTypeKind.Dictionary;
- var (keyType, valueType) = DictionaryConverterFactory.ResolveDictionaryTypes(type)!;
- result.KeyType = GetOrCreateMetadataFor(keyType, options);
- result.ValueType = GetOrCreateMetadataFor(valueType, options);
- return result;
- }
+ if (type.IsGenericTypeDefinition)
+ {
+ Log.GenericTypeDefinitionNotSupported(_logger, type);
+ _context.TypeMetadata.Remove(type);
+ return null;
+ }
- if (_collectionFactory.CanConvert(type, options))
- {
- result.Kind = FormDataTypeKind.Collection;
- result.ElementType = GetOrCreateMetadataFor(CollectionConverterFactory.ResolveElementType(type)!, options);
- return result;
- }
+ // Check for cycles and mark any type as recursive if needed.
+ DetectCyclesAndMarkMetadataTypesAsRecursive(type, result);
- result.Kind = FormDataTypeKind.Object;
- _context.Track(type);
- var constructors = type.GetConstructors();
+ // We found the value on the existing metadata, we can return it.
+ if (value != null)
+ {
+ return result;
+ }
- if (constructors.Length == 1)
- {
- result.Constructor = constructors[0];
- }
+ // These blocks are evaluated in a specific order.
+ if (_parsableFactory.CanConvert(type, options) || type.IsEnum ||
+ (Nullable.GetUnderlyingType(type) is { } underlyingType &&
+ _parsableFactory.CanConvert(underlyingType, options)))
+ {
+ Log.PrimitiveType(_logger, type);
+ result.Kind = FormDataTypeKind.Primitive;
+ return result;
+ }
- if (result.Constructor != null)
- {
- var values = result.Constructor.GetParameters();
+ if (_dictionaryFactory.CanConvert(type, options))
+ {
+ Log.DictionaryType(_logger, type);
+ result.Kind = FormDataTypeKind.Dictionary;
+ var (keyType, valueType) = DictionaryConverterFactory.ResolveDictionaryTypes(type)!;
+ result.KeyType = GetOrCreateMetadataFor(keyType, options);
+ result.ValueType = GetOrCreateMetadataFor(valueType, options);
+ return result;
+ }
- foreach (var parameter in values)
+ if (_collectionFactory.CanConvert(type, options))
{
- var parameterTypeInfo = GetOrCreateMetadataFor(parameter.ParameterType, options);
- result.ConstructorParameters.Add(new FormDataParameterMetadata(parameter, parameterTypeInfo));
+ Log.CollectionType(_logger, type);
+ result.Kind = FormDataTypeKind.Collection;
+ result.ElementType = GetOrCreateMetadataFor(CollectionConverterFactory.ResolveElementType(type)!, options);
+ return result;
}
- }
- var candidateProperty = PropertyHelper.GetVisibleProperties(type);
- foreach (var propertyHelper in candidateProperty)
- {
- var property = propertyHelper.Property;
- var matchingConstructorParameter = result
- .ConstructorParameters
- .FirstOrDefault(p => string.Equals(p.Name, property.Name, StringComparison.OrdinalIgnoreCase));
+ Log.ObjectType(_logger, type);
+ result.Kind = FormDataTypeKind.Object;
+ _context.Track(type);
+ var constructors = type.GetConstructors();
- if (matchingConstructorParameter != null)
+ if (constructors.Length == 1)
{
- var dataMember = property.GetCustomAttribute();
- if (dataMember != null && dataMember.IsNameSetExplicitly && dataMember.Name != null)
+ result.Constructor = constructors[0];
+ if (type.IsAbstract)
{
- matchingConstructorParameter.Name = dataMember.Name;
+ Log.AbstractClassesNotSupported(_logger, type);
+ _context.TypeMetadata.Remove(type);
+ return null;
}
-
- // The propertyHelper is already present in the constructor, we don't need to add it again.
- continue;
}
-
- var ignoreDataMember = property.GetCustomAttribute();
- if (ignoreDataMember != null)
+ else if (constructors.Length > 1)
{
- // The propertyHelper is marked as ignored, we don't need to add it.
- continue;
+ // We can't select the constructor when there are multiple of them.
+ Log.MultiplePublicConstructorsFound(_logger, type);
+ return null;
}
-
- if (property.SetMethod == null || !property.SetMethod.IsPublic)
+ else if (!type.IsValueType)
{
- // The property is readonly, we don't need to add it.
- continue;
- }
+ if (type.IsInterface)
+ {
+ Log.InterfacesNotSupported(_logger, type);
+ }
+ else if (type.IsAbstract)
+ {
+ Log.AbstractClassesNotSupported(_logger, type);
+ }
+ else
+ {
+ Log.NoPublicConstructorFound(_logger, type);
+ }
- var propertyTypeInfo = GetOrCreateMetadataFor(property.PropertyType, options);
- var propertyInfo = new FormDataPropertyMetadata(property, propertyTypeInfo);
+ _context.TypeMetadata.Remove(type);
+ // We can't bind to reference types without constructors.
+ return null;
+ }
- var dataMemberAttribute = property.GetCustomAttribute();
- if (dataMemberAttribute != null && dataMemberAttribute.IsNameSetExplicitly && dataMemberAttribute.Name != null)
+ if (result.Constructor != null)
{
- propertyInfo.Name = dataMemberAttribute.Name;
- propertyInfo.Required = dataMemberAttribute.IsRequired;
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ var parameters = $"({string.Join(", ", result.Constructor.GetParameters().Select(p => p.ParameterType.Name))})";
+ Log.ConstructorFound(_logger, type, parameters);
+ }
+
+ var values = result.Constructor.GetParameters();
+
+ foreach (var parameter in values)
+ {
+ Log.ConstructorParameter(_logger, type, parameter.Name!, parameter.ParameterType);
+ var parameterTypeInfo = GetOrCreateMetadataFor(parameter.ParameterType, options);
+ if (parameterTypeInfo == null)
+ {
+ Log.ConstructorParameterTypeNotSupported(_logger, type, parameter.Name!, parameter.ParameterType);
+ _context.TypeMetadata.Remove(type);
+ return null;
+ }
+
+ result.ConstructorParameters.Add(new FormDataParameterMetadata(parameter, parameterTypeInfo));
+ }
}
- var requiredAttribute = property.GetCustomAttribute();
- if (requiredAttribute != null)
+ var candidateProperty = PropertyHelper.GetVisibleProperties(type);
+ foreach (var propertyHelper in candidateProperty)
{
- propertyInfo.Required = true;
+ var property = propertyHelper.Property;
+ Log.CandidateProperty(_logger, propertyHelper.Name, property.PropertyType);
+ var matchingConstructorParameter = result
+ .ConstructorParameters
+ .FirstOrDefault(p => string.Equals(p.Name, property.Name, StringComparison.OrdinalIgnoreCase));
+
+ if (matchingConstructorParameter != null)
+ {
+ Log.MatchingConstructorParameterFound(_logger, matchingConstructorParameter.Name, property.Name);
+ var dataMember = property.GetCustomAttribute();
+ if (dataMember != null && dataMember.IsNameSetExplicitly && dataMember.Name != null)
+ {
+ Log.CustomParameterNameMetadata(_logger, dataMember.Name, property.Name);
+ matchingConstructorParameter.Name = dataMember.Name;
+ }
+
+ // The propertyHelper is already present in the constructor, we don't need to add it again.
+ continue;
+ }
+
+ var ignoreDataMember = property.GetCustomAttribute();
+ if (ignoreDataMember != null)
+ {
+ Log.IgnoredProperty(_logger, property.Name);
+ // The propertyHelper is marked as ignored, we don't need to add it.
+ continue;
+ }
+
+ if (property.SetMethod == null || !property.SetMethod.IsPublic)
+ {
+ Log.NonPublicSetter(_logger, property.Name);
+ // The property is readonly, we don't need to add it.
+ continue;
+ }
+
+ var propertyTypeInfo = GetOrCreateMetadataFor(property.PropertyType, options);
+ if (propertyTypeInfo == null)
+ {
+ Log.PropertyTypeNotSupported(_logger, type, property.Name, property.PropertyType);
+ _context.TypeMetadata.Remove(type);
+ return null;
+ }
+ var propertyInfo = new FormDataPropertyMetadata(property, propertyTypeInfo);
+
+ var dataMemberAttribute = property.GetCustomAttribute();
+ if (dataMemberAttribute != null && dataMemberAttribute.IsNameSetExplicitly && dataMemberAttribute.Name != null)
+ {
+ Log.CustomParameterNameMetadata(_logger, dataMemberAttribute.Name, property.Name);
+ propertyInfo.Name = dataMemberAttribute.Name;
+ propertyInfo.Required = dataMemberAttribute.IsRequired;
+ Log.PropertyRequired(_logger, propertyInfo.Name);
+ }
+
+ var requiredAttribute = property.GetCustomAttribute();
+ if (requiredAttribute != null)
+ {
+ propertyInfo.Required = true;
+ Log.PropertyRequired(_logger, propertyInfo.Name);
+ }
+
+ result.Properties.Add(propertyInfo);
}
- result.Properties.Add(propertyInfo);
+ Log.MetadataComputed(_logger, type);
+ return result;
}
-
- return result;
- }
- finally
- {
- _context.Untrack(type);
- if (shouldClearContext)
+ finally
{
- _context.EndResolveGraph();
+ _context.Untrack(type);
+ if (shouldClearContext)
+ {
+ Log.EndResolveMetadataGraph(_logger, type);
+ _context.EndResolveGraph();
+ }
}
}
}
@@ -187,15 +271,29 @@ private void DetectCyclesAndMarkMetadataTypesAsRecursive(Type type, FormDataType
// We found an exact match in the current resolution graph.
// This means that the type is recursive.
result.IsRecursive = true;
+ ReportRecursiveChain(type);
+
}
else if (type.IsSubclassOf(_context.CurrentTypes[i]))
{
// We found a type that is assignable from the current type.
// This means that the type is recursive.
- // The type must have already been registered in DI.
var existingType = _context.TypeMetadata[_context.CurrentTypes[i]];
+ ReportRecursiveChain(type);
existingType.IsRecursive = true;
}
+
+ // We don't need to check for interfaces here because we never map to an interface, so if we encounter one, we will fail,
+ // as we can't construct the converter to map the interface to a concrete instance.
+ }
+
+ void ReportRecursiveChain(Type type)
+ {
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ var chain = string.Join(" -> ", _context.CurrentTypes.Append(type).Select(t => t.Name));
+ Log.RecursiveTypeFound(_logger, type, chain);
+ }
}
}
@@ -236,4 +334,82 @@ internal void Untrack(Type type)
CurrentTypes.Remove(type);
}
}
+
+ private static partial class Log
+ {
+ [LoggerMessage(1, LogLevel.Debug, "Begin resolve metadata graph for type '{Type}'.", EventName = nameof(StartResolveMetadataGraph))]
+ public static partial void StartResolveMetadataGraph(ILogger logger, Type type);
+
+ [LoggerMessage(2, LogLevel.Debug, "End resolve metadata graph for type '{Type}'.", EventName = nameof(EndResolveMetadataGraph))]
+ public static partial void EndResolveMetadataGraph(ILogger logger, Type type);
+
+ [LoggerMessage(3, LogLevel.Debug, "Cached metadata found for type '{Type}'.", EventName = nameof(Metadata))]
+ public static partial void MetadataFound(ILogger logger, Type type);
+
+ [LoggerMessage(4, LogLevel.Debug, "No cached metadata graph for type '{Type}'.", EventName = nameof(NoMetadataFound))]
+ public static partial void NoMetadataFound(ILogger logger, Type type);
+
+ [LoggerMessage(5, LogLevel.Debug, "Recursive type '{Type}' found in the resolution graph '{Chain}'.", EventName = nameof(RecursiveTypeFound))]
+ public static partial void RecursiveTypeFound(ILogger logger, Type type, string chain);
+
+ [LoggerMessage(6, LogLevel.Debug, "'{Type}' identified as primitive.", EventName = nameof(PrimitiveType))]
+ public static partial void PrimitiveType(ILogger logger, Type type);
+
+ [LoggerMessage(7, LogLevel.Debug, "'{Type}' identified as dictionary.", EventName = nameof(DictionaryType))]
+ public static partial void DictionaryType(ILogger logger, Type type);
+
+ [LoggerMessage(8, LogLevel.Debug, "'{Type}' identified as collection.", EventName = nameof(CollectionType))]
+ public static partial void CollectionType(ILogger logger, Type type);
+
+ [LoggerMessage(9, LogLevel.Debug, "'{Type}' identified as object.", EventName = nameof(ObjectType))]
+ public static partial void ObjectType(ILogger logger, Type type);
+
+ [LoggerMessage(10, LogLevel.Debug, "Constructor found for type '{Type}' with parameters '{Parameters}'.", EventName = nameof(ConstructorFound))]
+ public static partial void ConstructorFound(ILogger logger, Type type, string parameters);
+
+ [LoggerMessage(11, LogLevel.Debug, "Constructor parameter '{Name}' of type '{ParameterType}' found for type '{Type}'.", EventName = nameof(ConstructorParameter))]
+ public static partial void ConstructorParameter(ILogger logger, Type type, string name, Type parameterType);
+
+ [LoggerMessage(12, LogLevel.Debug, "Candidate property '{Name}' of type '{PropertyType}'.", EventName = nameof(CandidateProperty))]
+ public static partial void CandidateProperty(ILogger logger, string name, Type propertyType);
+
+ [LoggerMessage(13, LogLevel.Debug, "Candidate property {PropertyName} has a matching constructor parameter '{ConstructorParameterName}'.", EventName = nameof(MatchingConstructorParameterFound))]
+ public static partial void MatchingConstructorParameterFound(ILogger logger, string constructorParameterName, string propertyName);
+
+ [LoggerMessage(14, LogLevel.Debug, "Candidate property or constructor parameter {PropertyName} defines a custom name '{CustomName}'.", EventName = nameof(CustomParameterNameMetadata))]
+ public static partial void CustomParameterNameMetadata(ILogger logger, string customName, string propertyName);
+
+ [LoggerMessage(15, LogLevel.Debug, "Candidate property {Name} will not be mapped. It has been explicitly ignored.", EventName = nameof(IgnoredProperty))]
+ public static partial void IgnoredProperty(ILogger logger, string name);
+
+ [LoggerMessage(16, LogLevel.Debug, "Candidate property {Name} will not be mapped. It has no public setter.", EventName = nameof(NonPublicSetter))]
+ public static partial void NonPublicSetter(ILogger logger, string name);
+
+ [LoggerMessage(17, LogLevel.Debug, "Candidate property {Name} is marked as required.", EventName = nameof(PropertyRequired))]
+ public static partial void PropertyRequired(ILogger logger, string name);
+
+ [LoggerMessage(18, LogLevel.Debug, "Metadata created for {Type}.", EventName = nameof(MetadataComputed))]
+ public static partial void MetadataComputed(ILogger logger, Type type);
+
+ [LoggerMessage(19, LogLevel.Debug, "Can not map type generic type definition '{Type}'.", EventName = nameof(GenericTypeDefinitionNotSupported))]
+ public static partial void GenericTypeDefinitionNotSupported(ILogger logger, Type type);
+
+ [LoggerMessage(20, LogLevel.Debug, "Unable to select a constructor. Multiple public constructors found for type '{Type}'.", EventName = nameof(MultiplePublicConstructorsFound))]
+ public static partial void MultiplePublicConstructorsFound(ILogger logger, Type type);
+
+ [LoggerMessage(21, LogLevel.Debug, "Can not map interface type '{Type}'.", EventName = nameof(InterfacesNotSupported))]
+ public static partial void InterfacesNotSupported(ILogger logger, Type type);
+
+ [LoggerMessage(22, LogLevel.Debug, "Can not map abstract type '{Type}'.", EventName = nameof(AbstractClassesNotSupported))]
+ public static partial void AbstractClassesNotSupported(ILogger logger, Type type);
+
+ [LoggerMessage(23, LogLevel.Debug, "Unable to select a constructor. No public constructors found for type '{Type}'.", EventName = nameof(NoPublicConstructorFound))]
+ public static partial void NoPublicConstructorFound(ILogger logger, Type type);
+
+ [LoggerMessage(24, LogLevel.Debug, "Can not map type '{Type}'. Constructor parameter {Name} of type {ParameterType} is not supported.", EventName = nameof(ConstructorParameterTypeNotSupported))]
+ public static partial void ConstructorParameterTypeNotSupported(ILogger logger, Type type, string name, Type parameterType);
+
+ [LoggerMessage(25, LogLevel.Debug, "Can not map type '{Type}'. Property {Name} of type {PropertyType} is not supported.", EventName = nameof(PropertyTypeNotSupported))]
+ public static partial void PropertyTypeNotSupported(ILogger logger, Type type, string name, Type propertyType);
+ }
}
diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt
index a3ca0d2f9115..39f2c2d3031a 100644
--- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt
@@ -18,16 +18,18 @@ Microsoft.AspNetCore.Components.Endpoints.Infrastructure.RenderModeEndpointProvi
Microsoft.AspNetCore.Components.Endpoints.Infrastructure.RenderModeEndpointProvider.RenderModeEndpointProvider() -> void
Microsoft.AspNetCore.Components.Endpoints.IRazorComponentEndpointInvoker
Microsoft.AspNetCore.Components.Endpoints.IRazorComponentEndpointInvoker.Render(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task!
-Microsoft.AspNetCore.Components.Endpoints.RazorComponentsOptions
-Microsoft.AspNetCore.Components.Endpoints.RazorComponentsOptions.MaxFormMappingCollectionSize.get -> int
-Microsoft.AspNetCore.Components.Endpoints.RazorComponentsOptions.MaxFormMappingCollectionSize.set -> void
-Microsoft.AspNetCore.Components.Endpoints.RazorComponentsOptions.MaxFormMappingErrorCount.get -> int
-Microsoft.AspNetCore.Components.Endpoints.RazorComponentsOptions.MaxFormMappingErrorCount.set -> void
-Microsoft.AspNetCore.Components.Endpoints.RazorComponentsOptions.MaxFormMappingKeySize.get -> int
-Microsoft.AspNetCore.Components.Endpoints.RazorComponentsOptions.MaxFormMappingKeySize.set -> void
-Microsoft.AspNetCore.Components.Endpoints.RazorComponentsOptions.MaxFormMappingRecursionDepth.get -> int
-Microsoft.AspNetCore.Components.Endpoints.RazorComponentsOptions.MaxFormMappingRecursionDepth.set -> void
-Microsoft.AspNetCore.Components.Endpoints.RazorComponentsOptions.RazorComponentsOptions() -> void
+Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions
+Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.DetailedErrors.get -> bool
+Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.DetailedErrors.set -> void
+Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.MaxFormMappingCollectionSize.get -> int
+Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.MaxFormMappingCollectionSize.set -> void
+Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.MaxFormMappingErrorCount.get -> int
+Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.MaxFormMappingErrorCount.set -> void
+Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.MaxFormMappingKeySize.get -> int
+Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.MaxFormMappingKeySize.set -> void
+Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.MaxFormMappingRecursionDepth.get -> int
+Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.MaxFormMappingRecursionDepth.set -> void
+Microsoft.AspNetCore.Components.Endpoints.RazorComponentsServiceOptions.RazorComponentsServiceOptions() -> void
Microsoft.AspNetCore.Components.Endpoints.RootComponentMetadata
Microsoft.AspNetCore.Components.Endpoints.RootComponentMetadata.RootComponentMetadata(System.Type! rootComponentType) -> void
Microsoft.AspNetCore.Components.Endpoints.RootComponentMetadata.Type.get -> System.Type!
@@ -57,4 +59,4 @@ Microsoft.Extensions.DependencyInjection.IRazorComponentsBuilder.Services.get ->
Microsoft.Extensions.DependencyInjection.RazorComponentsServiceCollectionExtensions
static Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilderExtensions.AddAdditionalAssemblies(this Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! builder, params System.Reflection.Assembly![]! assemblies) -> Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder!
static Microsoft.AspNetCore.Builder.RazorComponentsEndpointRouteBuilderExtensions.MapRazorComponents(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints) -> Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder!
-static Microsoft.Extensions.DependencyInjection.RazorComponentsServiceCollectionExtensions.AddRazorComponents(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action? configure = null) -> Microsoft.Extensions.DependencyInjection.IRazorComponentsBuilder!
+static Microsoft.Extensions.DependencyInjection.RazorComponentsServiceCollectionExtensions.AddRazorComponents(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action? configure = null) -> Microsoft.Extensions.DependencyInjection.IRazorComponentsBuilder!
\ No newline at end of file
diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs
index 10c18e627fc2..0b96f6549c7c 100644
--- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs
+++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs
@@ -165,7 +165,7 @@ private static void HandleExceptionAfterResponseStarted(HttpContext httpContext,
// We already started the response so we have no choice but to return a 200 with HTML and will
// have to communicate the error information within that
var env = httpContext.RequestServices.GetRequiredService();
- var options = httpContext.RequestServices.GetRequiredService>();
+ var options = httpContext.RequestServices.GetRequiredService>();
var showDetailedErrors = env.IsDevelopment() || options.Value.DetailedErrors;
var message = showDetailedErrors
? exception.ToString()
diff --git a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs
index 31b48e01fa12..e82936aa8e1a 100644
--- a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs
+++ b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs
@@ -267,6 +267,112 @@ public void Deserialize_Collections_SingleElement_ReturnsCollection()
Assert.Equal(10, value);
}
+ [Fact]
+ public void Deserialize_Collections_SupportsMultipleElementsPerKey_ForSingleValueElementTypes_ParsableTypes()
+ {
+ // Arrange
+ var data = new Dictionary() { ["values"] = new StringValues(new[] { "10", "11" }) };
+ var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
+ reader.PushPrefix("values");
+ var options = new FormDataMapperOptions();
+
+ // Act
+ var result = FormDataMapper.Map>(reader, options);
+
+ // Assert
+ Assert.Collection(result,
+ v => Assert.Equal(10, v),
+ v => Assert.Equal(11, v));
+ }
+
+ [Fact]
+ public void Deserialize_Collections_SupportsMultipleElementsPerKey_ForSingleValueElementTypes_EnumTypes()
+ {
+ // Arrange
+ var data = new Dictionary() { ["values"] = new StringValues(new[] { "Red", "Blue" }) };
+ var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
+ reader.PushPrefix("values");
+ var options = new FormDataMapperOptions();
+
+ // Act
+ var result = FormDataMapper.Map>(reader, options);
+
+ // Assert
+ Assert.Collection(result,
+ v => Assert.Equal(Colors.Red, v),
+ v => Assert.Equal(Colors.Blue, v));
+ }
+
+ [Fact]
+ public void Deserialize_Collections_SupportsMultipleElementsPerKey_ForSingleValueElementTypes_Nullable_WhenUnderlyingElementIsSingleValue()
+ {
+ // Arrange
+ var data = new Dictionary() { ["values"] = new StringValues(new[] { "Red", "Blue" }) };
+ var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
+ reader.PushPrefix("values");
+ var options = new FormDataMapperOptions();
+
+ // Act
+ var result = FormDataMapper.Map>(reader, options);
+
+ // Assert
+ Assert.Collection(result,
+ v => Assert.Equal(Colors.Red, v),
+ v => Assert.Equal(Colors.Blue, v));
+ }
+
+ [Fact]
+ public void Deserialize_Collections_MultipleElementsPerKey_CanReportErrors()
+ {
+ // Arrange
+ var data = new Dictionary() { ["values"] = new StringValues(new[] { "10", "a" }) };
+ var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
+ reader.PushPrefix("values");
+ var errors = new List();
+ reader.ErrorHandler = (key, message, attemptedValue) =>
+ {
+ errors.Add(new FormDataMappingError(key, message, attemptedValue));
+ };
+
+ var options = new FormDataMapperOptions();
+
+ // Act
+ var result = FormDataMapper.Map>(reader, options);
+
+ // Assert
+ Assert.Equal(10, Assert.Single(result));
+ var error = Assert.Single(errors);
+ Assert.Equal("values", error.Key);
+ Assert.Equal("The value 'a' is not valid for 'values'.", error.Message.ToString(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void Deserialize_Collections_MultipleElementsPerKey_ContinuesProcessingValuesAfterErrors()
+ {
+ // Arrange
+ var data = new Dictionary() { ["values"] = new StringValues(new[] { "10", "a", "11" }) };
+ var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
+ reader.PushPrefix("values");
+ var errors = new List();
+ reader.ErrorHandler = (key, message, attemptedValue) =>
+ {
+ errors.Add(new FormDataMappingError(key, message, attemptedValue));
+ };
+
+ var options = new FormDataMapperOptions();
+
+ // Act
+ var result = FormDataMapper.Map>(reader, options);
+
+ // Assert
+ Assert.Collection(result,
+ v => Assert.Equal(10, v),
+ v => Assert.Equal(11, v));
+ var error = Assert.Single(errors);
+ Assert.Equal("values", error.Key);
+ Assert.Equal("The value 'a' is not valid for 'values'.", error.Message.ToString(CultureInfo.InvariantCulture));
+ }
+
[Theory]
[InlineData(99)]
[InlineData(100)]
@@ -337,6 +443,66 @@ public void Deserialize_Collections_PoolsArraysCorrectly(int size)
Assert.Equal(rented.Count, returned.Count);
}
+ [Theory]
+ [InlineData(2)]
+ [InlineData(99)]
+ [InlineData(100)]
+ [InlineData(101)]
+ [InlineData(109)]
+ [InlineData(110)]
+ [InlineData(120)]
+ public void Deserialize_Collections_AlwaysReturnsBuffer(int size)
+ {
+ // Arrange
+ var rented = new List();
+ var returned = new List();
+
+ var data = new Dictionary(Enumerable.Range(0, size)
+ .Select(i => new KeyValuePair(
+ $"[{i.ToString(CultureInfo.InvariantCulture)}]",
+ (i + 10).ToString(CultureInfo.InvariantCulture))));
+
+ var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
+ var options = new FormDataMapperOptions
+ {
+ MaxCollectionSize = 140
+ };
+
+ var elementConverter = new ThrowingConverter();
+ elementConverter.OnTryReadDelegate = (ref FormDataReader context, Type type, FormDataMapperOptions options, out int result, out bool found) =>
+ {
+ context.TryGetValue(out var value);
+ var index = int.Parse(value, CultureInfo.InvariantCulture) - 10;
+ if (index + 1 == size)
+ {
+ throw new InvalidOperationException("Can't parse this!");
+ }
+ result = default;
+ found = true;
+ return false;
+ };
+
+ var converter = new CollectionConverter<
+ int[],
+ TestArrayPoolBufferAdapter,
+ TestArrayPoolBufferAdapter.PooledBuffer,
+ int>(elementConverter);
+
+ options.AddConverter(converter);
+
+ TestArrayPoolBufferAdapter.OnRent += rented.Add;
+ TestArrayPoolBufferAdapter.OnReturn += returned.Add;
+
+ // Act
+ var result = Assert.Throws(() => FormDataMapper.Map(reader, options));
+
+ TestArrayPoolBufferAdapter.OnRent -= rented.Add;
+ TestArrayPoolBufferAdapter.OnReturn -= returned.Add;
+
+ // Assert
+ Assert.Equal(rented.Count, returned.Count);
+ }
+
[Theory]
[InlineData(1, 2)]
[InlineData(2, 2)]
@@ -2118,3 +2284,17 @@ public ThrowsWithMissingParameterValue(string key)
public int Value { get; set; }
}
+
+public class Throwing { }
+
+internal class ThrowingConverter : FormDataConverter
+{
+ internal delegate bool OnTryRead(ref FormDataReader context, Type type, FormDataMapperOptions options, out int result, out bool found);
+
+ internal OnTryRead OnTryReadDelegate { get; set; } =
+ (ref FormDataReader context, Type type, FormDataMapperOptions options, out int result, out bool found) =>
+ throw new InvalidOperationException("Could not read value.");
+
+ internal override bool TryRead(ref FormDataReader context, Type type, FormDataMapperOptions options, out int result, out bool found) =>
+ OnTryReadDelegate.Invoke(ref context, type, options, out result, out found);
+}
diff --git a/src/Components/Endpoints/test/Binding/FormDataMetadataFactoryTests.cs b/src/Components/Endpoints/test/Binding/FormDataMetadataFactoryTests.cs
index f5e1f4c93812..adc3c4579f51 100644
--- a/src/Components/Endpoints/test/Binding/FormDataMetadataFactoryTests.cs
+++ b/src/Components/Endpoints/test/Binding/FormDataMetadataFactoryTests.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.Extensions.Logging.Testing;
+
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping.Metadata;
public class FormDataMetadataFactoryTests
@@ -9,7 +11,7 @@ public class FormDataMetadataFactoryTests
public void CanCreateMetadata_ForBasicClassTypes()
{
// Arrange
- var (factory, options) = ResolveFactory();
+ var (factory, options, logs) = ResolveFactory();
// Act
var metadata = factory.GetOrCreateMetadataFor(typeof(Customer), options);
@@ -43,7 +45,7 @@ public void CanCreateMetadata_ForBasicClassTypes()
public void CanCreateMetadata_ForMoreComplexClassTypes()
{
// Arrange
- var (factory, options) = ResolveFactory();
+ var (factory, options, logs) = ResolveFactory();
// Act
var metadata = factory.GetOrCreateMetadataFor(typeof(CustomerWithAddress), options);
@@ -116,7 +118,7 @@ public void CanCreateMetadata_ForMoreComplexClassTypes()
public void CanCreateMetadata_ForValueTypes()
{
// Arrange
- var (factory, options) = ResolveFactory();
+ var (factory, options, logs) = ResolveFactory();
// Act
var metadata = factory.GetOrCreateMetadataFor(typeof(Address), options);
@@ -150,7 +152,7 @@ public void CanCreateMetadata_ForValueTypes()
public void CanCreateMetadata_DetectsConstructors()
{
// Arrange
- var (factory, options) = ResolveFactory();
+ var (factory, options, logs) = ResolveFactory();
// Act
var metadata = factory.GetOrCreateMetadataFor(typeof(KeyValuePair), options);
@@ -184,7 +186,7 @@ public void CanCreateMetadata_DetectsConstructors()
public void CanCreateMetadata_ForTypeWithCollection()
{
// Arrange
- var (factory, options) = ResolveFactory();
+ var (factory, options, logs) = ResolveFactory();
// Act
var metadata = factory.GetOrCreateMetadataFor(typeof(CustomerWithOrders), options);
Assert.NotNull(metadata);
@@ -247,7 +249,7 @@ public void CanCreateMetadata_ForTypeWithCollection()
public void CanCreateMetadata_ForTypeWithDictionary()
{
// Arrange
- var (factory, options) = ResolveFactory();
+ var (factory, options, logs) = ResolveFactory();
// Act
var metadata = factory.GetOrCreateMetadataFor(typeof(CompanyWithWarehousesByLocation), options);
@@ -321,7 +323,7 @@ public void CanCreateMetadata_ForTypeWithDictionary()
public void CanCreateMetadata_ForRecursiveTypes()
{
// Arrange
- var (factory, options) = ResolveFactory();
+ var (factory, options, logs) = ResolveFactory();
// Act
var metadata = factory.GetOrCreateMetadataFor(typeof(RecursiveList), options);
@@ -355,7 +357,7 @@ public void CanCreateMetadata_ForRecursiveTypes()
public void CanCreateMetadata_ForRecursiveTypesWithInheritance()
{
// Arrange
- var (factory, options) = ResolveFactory();
+ var (factory, options, logs) = ResolveFactory();
// Act
var metadata = factory.GetOrCreateMetadataFor(typeof(BaseList), options);
@@ -408,7 +410,7 @@ public void CanCreateMetadata_ForRecursiveTypesWithInheritance()
public void CanCreateMetadata_ForRecursiveTypesCollections()
{
// Arrange
- var (factory, options) = ResolveFactory();
+ var (factory, options, logs) = ResolveFactory();
// Act
var metadata = factory.GetOrCreateMetadataFor(typeof(Tree), options);
@@ -445,7 +447,7 @@ public void CanCreateMetadata_ForRecursiveTypesCollections()
public void CanCreateMetadata_ForRecursiveTypesDictionaries()
{
// Arrange
- var (factory, options) = ResolveFactory();
+ var (factory, options, logs) = ResolveFactory();
// Act
var metadata = factory.GetOrCreateMetadataFor(typeof(DictionaryTree), options);
@@ -478,14 +480,186 @@ public void CanCreateMetadata_ForRecursiveTypesDictionaries()
});
}
- private (FormDataMetadataFactory, FormDataMapperOptions) ResolveFactory()
+ [Fact]
+ public void CanCreateMetadata_SinglePublicConstructorAndNonPublicConstructors()
+ {
+ // Arrange
+ var (factory, options, logs) = ResolveFactory();
+
+ // Act
+ var metadata = factory.GetOrCreateMetadataFor(typeof(TypeWithNonPublicConstructors), options);
+
+ // Assert
+ Assert.NotNull(metadata);
+ Assert.Equal(typeof(TypeWithNonPublicConstructors), metadata.Type);
+ Assert.Equal(FormDataTypeKind.Object, metadata.Kind);
+ Assert.False(metadata.IsRecursive);
+ Assert.NotNull(metadata.Constructor);
+ Assert.Collection(metadata.ConstructorParameters,
+ parameter =>
+ {
+ Assert.Equal("id", parameter.Name);
+ Assert.NotNull(parameter.ParameterMetadata);
+ Assert.Equal(typeof(int), parameter.ParameterMetadata.Type);
+ Assert.Equal(FormDataTypeKind.Primitive, parameter.ParameterMetadata.Kind);
+ Assert.Null(parameter.ParameterMetadata.Constructor);
+ Assert.Empty(parameter.ParameterMetadata.Properties);
+ });
+ }
+
+ [Fact]
+ public void CreateMetadata_ReturnsNull_ForInterfaceTypes()
+ {
+ // Arrange
+ var (factory, options, logs) = ResolveFactory();
+ // Act
+ var metadata = factory.GetOrCreateMetadataFor(typeof(ICustomer), options);
+
+ // Assert
+ Assert.Null(metadata);
+ }
+
+ [Fact]
+ public void CreateMetadata_ReturnsNull_ForGenericTypeDefinitions()
+ {
+ // Arrange
+ var (factory, options, logs) = ResolveFactory();
+ // Act
+ var metadata = factory.GetOrCreateMetadataFor(typeof(IList<>), options);
+
+ // Assert
+ Assert.Null(metadata);
+ }
+
+ [Fact]
+ public void CreateMetadata_ReturnsNull_ForTypesWithMultiplePublicConstructors()
{
- var options = new FormDataMapperOptions();
+ // Arrange
+ var (factory, options, logs) = ResolveFactory();
+ // Act
+ var metadata = factory.GetOrCreateMetadataFor(typeof(TypeWithMultipleConstructors), options);
+
+ // Assert
+ Assert.Null(metadata);
+ }
+
+ [Fact]
+ public void CreateMetadata_ReturnsNull_ForAbstractTypes()
+ {
+ // Arrange
+ var (factory, options, logs) = ResolveFactory();
+ // Act
+ var metadata = factory.GetOrCreateMetadataFor(typeof(AbstracType), options);
+
+ // Assert
+ Assert.Null(metadata);
+ }
+
+ [Fact]
+ public void CreateMetadata_ReturnsNull_ForTypesWithNoPublicConstructors()
+ {
+ // Arrange
+ var (factory, options, logs) = ResolveFactory();
+ // Act
+ var metadata = factory.GetOrCreateMetadataFor(typeof(NoPublicConstructor), options);
+
+ // Assert
+ Assert.Null(metadata);
+ }
+
+ [Fact]
+ public void CreateMetadata_ReturnsNull_ForTypesWithUnsupportedConstructorParameters()
+ {
+ // Arrange
+ var (factory, options, logs) = ResolveFactory();
+
+ // Act
+ var metadata = factory.GetOrCreateMetadataFor(typeof(UnsupportedConstructorParameterType), options);
+
+ // Assert
+ Assert.Null(metadata);
+ }
+
+ [Fact]
+ public void CreateMetadata_ReturnsNull_ForTypesWithUnsupportedProperties()
+ {
+ // Arrange
+ var (factory, options, logs) = ResolveFactory();
+
+ // Act
+ var metadata = factory.GetOrCreateMetadataFor(typeof(UnsupportedPropertyType), options);
+
+ // Assert
+ Assert.Null(metadata);
+ }
+
+ private (FormDataMetadataFactory, FormDataMapperOptions, TestSink) ResolveFactory()
+ {
+ var logMessages = new List();
+ var sink = new TestSink();
+ var options = new FormDataMapperOptions(new TestLoggerFactory(sink, enabled: true));
var factory = options.Factories.OfType().Single().MetadataFactory;
- return (factory, options);
+ return (factory, options, sink);
}
}
+public interface ICustomer
+{
+ public int Id { get; set; }
+}
+
+public class TypeWithMultipleConstructors
+{
+ public TypeWithMultipleConstructors()
+ {
+ }
+
+ public TypeWithMultipleConstructors(int id)
+ {
+ }
+
+ public int Id { get; set; }
+}
+
+public abstract class AbstracType
+{
+ public AbstracType()
+ {
+ }
+}
+
+public class NoPublicConstructor
+{
+ private NoPublicConstructor()
+ {
+ }
+}
+
+public class UnsupportedConstructorParameterType
+{
+ public UnsupportedConstructorParameterType(NoPublicConstructor noPublicConstructor)
+ {
+ }
+}
+
+public class UnsupportedPropertyType
+{
+ public ICustomer Customer { get; set; }
+}
+
+public class TypeWithNonPublicConstructors
+{
+ internal TypeWithNonPublicConstructors()
+ {
+ }
+
+ public TypeWithNonPublicConstructors(int id)
+ {
+ }
+
+ public int Id { get; set; }
+}
+
public class CustomerWithOrders
{
public int Id { get; set; }
@@ -557,3 +731,16 @@ public class CustomerWithAddress
public Address BillingAddress { get; set; }
public Address ShippingAddress { get; set; }
}
+
+internal record struct LogMessage(int id, string message, string eventName)
+{
+ public static implicit operator (int id, string message, string eventName)(LogMessage value)
+ {
+ return (value.id, value.message, value.eventName);
+ }
+
+ public static implicit operator LogMessage((int id, string message, string eventName) value)
+ {
+ return new LogMessage(value.id, value.message, value.eventName);
+ }
+}
diff --git a/src/Components/Endpoints/test/HttpContextFormValueMapperTest.cs b/src/Components/Endpoints/test/HttpContextFormValueMapperTest.cs
index 957b200625ae..1b139fcf8549 100644
--- a/src/Components/Endpoints/test/HttpContextFormValueMapperTest.cs
+++ b/src/Components/Endpoints/test/HttpContextFormValueMapperTest.cs
@@ -36,7 +36,7 @@ public void CanMap_MatchesOnScopeAndFormName(bool expectedResult, string incomin
var formData = new HttpContextFormDataProvider();
formData.SetFormData(incomingFormName, new Dictionary());
- var mapper = new HttpContextFormValueMapper(formData, Options.Create(new()));
+ var mapper = new HttpContextFormValueMapper(formData, Options.Create(new()));
var canMap = mapper.CanMap(typeof(string), scopeName, formNameOrNull);
Assert.Equal(expectedResult, canMap);
diff --git a/src/Components/Endpoints/test/RazorComponentsServiceCollectionExtensionsTest.cs b/src/Components/Endpoints/test/RazorComponentsServiceCollectionExtensionsTest.cs
index 74aa7caad748..7883b69fa5ed 100644
--- a/src/Components/Endpoints/test/RazorComponentsServiceCollectionExtensionsTest.cs
+++ b/src/Components/Endpoints/test/RazorComponentsServiceCollectionExtensionsTest.cs
@@ -1,9 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms.Mapping;
+using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.DependencyInjection;
@@ -14,6 +14,7 @@ public void AddRazorComponents_RegistersServices()
{
// Arrange
var services = new ServiceCollection();
+ services.AddSingleton(new ConfigurationBuilder().AddInMemoryCollection().Build());
// Act
RazorComponentsServiceCollectionExtensions.AddRazorComponents(services);
@@ -40,6 +41,7 @@ public void AddRazorComponentsTwice_DoesNotDuplicateServices()
{
// Arrange
var services = new ServiceCollection();
+ services.AddSingleton(new ConfigurationBuilder().AddInMemoryCollection().Build());
// Act
RazorComponentsServiceCollectionExtensions.AddRazorComponents(services);
diff --git a/src/Components/Forms/src/FieldIdentifier.cs b/src/Components/Forms/src/FieldIdentifier.cs
index 5422554b1a6f..3c147a02841f 100644
--- a/src/Components/Forms/src/FieldIdentifier.cs
+++ b/src/Components/Forms/src/FieldIdentifier.cs
@@ -1,8 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
+using Microsoft.AspNetCore.Components.HotReload;
namespace Microsoft.AspNetCore.Components.Forms;
@@ -12,6 +15,13 @@ namespace Microsoft.AspNetCore.Components.Forms;
///
public readonly struct FieldIdentifier : IEquatable
{
+ private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), Func