Skip to content

Commit 878f68c

Browse files
committed
Binding primitive types
1 parent cfbeda2 commit 878f68c

29 files changed

+326
-37
lines changed

src/Components/Authorization/test/AuthorizeRouteViewTest.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,11 @@ public bool CanBind(string formName, Type valueType)
478478
return false;
479479
}
480480

481+
public bool CanConvertSingleValue(Type type)
482+
{
483+
return false;
484+
}
485+
481486
public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue)
482487
{
483488
boundValue = null;

src/Components/Components/src/Binding/CascadingModelBinder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ internal void UpdateBindingInformation(string url)
114114
var bindingContext = _bindingContext != null &&
115115
string.Equals(_bindingContext.Name, name, StringComparison.Ordinal) &&
116116
string.Equals(_bindingContext.BindingContextId, bindingId, StringComparison.Ordinal) ?
117-
_bindingContext : new ModelBindingContext(name, bindingId);
117+
_bindingContext : new ModelBindingContext(name, bindingId, FormValueSupplier.CanConvertSingleValue);
118118

119119
// It doesn't matter that we don't check IsFixed, since the CascadingValue we are setting up will throw if the app changes.
120120
if (IsFixed && _bindingContext != null && _bindingContext != bindingContext)

src/Components/Components/src/Binding/IFormValueSupplier.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ public interface IFormValueSupplier
1818
/// <returns><c>true</c> if the value type can be bound; otherwise, <c>false</c>.</returns>
1919
bool CanBind(string formName, Type valueType);
2020

21+
/// <summary>
22+
/// Determines whether a given <see cref="Type"/> can be converted from a single string value.
23+
/// For example, strings, numbers, boolean values, enums, guids, etc. fall in this category.
24+
/// </summary>
25+
/// <param name="type">The <see cref="Type"/> to check.</param>
26+
/// <returns><c>true</c> if the type can be converted from a single string value; otherwise, <c>false</c>.</returns>
27+
bool CanConvertSingleValue(Type type);
28+
2129
/// <summary>
2230
/// Tries to bind the form with the specified name to a value of the specified type.
2331
/// </summary>

src/Components/Components/src/Binding/ModelBindingContext.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ namespace Microsoft.AspNetCore.Components;
88
/// </summary>
99
public sealed class ModelBindingContext
1010
{
11-
internal ModelBindingContext(string name, string bindingContextId)
11+
private readonly Predicate<Type> _canBind;
12+
13+
internal ModelBindingContext(string name, string bindingContextId, Predicate<Type> canBind)
1214
{
1315
ArgumentNullException.ThrowIfNull(name);
1416
ArgumentNullException.ThrowIfNull(bindingContextId);
17+
ArgumentNullException.ThrowIfNull(canBind);
1518
// We are initializing the root context, that can be a "named" root context, or the default context.
1619
// A named root context only provides a name, and that acts as the BindingId
1720
// A "default" root context does not provide a name, and instead it provides an explicit Binding ID.
@@ -23,6 +26,7 @@ internal ModelBindingContext(string name, string bindingContextId)
2326

2427
Name = name;
2528
BindingContextId = bindingContextId ?? name;
29+
_canBind = canBind;
2630
}
2731

2832
/// <summary>
@@ -37,4 +41,9 @@ internal ModelBindingContext(string name, string bindingContextId)
3741

3842
internal static string Combine(ModelBindingContext? parentContext, string name) =>
3943
string.IsNullOrEmpty(parentContext?.Name) ? name : $"{parentContext.Name}.{name}";
44+
45+
internal bool CanConvert(Type type)
46+
{
47+
return _canBind(type);
48+
}
4049
}

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
abstract Microsoft.AspNetCore.Components.RenderModeAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode!
33
Microsoft.AspNetCore.Components.Binding.IFormValueSupplier
44
Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.CanBind(string! formName, System.Type! valueType) -> bool
5+
Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.CanConvertSingleValue(System.Type! type) -> bool
56
Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.TryBind(string! formName, System.Type! valueType, out object? boundValue) -> bool
67
Microsoft.AspNetCore.Components.CascadingModelBinder
78
Microsoft.AspNetCore.Components.CascadingModelBinder.CascadingModelBinder() -> void

src/Components/Components/test/CascadingModelBinderTest.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,11 @@ public bool CanBind(string formName, Type valueType)
338338
return false;
339339
}
340340

341+
public bool CanConvertSingleValue(Type type)
342+
{
343+
return false;
344+
}
345+
341346
public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue)
342347
{
343348
boundValue = null;

src/Components/Components/test/CascadingParameterStateTest.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,11 @@ public bool CanBind(string formName, Type valueType)
535535
valueType == ValueType;
536536
}
537537

538+
public bool CanConvertSingleValue(Type type)
539+
{
540+
return type == ValueType;
541+
}
542+
538543
public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue)
539544
{
540545
boundValue = BoundValue;

src/Components/Components/test/ModelBindingContextTest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,30 @@ public class ModelBindingContextTest
88
[Fact]
99
public void CanCreate_BindingContext_WithDefaultName()
1010
{
11-
var context = new ModelBindingContext("", "");
11+
var context = new ModelBindingContext("", "", t => true);
1212
Assert.Equal("", context.Name);
1313
Assert.Equal("", context.BindingContextId);
1414
}
1515

1616
[Fact]
1717
public void CanCreate_BindingContext_WithName()
1818
{
19-
var context = new ModelBindingContext("name", "path?handler=name");
19+
var context = new ModelBindingContext("name", "path?handler=name", t => true);
2020
Assert.Equal("name", context.Name);
2121
Assert.Equal("path?handler=name", context.BindingContextId);
2222
}
2323

2424
[Fact]
2525
public void Throws_WhenNameIsProvided_AndNoBindingContextId()
2626
{
27-
var exception = Assert.Throws<InvalidOperationException>(() => new ModelBindingContext("name", ""));
27+
var exception = Assert.Throws<InvalidOperationException>(() => new ModelBindingContext("name", "", t => true));
2828
Assert.Equal("A root binding context needs to provide a name and explicit binding context id or none.", exception.Message);
2929
}
3030

3131
[Fact]
3232
public void Throws_WhenBindingContextId_IsProvidedForDefaultName()
3333
{
34-
var exception = Assert.Throws<InvalidOperationException>(() => new ModelBindingContext("", "context"));
34+
var exception = Assert.Throws<InvalidOperationException>(() => new ModelBindingContext("", "context", t => true));
3535
Assert.Equal("A root binding context needs to provide a name and explicit binding context id or none.", exception.Message);
3636
}
3737
}

src/Components/Components/test/RouteViewTest.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ public bool CanBind(string formName, Type valueType)
248248
return false;
249249
}
250250

251+
public bool CanConvertSingleValue(Type type)
252+
{
253+
return false;
254+
}
255+
251256
public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue)
252257
{
253258
boundValue = null;
Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Concurrent;
45
using System.Diagnostics.CodeAnalysis;
6+
using System.Globalization;
7+
using System.Reflection;
58
using Microsoft.AspNetCore.Components.Binding;
9+
using Microsoft.AspNetCore.Components.Endpoints.Binding;
610
using Microsoft.AspNetCore.Components.Forms;
11+
using Microsoft.Extensions.Primitives;
712

813
namespace Microsoft.AspNetCore.Components.Endpoints;
914

1015
internal class DefaultFormValuesSupplier : IFormValueSupplier
1116
{
17+
private static readonly MethodInfo _method = typeof(DefaultFormValuesSupplier)
18+
.GetMethod(
19+
nameof(DeserializeCore),
20+
BindingFlags.NonPublic | BindingFlags.Static) ??
21+
throw new InvalidOperationException($"Unable to find method '{nameof(DeserializeCore)}'.");
22+
1223
private readonly FormDataProvider _formData;
24+
private readonly FormDataSerializerOptions _options = new();
25+
private static readonly ConcurrentDictionary<Type, Func<IReadOnlyDictionary<string, StringValues>, FormDataSerializerOptions, string, object>> _cache =
26+
new();
1327

1428
public DefaultFormValuesSupplier(FormDataProvider formData)
1529
{
@@ -20,33 +34,47 @@ public bool CanBind(string formName, Type valueType)
2034
{
2135
return _formData.IsFormDataAvailable &&
2236
string.Equals(formName, _formData.Name, StringComparison.Ordinal) &&
23-
valueType == typeof(string);
37+
_options.HasConverter(valueType);
2438
}
2539

2640
public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object? boundValue)
2741
{
28-
// This will delegate to a proper binder
42+
// This will func to a proper binder
2943
if (!CanBind(formName, valueType))
3044
{
3145
boundValue = null;
3246
return false;
3347
}
3448

35-
if (!_formData.Entries.TryGetValue("value", out var rawValue) || rawValue.Count != 1)
36-
{
37-
boundValue = null;
38-
return false;
39-
}
40-
41-
var valueAsString = rawValue.ToString();
49+
var deserializer = _cache.GetOrAdd(valueType, CreateDeserializer);
4250

43-
if (valueType == typeof(string))
51+
var result = deserializer(_formData.Entries, _options, "value");
52+
if (result != default)
4453
{
45-
boundValue = valueAsString;
54+
// This is not correct, but works for primtive values.
55+
// Will change the interface when we add support for complex types.
56+
boundValue = result;
4657
return true;
4758
}
4859

49-
boundValue = null;
60+
boundValue = valueType.IsValueType ? Activator.CreateInstance(valueType) : null;
5061
return false;
5162
}
63+
64+
private Func<IReadOnlyDictionary<string, StringValues>, FormDataSerializerOptions, string, object> CreateDeserializer(Type type) =>
65+
_method.MakeGenericMethod(type)
66+
.CreateDelegate<Func<IReadOnlyDictionary<string, StringValues>, FormDataSerializerOptions, string, object>>();
67+
68+
private static object? DeserializeCore<T>(IReadOnlyDictionary<string, StringValues> form, FormDataSerializerOptions options, string value)
69+
{
70+
// Culture needs to come from the request.
71+
var reader = new FormDataReader(form, CultureInfo.CurrentCulture);
72+
reader.PushPrefix(value);
73+
return FormDataDeserializer.Deserialize<T>(reader, options);
74+
}
75+
76+
public bool CanConvertSingleValue(Type type)
77+
{
78+
return _options.IsSingleValueConverter(type);
79+
}
5280
}

0 commit comments

Comments
 (0)