Skip to content

Consider adding support for TryParse as a way to bind primitives #39682

@brunolins16

Description

@brunolins16

Current design

Types that do not contain a Converter from string available are considered Complex Types.

IsComplexType = !TypeDescriptor.GetConverter(ModelType).CanConvertFrom(typeof(string));

That means they will be bind using the ComplexObjectModelBinder when they are not special types that have your own binder (Eg. CancellationToken or Dictionary) or when the [FromBody] was not inferred, in this case the BodyModelBinder will be used.

On the other hand, when it is detected as a simple type (can be converted from string) the SimpleTypeModelBinder will be used, except for the types listed below:

  • DateTimeModelBinder => context.Metadata.UnderlyingOrModelType == typeof(DateTime)
  • DecimalModelBinder => modelType == typeof(decimal)
  • DoubleModelBinder => modelType == typeof(double)
  • FloatModelBinder => modelType == typeof(float)
  • EnumTypeModelBinder => context.Metadata.IsEnum

Today, this binder will basically use the actual value if the model type is string or call the converter when not.

Proposed Change

There is already the class Microsoft.AspNetCore.Http.ParameterBindingMethodCache exposing two methods HasTryParseMethod and FindTryParseMethod that will be used in this proposal, however, currently it works only with InvariantCulture instead of allowing the caller to provide a different Culture what is required in this proposal and will require a small change to this class.

With the capability to detect the TryParse method, the proposal is change ModelMetadata to include a new internal property HasTryParse, that will indicate that a TryParse method is available with the type.

namespace Microsoft.AspNetCore.Mvc.ModelBinding;

public abstract class ModelMetadata : IEquatable<ModelMetadata?>, IModelMetadataProvider
{
    internal bool HasTryParse { get; private set; }
}

An important aspect of this change, the ModelMetadata will hold a static instance of the Microsoft.AspNetCore.Http.ParameterBindingMethodCache that will be used across all calls.

With this new property available the proposal is to add a new public available ModelBinderProvider that will create a ModelBinder only when ModelMetadata.HasTryParse == true

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders;

+ public class TryParseModelBinderProvider : IModelBinderProvider
+ {
+     public IModelBinder? GetBinder(ModelBinderProviderContext context!!)
+     { 
+         if (context.Metadata.HasTryParse)
+         {
+             var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
+             return new TryParseModelBinder(context.Metadata.ModelType, loggerFactory);
+         }
 
+         return null;
+     }
+ }
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders;

+ public class TryParseModelBinder : IModelBinder
+ {
+     private readonly Func<ParameterExpression, IFormatProvider, Expression> _tryParseMethodExpession;
+     private readonly ILogger _logger;

+     public TryParseModelBinder(Type type!!, ILoggerFactory loggerFactory!!)
+     {
+         _tryParseMethodExpession = ModelMetadata.FindTryParseMethod(type)!;
+         _logger = loggerFactory.CreateLogger<SimpleTypeModelBinder>();
+     }
+ }

This new provider will be added to the list of the default ModelProviders and executed before the SimpleTypeModelBinderProvider. That means that for all types that contains a TryParse and was processed previously by the SimpleTypeModelBinderProvider because it has a Converter from string will now be bound using the new TryParseModelBinder. A very common example will be int type.

Also, this proposal will not change how types that have a TryParse method but not a Converter from string have their Binding Source inferred, so, for those types of the parameter will need to be explicitly defined the binding source, eg: FromQuery.

Usage Examples

The new behavior will be enabled without any code change, however, the following piece of code will rollback to the previous behavior if the users want to.

services.Configure<MvcOptions>(options => {
    options.ModelBinderProviders.RemoveType<TryParseModelBinderProvider>();
});

Metadata

Metadata

Assignees

Labels

breaking-changeThis issue / pr will introduce a breaking change, when resolved / merged.feature-model-bindingold-area-web-frameworks-do-not-use*DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions