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> _fieldAccessors = new(); + + static FieldIdentifier() + { + HotReloadManager.Default.OnDeltaApplied += ClearCache; + } + /// /// Initializes a new instance of the structure. /// @@ -100,32 +110,30 @@ private static void ParseAccessor(Expression> accessor, out object mo fieldName = memberExpression.Member.Name; // Get a reference to the model object // i.e., given a value like "(something).MemberName", determine the runtime value of "(something)", - if (memberExpression.Expression is ConstantExpression constantExpression) + switch (memberExpression.Expression) { - if (constantExpression.Value is null) - { + case ConstantExpression constant when constant.Value == null: throw new ArgumentException("The provided expression must evaluate to a non-null value."); - } - model = constantExpression.Value; - } - else if (memberExpression.Expression != null) - { - // It would be great to cache this somehow, but it's unclear there's a reasonable way to do - // so, given that it embeds captured values such as "this". We could consider special-casing - // for "() => something.Member" and building a cache keyed by "something.GetType()" with values - // of type Func so we can cheaply map from "something" to "something.Member". - var modelLambda = Expression.Lambda(memberExpression.Expression); - var modelLambdaCompiled = (Func)modelLambda.Compile(); - var result = modelLambdaCompiled(); - if (result is null) - { - throw new ArgumentException("The provided expression must evaluate to a non-null value."); - } - model = result; - } - else - { - throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object."); + case ConstantExpression constant when constant.Value != null: + model = constant.Value; + break; + case MemberExpression member when member.Expression is ConstantExpression: + model = GetModelFromMemberAccess(member); + break; + case not null: + // It would be great to cache this somehow, but it's unclear there's a reasonable way to do + // so, given that it embeds captured values such as "this". We could consider special-casing + // for "() => something.Member" and building a cache keyed by "something.GetType()" with values + // of type Func so we can cheaply map from "something" to "something.Member". + var modelLambda = Expression.Lambda(memberExpression.Expression); + var modelLambdaCompiled = (Func)modelLambda.Compile(); + var result = modelLambdaCompiled() ?? + throw new ArgumentException("The provided expression must evaluate to a non-null value."); + + model = result; + break; + default: + throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object."); } break; case MethodCallExpression methodCallExpression when ExpressionFormatter.IsSingleArgumentIndexer(accessorBody): @@ -141,6 +149,53 @@ private static void ParseAccessor(Expression> accessor, out object mo } } + internal static object GetModelFromMemberAccess( + MemberExpression member, + ConcurrentDictionary<(Type ModelType, string FieldName), Func>? cache = null) + { + cache ??= _fieldAccessors; + Func? accessor = null; + object? value = null; + switch (member.Expression) + { + case ConstantExpression model: + value = model.Value ?? throw new ArgumentException("The provided expression must evaluate to a non-null value."); + accessor = cache.GetOrAdd((value.GetType(), member.Member.Name), CreateAccessor); + break; + default: + break; + } + + if (accessor == null) + { + throw new InvalidOperationException($"Unable to compile expression: {member}"); + } + + if (value == null) + { + throw new ArgumentException("The provided expression must evaluate to a non-null value."); + } + + var result = accessor(value); + return result; + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Application code does not get trimmed. We expect the members in the expression to not be trimmed.")] + static Func CreateAccessor((Type model, string member) arg) + { + var parameter = Expression.Parameter(typeof(object), "value"); + Expression expression = Expression.Convert(parameter, arg.model); + expression = Expression.PropertyOrField(expression, arg.member); + expression = Expression.Convert(expression, typeof(object)); + var lambda = Expression.Lambda>(expression, parameter); + + var func = lambda.Compile(); + return func; + } + } + private static object GetModelFromIndexer(Expression methodCallExpression) { object model; @@ -154,4 +209,9 @@ private static object GetModelFromIndexer(Expression methodCallExpression) model = result; return model; } + + private static void ClearCache() + { + _fieldAccessors.Clear(); + } } diff --git a/src/Components/Forms/src/Microsoft.AspNetCore.Components.Forms.csproj b/src/Components/Forms/src/Microsoft.AspNetCore.Components.Forms.csproj index c66e19f1252f..654c4ac2d3fe 100644 --- a/src/Components/Forms/src/Microsoft.AspNetCore.Components.Forms.csproj +++ b/src/Components/Forms/src/Microsoft.AspNetCore.Components.Forms.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Components/Forms/test/ExpressionFormatterTest.cs b/src/Components/Forms/test/ExpressionFormatterTest.cs index 016cc2bbd3d4..9f53bc41fa60 100644 --- a/src/Components/Forms/test/ExpressionFormatterTest.cs +++ b/src/Components/Forms/test/ExpressionFormatterTest.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 System.Runtime.Serialization; + namespace Microsoft.AspNetCore.Components.Forms; public sealed class ExpressionFormatterTest : IDisposable @@ -18,6 +20,19 @@ public void Works_MemberAccessOnly() Assert.Equal("person.Parent.Name", result); } + [Fact] + public void Works_MemberAccessDataAnnotation() + { + // Arrange + var person = new Person(); + + // Act + var result = ExpressionFormatter.FormatLambda(() => person.Region); + + // Assert + Assert.Equal("person.Country", result); + } + [Fact] public void Works_MemberAccessWithConstIndex() { @@ -167,6 +182,9 @@ private class Person public int Age { get; init; } + [DataMember(Name = "Country")] + public string Region { get; set; } + public Person Parent { get; init; } public Person[] Children { get; init; } = Array.Empty(); diff --git a/src/Components/Forms/test/FieldIdentifierTest.cs b/src/Components/Forms/test/FieldIdentifierTest.cs index d273fab86445..0f4b0c903576 100644 --- a/src/Components/Forms/test/FieldIdentifierTest.cs +++ b/src/Components/Forms/test/FieldIdentifierTest.cs @@ -1,6 +1,7 @@ // 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.Linq.Expressions; namespace Microsoft.AspNetCore.Components.Forms; @@ -137,6 +138,25 @@ public void CanCreateFromExpression_Property() Assert.Equal(nameof(model.StringProperty), fieldIdentifier.FieldName); } + [Fact] + public void CanCreateFromExpression_PropertyUsesCache() + { + var models = new TestModel[] { new TestModel(), new TestModel() }; + var cache = new ConcurrentDictionary<(Type ModelType, string FieldName), Func>(); + var result = new TestModel[2]; + for (var i = 0; i < models.Length; i++) + { + var model = models[i]; + LambdaExpression expression = () => model.StringProperty; + var body = expression.Body as MemberExpression; + var value = FieldIdentifier.GetModelFromMemberAccess((MemberExpression)body.Expression, cache); + result[i] = Assert.IsType(value); + } + + Assert.Single(cache); + Assert.Equal(models, result); + } + [Fact] public void CannotCreateFromExpression_NonMember() { diff --git a/src/Components/Shared/src/ExpressionFormatting/ExpressionFormatter.cs b/src/Components/Shared/src/ExpressionFormatting/ExpressionFormatter.cs index 38243e07f660..73eea91e128d 100644 --- a/src/Components/Shared/src/ExpressionFormatting/ExpressionFormatter.cs +++ b/src/Components/Shared/src/ExpressionFormatting/ExpressionFormatter.cs @@ -5,11 +5,18 @@ using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; +using System.Runtime.Serialization; +using Microsoft.AspNetCore.Components.HotReload; namespace Microsoft.AspNetCore.Components.Forms; internal static class ExpressionFormatter { + static ExpressionFormatter() + { + HotReloadManager.Default.OnDeltaApplied += ClearCache; + } + internal const int StackAllocBufferSize = 128; private delegate void CapturedValueFormatter(object closure, ref ReverseStringBuilder builder); @@ -105,7 +112,7 @@ public static string FormatLambda(LambdaExpression expression, string? prefix = wasLastExpressionMemberAccess = true; wasLastExpressionIndexer = false; - var name = memberExpression.Member.Name; + var name = memberExpression.Member.GetCustomAttribute()?.Name ?? memberExpression.Member.Name; builder.InsertFront(name); break; diff --git a/src/Components/Components/src/HotReload/HotReloadManager.cs b/src/Components/Shared/src/HotReloadManager.cs similarity index 100% rename from src/Components/Components/src/HotReload/HotReloadManager.cs rename to src/Components/Shared/src/HotReloadManager.cs diff --git a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs index e61914964bec..466ce5cf9c1c 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs @@ -57,6 +57,50 @@ public void CanBindParameterToTheDefaultForm(bool suppressEnhancedNavigation) DispatchToFormCore(dispatchToForm); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DataAnnotationsWorkForForms(bool suppressEnhancedNavigation) + { + var dispatchToForm = new DispatchToForm(this) + { + Url = "forms/default-form-bound-parameter-annotations", + FormCssSelector = "form", + InputFieldId = "Parameter.FirstName", + InputFieldCssSelector = "input[name='Parameter.FirstName']", + InputFieldValue = "John", + SuppressEnhancedNavigation = suppressEnhancedNavigation, + ErrorSelector = "ul.validation-errors li.validation-message", + AssertErrors = errors => + { + var error = Assert.Single(errors); + Assert.Equal("Name is too long", error.Text); + Assert.Equal("John", Browser.FindElement(By.CssSelector("input[name='Parameter.FirstName']")).GetAttribute("value")); + }, + }; + DispatchToFormCore(dispatchToForm); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DataContractAttributesWorkForForms(bool suppressEnhancedNavigation) + { + var dispatchToForm = new DispatchToForm(this) + { + Url = "forms/default-form-bound-parameter-annotations", + FormCssSelector = "form", + InputFieldId = "Parameter.FirstName", + InputFieldCssSelector = "input[name='Parameter.FirstName']", + InputFieldValue = "Jon", + SuppressEnhancedNavigation = suppressEnhancedNavigation + }; + DispatchToFormCore(dispatchToForm); + + var text = Browser.Exists(By.Id("pass-id")).Text; + Assert.Equal("0", text); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundParameterAnnotations.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundParameterAnnotations.razor new file mode 100644 index 000000000000..553ec45b3b5a --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundParameterAnnotations.razor @@ -0,0 +1,45 @@ +@page "/forms/default-form-bound-parameter-annotations" +@using Microsoft.AspNetCore.Components.Forms +@using System.ComponentModel.DataAnnotations; +@using System.Runtime.Serialization; + +

Default form with bound parameter using data annotations for validation and data member to tweak the binding.

+ + + + +

+ +

+ + +
+ +@if (_submitted) +{ +

Hello @Parameter.Name!

+

@Parameter.Id

+} + +@code { + bool _submitted = false; + + [SupplyParameterFromForm] public Customer Parameter { get; set; } + + protected override void OnInitialized() + { + Parameter ??= new(); + } + + public class Customer + { + [IgnoreDataMember] + public int Id { get; set; } + + [StringLength(3, ErrorMessage = "Name is too long")] + [DataMember(Name = "FirstName")] + public string Name { get; set; } + } +} diff --git a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs index 080c9c8f8026..b547292c02cb 100644 --- a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs +++ b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs @@ -12,7 +12,7 @@ using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; -using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metrics; using Moq; namespace Microsoft.AspNetCore.Hosting.Tests; diff --git a/src/Hosting/Hosting/test/HostingMetricsTests.cs b/src/Hosting/Hosting/test/HostingMetricsTests.cs index 18cd4f54c54a..ad479b6aa665 100644 --- a/src/Hosting/Hosting/test/HostingMetricsTests.cs +++ b/src/Hosting/Hosting/test/HostingMetricsTests.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metrics; namespace Microsoft.AspNetCore.Hosting.Tests; diff --git a/src/Http/Routing/test/UnitTests/RoutingMetricsTests.cs b/src/Http/Routing/test/UnitTests/RoutingMetricsTests.cs index 74eb9e4d391f..77aff5341c73 100644 --- a/src/Http/Routing/test/UnitTests/RoutingMetricsTests.cs +++ b/src/Http/Routing/test/UnitTests/RoutingMetricsTests.cs @@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metrics; using Moq; namespace Microsoft.AspNetCore.Routing; diff --git a/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs index 92e456adcb38..06e202c67af2 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs @@ -17,7 +17,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metrics; namespace Microsoft.AspNetCore.Diagnostics; diff --git a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs index fd0c791cd287..88c55e7a517d 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs @@ -19,7 +19,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metrics; using Moq; namespace Microsoft.AspNetCore.Diagnostics; diff --git a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs index 93431bee80ef..25358538354f 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs @@ -14,7 +14,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; -using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metrics; namespace Microsoft.AspNetCore.Diagnostics; diff --git a/src/Middleware/RateLimiting/test/RateLimitingMetricsTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMetricsTests.cs index 44a664338d8d..480f57f085b3 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingMetricsTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingMetricsTests.cs @@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metrics; using Moq; namespace Microsoft.AspNetCore.RateLimiting; diff --git a/src/Mvc/Mvc/test/MvcServiceCollectionExtensionsTest.cs b/src/Mvc/Mvc/test/MvcServiceCollectionExtensionsTest.cs index 3520e30a01ee..9699de27e25d 100644 --- a/src/Mvc/Mvc/test/MvcServiceCollectionExtensionsTest.cs +++ b/src/Mvc/Mvc/test/MvcServiceCollectionExtensionsTest.cs @@ -162,6 +162,7 @@ public void AddMvc_Twice_DoesNotAddDuplicates() { // Arrange var services = new ServiceCollection(); + services.AddSingleton(new ConfigurationBuilder().AddInMemoryCollection().Build()); services.AddLogging(); services.AddSingleton(GetHostingEnvironment()); @@ -178,6 +179,7 @@ public void AddControllersAddRazorPages_Twice_DoesNotAddDuplicates() { // Arrange var services = new ServiceCollection(); + services.AddSingleton(new ConfigurationBuilder().AddInMemoryCollection().Build()); services.AddLogging(); services.AddSingleton(GetHostingEnvironment()); @@ -196,6 +198,7 @@ public void AddControllersWithViews_Twice_DoesNotAddDuplicates() { // Arrange var services = new ServiceCollection(); + services.AddSingleton(new ConfigurationBuilder().AddInMemoryCollection().Build()); services.AddLogging(); services.AddSingleton(GetHostingEnvironment()); @@ -212,6 +215,7 @@ public void AddRazorPages_Twice_DoesNotAddDuplicates() { // Arrange var services = new ServiceCollection(); + services.AddSingleton(new ConfigurationBuilder().AddInMemoryCollection().Build()); services.AddLogging(); services.AddSingleton(GetHostingEnvironment()); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionLimitTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionLimitTests.cs index 03751ed317f0..0f407106a6dc 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionLimitTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionLimitTests.cs @@ -16,7 +16,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Tests; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Diagnostics.Metrics; -using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metrics; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs index 9639aec3d667..617d6974611c 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs @@ -16,7 +16,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metrics; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http2/Http2RequestTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http2/Http2RequestTests.cs index a1417c32bed7..b6cec58fd34d 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http2/Http2RequestTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http2/Http2RequestTests.cs @@ -16,7 +16,7 @@ using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metrics; namespace Interop.FunctionalTests.Http2; diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs index 86cf5a0891a1..f13a4a7e6fd4 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs @@ -24,7 +24,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; -using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metrics; using Xunit; namespace Interop.FunctionalTests.Http3; diff --git a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs index f13130fb1810..dae4df9c7c83 100644 --- a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs +++ b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs @@ -41,7 +41,7 @@ using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; -using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metrics; using Microsoft.IdentityModel.Tokens; using Moq; using Newtonsoft.Json;