-
Notifications
You must be signed in to change notification settings - Fork 10.3k
[Blazor] Support for primitive types in [SupplyParameterFromForm] #48432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
4fb1959
Binding primitive types
javiercn 30f2e9d
Binding factories and tests
javiercn 0de6ad6
Fix warning
javiercn d8b3680
Fix tests
javiercn 07f44c5
Custom parsable and nullable
javiercn 6f8d352
Fix build
javiercn 9073c56
Cleanups
javiercn ebcd8f4
Cleanups
javiercn ee67e2a
Cleanups
javiercn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
src/Components/Endpoints/src/Binding/Converters/NullableConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// 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.Binding; | ||
|
||
internal sealed class NullableConverter<T> : FormDataConverter<T?> where T : struct | ||
{ | ||
private readonly FormDataConverter<T> _nonNullableConverter; | ||
|
||
public NullableConverter(FormDataConverter<T> nonNullableConverter) | ||
{ | ||
_nonNullableConverter = nonNullableConverter; | ||
} | ||
|
||
internal override bool TryRead(ref FormDataReader context, Type type, FormDataMapperOptions options, out T? result, out bool found) | ||
{ | ||
if (!(_nonNullableConverter.TryRead(ref context, type, options, out var innerResult, out found) && found)) | ||
{ | ||
result = null; | ||
return false; | ||
} | ||
else | ||
{ | ||
result = innerResult; | ||
return true; | ||
} | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
src/Components/Endpoints/src/Binding/Converters/ParsableConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// 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.Binding; | ||
|
||
internal sealed class ParsableConverter<T> : FormDataConverter<T>, ISingleValueConverter where T : IParsable<T> | ||
{ | ||
internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMapperOptions options, out T? result, out bool found) | ||
{ | ||
found = reader.TryGetValue(out var value); | ||
if (found && T.TryParse(value, reader.Culture, out result)) | ||
{ | ||
return true; | ||
} | ||
else | ||
{ | ||
result = default; | ||
return false; | ||
} | ||
} | ||
} |
60 changes: 45 additions & 15 deletions
60
src/Components/Endpoints/src/Binding/DefaultFormValuesSupplier.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,82 @@ | ||
// 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.Globalization; | ||
using System.Reflection; | ||
using Microsoft.AspNetCore.Components.Binding; | ||
using Microsoft.AspNetCore.Components.Endpoints.Binding; | ||
using Microsoft.AspNetCore.Components.Forms; | ||
using Microsoft.Extensions.Primitives; | ||
|
||
namespace Microsoft.AspNetCore.Components.Endpoints; | ||
|
||
internal class DefaultFormValuesSupplier : IFormValueSupplier | ||
internal sealed class DefaultFormValuesSupplier : IFormValueSupplier | ||
{ | ||
private readonly FormDataProvider _formData; | ||
private static readonly MethodInfo _method = typeof(DefaultFormValuesSupplier) | ||
.GetMethod( | ||
nameof(DeserializeCore), | ||
BindingFlags.NonPublic | BindingFlags.Static) ?? | ||
throw new InvalidOperationException($"Unable to find method '{nameof(DeserializeCore)}'."); | ||
|
||
private readonly HttpContextFormDataProvider _formData; | ||
private readonly FormDataMapperOptions _options = new(); | ||
private static readonly ConcurrentDictionary<Type, Func<IReadOnlyDictionary<string, StringValues>, FormDataMapperOptions, string, object>> _cache = | ||
new(); | ||
|
||
public DefaultFormValuesSupplier(FormDataProvider formData) | ||
{ | ||
_formData = formData; | ||
_formData = (HttpContextFormDataProvider)formData; | ||
} | ||
|
||
public bool CanBind(string formName, Type valueType) | ||
{ | ||
return _formData.IsFormDataAvailable && | ||
string.Equals(formName, _formData.Name, StringComparison.Ordinal) && | ||
valueType == typeof(string); | ||
_options.ResolveConverter(valueType) != null; | ||
} | ||
|
||
public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object? boundValue) | ||
{ | ||
// This will delegate to a proper binder | ||
// This will func to a proper binder | ||
if (!CanBind(formName, valueType)) | ||
{ | ||
boundValue = null; | ||
return false; | ||
} | ||
|
||
if (!_formData.Entries.TryGetValue("value", out var rawValue) || rawValue.Count != 1) | ||
{ | ||
boundValue = null; | ||
return false; | ||
} | ||
|
||
var valueAsString = rawValue.ToString(); | ||
var deserializer = _cache.GetOrAdd(valueType, CreateDeserializer); | ||
|
||
if (valueType == typeof(string)) | ||
var result = deserializer(_formData.Entries, _options, "value"); | ||
if (result != default) | ||
{ | ||
boundValue = valueAsString; | ||
// This is not correct, but works for primtive values. | ||
// Will change the interface when we add support for complex types. | ||
boundValue = result; | ||
return true; | ||
} | ||
|
||
boundValue = null; | ||
boundValue = valueType.IsValueType ? Activator.CreateInstance(valueType) : null; | ||
return false; | ||
} | ||
|
||
private Func<IReadOnlyDictionary<string, StringValues>, FormDataMapperOptions, string, object> CreateDeserializer(Type type) => | ||
_method.MakeGenericMethod(type) | ||
.CreateDelegate<Func<IReadOnlyDictionary<string, StringValues>, FormDataMapperOptions, string, object>>(); | ||
|
||
private static object? DeserializeCore<T>(IReadOnlyDictionary<string, StringValues> form, FormDataMapperOptions options, string value) | ||
{ | ||
// Form values are parsed according to the culture of the request, which is set to the current culture by the localization middleware. | ||
// Some form input types use the invariant culture when sending the data to the server. For those cases, we'll | ||
// provide a way to override the culture to use to parse that value. | ||
var reader = new FormDataReader(form, CultureInfo.CurrentCulture); | ||
reader.PushPrefix(value); | ||
return FormDataMapper.Map<T>(reader, options); | ||
} | ||
|
||
public bool CanConvertSingleValue(Type type) | ||
{ | ||
return _options.IsSingleValueConverter(type); | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
src/Components/Endpoints/src/Binding/Factories/NullableConverterFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 System.Diagnostics; | ||
|
||
namespace Microsoft.AspNetCore.Components.Endpoints.Binding; | ||
|
||
internal sealed class NullableConverterFactory : IFormDataConverterFactory | ||
{ | ||
public static readonly NullableConverterFactory Instance = new(); | ||
|
||
public bool CanConvert(Type type, FormDataMapperOptions options) | ||
{ | ||
var underlyingType = Nullable.GetUnderlyingType(type); | ||
return underlyingType != null && options.ResolveConverter(underlyingType) != null; | ||
} | ||
|
||
public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) | ||
{ | ||
var underlyingType = Nullable.GetUnderlyingType(type); | ||
Debug.Assert(underlyingType != null); | ||
|
||
var underlyingConverter = options.ResolveConverter(underlyingType); | ||
Debug.Assert(underlyingConverter != null); | ||
|
||
var expectedConverterType = typeof(NullableConverter<>).MakeGenericType(underlyingType); | ||
Debug.Assert(expectedConverterType != null); | ||
|
||
return Activator.CreateInstance(expectedConverterType, underlyingConverter) as FormDataConverter ?? | ||
throw new InvalidOperationException($"Unable to create converter for type '{type}'."); | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
src/Components/Endpoints/src/Binding/Factories/ParsableConverterFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// 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.Internal; | ||
|
||
namespace Microsoft.AspNetCore.Components.Endpoints.Binding; | ||
|
||
internal sealed class ParsableConverterFactory : IFormDataConverterFactory | ||
{ | ||
public static readonly ParsableConverterFactory Instance = new(); | ||
|
||
public bool CanConvert(Type type, FormDataMapperOptions options) | ||
{ | ||
return ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IParsable<>)) is not null; | ||
} | ||
|
||
public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) | ||
{ | ||
return Activator.CreateInstance(typeof(ParsableConverter<>).MakeGenericType(type)) as FormDataConverter ?? | ||
throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// 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.Binding; | ||
|
||
// Base type for all types that can map from form data to a .NET type. | ||
internal class FormDataConverter | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// 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.Binding; | ||
|
||
internal abstract class FormDataConverter<T> : FormDataConverter | ||
{ | ||
internal abstract bool TryRead(ref FormDataReader context, Type type, FormDataMapperOptions options, out T? result, out bool found); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is
type
ever going to be different fromtypeof(T)
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not obvious yet in this PR, but it's passed in for a different reason. When you do
typeof(T)
the generated code needs to ask the type system at runtime (this is in IL) for the type instance, which involves a lookup. For that reason, many serializers cache and pass the type instance along.If you look at System.Text.Json, they do something similar. See here
The other part is that we might still need this if in the future we want to support polymorphic binding or enable people to do so, so we can't just assume typeof(T) == type in general.