Expose some internal Blazor APIs to allow customization for form and querystring deserialization
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 290
Description
## Background and Motivation
Over the last couple of months I've logged a couple of bugs with the form / query binding in Blazor:
* #66607
* #65010
* #65002
And now I'm having another issue I'm struggling with, and that's the fact that `ParsableConverter` calls `TryParse` with `reader.Culture`, and when `FormDataReader` is instantiated it's hard coded to use `CultureInfo.InvariantCulture` as the culture. This is a problem for me because my website is for New Zealand and Australia and so all the date inputs format, so `InvariantCulture` will incorrectly deserialize the `DateTime` values.
In MVC there's similar issues, but I can create a custom implementation of `IModelBinderProvider` so I can fix it, but the relevant types here are all internal, like `FormDataConverter`, `FormDataConverter` , `ISingleValueConverter` and `FormDataMapperOptions`. If they where public I could create my own implementation and configure `FormDataMapperOptions` to add them.
Another approach I tried was to create my own implementation of `SupplyParameterFromFormAttribute`. From there there's lots of approaches I could take, possibly something crazy like wrapping some calls to `IModelBinderFactory` so my MVC and Blazor code could share serialization behavior, but that's not possible either because `ICascadingValueSupplier` is also internal.
## Proposed API
So I'd like to propose making `ICascadingValueSupplier` public, and either making `FormDataConverter` and `ISingleValueConverter` public, or creating a new interface to implement so `FormDataReader` doesn't need to be exposed publicly also.
## Usage Examples
### Custom FormDataConverter
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure(options =>
{
options.Converters[typeof(DateTime)] = new CustomDateTimeConverter();
});
internal class CustomDateTimeConverter : FormDataConverter, ISingleValueConverter
{
private static CultureInfo NZCulture = new CultureInfo("en-NZ");
public bool CanConvertSingleValue() => true;
public bool TryConvertValue(ref FormDataReader reader, string value, out DateTime result)
{
if (DateTime.TryParse(value, NZCulture.DateTimeFormat, out result!))
return true;
else
return false;
}
internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMapperOptions options, out DateTime result, out bool found)
{
found = reader.TryGetValue(out var value);
if (!found)
{
result = default;
return true;
}
else
{
return TryConvertValue(ref reader, value!, out result!);
}
}
}
```
### Custom SupplyParameterFromFormAttribute
``` csharp
public static class CustomSupplyParameterFromFormServiceCollectionExtensions
{
public static IServiceCollection AddCustomSupplyValueFromFormProvider(this IServiceCollection serviceCollection)
{
serviceCollection.AddScoped();
return serviceCollection;
}
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class CustomSupplyParameterFromFormAttribute : CascadingParameterAttributeBase
{
}
internal class CustomSupplyParameterFromFormValueProvider : ICascadingValueSupplier
{
private readonly IModelMetadataProvider _metadataProvider;
private readonly FormMappingContext _mappingContext;
private readonly IModelBinderFactory _modelBinderFactory;
private readonly IFormValueMapper? _formValueMapper;
public CustomSupplyParameterFromFormValueProvider(IFormValueMapper? formValueMapper, IModelMetadataProvider metadataProvider, IModelBinderFactory modelBinderFactory)
{
_metadataProvider = metadataProvider;
_mappingContext = new FormMappingContext("");
_modelBinderFactory = modelBinderFactory;
_formValueMapper = formValueMapper;
}
bool ICascadingValueSupplier.IsFixed => true;
public bool CanSupplyValue(in CascadingParameterInfo parameterInfo)
{
if (_formValueMapper is not null && parameterInfo.Attribute is CustomSupplyParameterFromFormAttribute)
return _formValueMapper.CanMap(parameterInfo.PropertyType, "", "");
return false;
}
public object? GetCurrentValue(object? key, in CascadingParameterInfo parameterInfo)
{
var modelMetadata = _metadataProvider.GetMetadataForType(parameterInfo.PropertyType);
var valueProvider = new FormMappingContextValueProvider(_mappingContext);
var actionContext = new ActionContext()
{
HttpContext = new DefaultHttpContext(),
RouteData = new Microsoft.AspNetCore.Routing.RouteData(),
ActionDescriptor = new Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor()
};
var modelBindingContext = DefaultModelBindingContext.CreateBindingContext(
actionContext,
valueProvider,
modelMetadata,
bindingInfo: null,
modelName: string.Empty);
var result = modelMetadata.ModelType.IsValueType ? Activator.CreateInstance(modelMetadata.ModelType) : null;
modelBindingContext.Model = result;
var factoryContext = new ModelBinderFactoryContext()
{
Metadata = modelMetadata,
BindingInfo = new BindingInfo()
{
BinderModelName = modelMetadata.BinderModelName,
BinderType = modelMetadata.BinderType,
BindingSource = modelMetadata.BindingSource,
PropertyFilterProvider = modelMetadata.PropertyFilterProvider,
},
CacheToken = modelMetadata,
};
var binder = _modelBinderFactory.CreateBinder(factoryContext);
binder.BindModelAsync(modelBindingContext).Wait();
if (modelBindingContext.ModelState.Count > 0)
return result;
return default;
}
void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
=> throw new NotSupportedException(); // IsFixed = true, so the framework won't call this
void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
=> throw new NotSupportedException(); // IsFixed = true, so the framework won't call this
}
```
## Alternative Designs
I think there's a lot of ways that these could be done, new interfaces so not so many internals are exposed.
## Risks
I'm unsure about how the internals of the form deserialization full works, especially when enhanced navigation is involved, so exposing these things might be much more complicated than I think, and it could be too complex to allow people to create their own implantations that don't break things entirely
Contributor guide
Assessment
This issue has not been assessed yet.