Skip to content

Instantly share code, notes, and snippets.

@AndreasAmMueller
Last active July 25, 2023 07:44
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save AndreasAmMueller/38c1a8d76ecd4450b4f75a479f3293c1 to your computer and use it in GitHub Desktop.
Save AndreasAmMueller/38c1a8d76ecd4450b4f75a479f3293c1 to your computer and use it in GitHub Desktop.
FIX: Localised number format and wrong input type for decimal fields

As Microsoft is not capable of fixing an issue, which influences most of the non-english-speaking world, here is a possible fix until they've seen the urgent priority for this to be solved.

Startup.cs:

public void ConfigureServices {
	// [...]
	
	services.AddControllersWithViews(options =>
	{
		options.ModelBinderProviders.Insert(0, new CustomFloatingPointModelBinderProvider());
		// [...]
	});
	
	// [...]
}
using System;
using System.Globalization;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// Custom floating point ModelBinder as the team of Microsoft is not capable of fixing their <see href="https://github.com/dotnet/aspnetcore/issues/6566">issue</see> with other cultures than en-US.
/// </summary>
public class CustomFloatingPointModelBinder : IModelBinder
{
private readonly NumberStyles supportedNumberStyles;
private readonly ILogger logger;
private readonly CultureInfo cultureInfo;
/// <summary>
/// Initializes a new instance of <see cref="CustomFloatingPointModelBinder"/>.
/// </summary>
/// <param name="supportedStyles">The <see cref="NumberStyles"/>.</param>
/// <param name="cultureInfo">The <see cref="CultureInfo"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public CustomFloatingPointModelBinder(NumberStyles supportedStyles, CultureInfo cultureInfo, ILoggerFactory loggerFactory)
{
if (loggerFactory == null)
throw new ArgumentNullException(nameof(loggerFactory));
this.cultureInfo = cultureInfo ?? throw new ArgumentNullException(nameof(cultureInfo));
supportedNumberStyles = supportedStyles;
logger = loggerFactory?.CreateLogger<CustomFloatingPointModelBinder>();
}
/// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
logger.AttemptingToBindModel(bindingContext);
string modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
logger.FoundNoValueInRequest(bindingContext);
// no entry
logger.DoneAttemptingToBindModel(bindingContext);
return Task.CompletedTask;
}
var modelState = bindingContext.ModelState;
modelState.SetModelValue(modelName, valueProviderResult);
var metadata = bindingContext.ModelMetadata;
var type = metadata.UnderlyingOrModelType;
try
{
string value = valueProviderResult.FirstValue;
var culture = cultureInfo ?? valueProviderResult.Culture;
object model;
if (string.IsNullOrWhiteSpace(value))
{
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty.
model = null;
}
else if (type == typeof(float))
{
model = float.Parse(value, supportedNumberStyles, culture);
}
else if (type == typeof(double))
{
model = double.Parse(value, supportedNumberStyles, culture);
}
else if (type == typeof(decimal))
{
model = decimal.Parse(value, supportedNumberStyles, culture);
}
else
{
// unreachable
throw new NotSupportedException();
}
// When converting value, a null model may indicate a failed conversion for an otherwise required
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the
// current bindingContext. If not, an error is logged.
if (model == null && !metadata.IsReferenceOrNullableType)
{
modelState.TryAddModelError(
modelName,
metadata
.ModelBindingMessageProvider
.ValueMustNotBeNullAccessor(valueProviderResult.ToString())
);
}
else
{
bindingContext.Result = ModelBindingResult.Success(model);
}
}
catch (Exception exception)
{
bool isFormatException = exception is FormatException;
if (!isFormatException && exception.InnerException != null)
{
// Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve
// this code in case a cursory review of the CoreFx code missed something.
exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
}
modelState.TryAddModelError(modelName, exception, metadata);
// Conversion failed.
}
logger.DoneAttemptingToBindModel(bindingContext);
return Task.CompletedTask;
}
}
}
using System;
using System.ComponentModel;
using System.Globalization;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// An <see cref="IModelBinderProvider"/> for binding <see cref="decimal"/>, <see cref="double"/>,
/// <see cref="float"/>, and their <see cref="Nullable{T}"/> wrappers.
/// Modified to set <see cref="NumberStyles"/> and <see cref="System.Globalization.CultureInfo"/>.
/// </summary>
/// <remarks>
/// To use this provider, insert it at the beginning of the providers list:<br/>
/// <code>
/// services.AddControllersWithViews(options =><br/>
/// {<br/>
/// options.ModelBinderProviders.Insert(0, new CustomFloatingPointModelBinderProvider());<br/>
/// });</code>
/// </remarks>
public class CustomFloatingPointModelBinderProvider : IModelBinderProvider
{
/// <summary>
/// Gets or sets the supported <see cref="NumberStyles"/> globally.
/// Default: <see cref="NumberStyles.Float"/> and <see cref="NumberStyles.AllowThousands"/>.
/// </summary>
/// <remarks>
/// <see cref="SimpleTypeModelBinder"/> uses <see cref="DecimalConverter"/> and similar. Those <see cref="TypeConverter"/>s default to <see cref="NumberStyles.Float"/>.
/// </remarks>
public static NumberStyles SupportedNumberStyles { get; set; } = NumberStyles.Float | NumberStyles.AllowThousands;
/// <summary>
/// Gets or sets the <see cref="System.Globalization.CultureInfo"/> to use while parsing globally.
/// Default: <see cref="CultureInfo.InvariantCulture"/>.
/// </summary>
public static CultureInfo CultureInfo { get; set; } = CultureInfo.InvariantCulture;
/// <inheritdoc />
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
var modelType = context.Metadata.UnderlyingOrModelType;
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
if (modelType == typeof(decimal) ||
modelType == typeof(double) ||
modelType == typeof(float))
{
return new CustomFloatingPointModelBinder(SupportedNumberStyles, CultureInfo, loggerFactory);
}
return null;
}
}
}
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
namespace Microsoft.Extensions.Logging
{
/// <summary>
/// Extensions for the <see cref="ILogger"/>.
/// </summary>
internal static class LoggerExtensions
{
// Found here:
// https://github.com/dotnet/aspnetcore/blob/a4c45262fb8549bdb4f5e4f76b16f98a795211ae/src/Mvc/Mvc.Core/src/MvcCoreLoggerExtensions.cs
public static void AttemptingToBindModel(this ILogger logger, ModelBindingContext bindingContext)
{
if (!logger.IsEnabled(LogLevel.Debug))
return;
var modelMetadata = bindingContext.ModelMetadata;
switch (modelMetadata.MetadataKind)
{
case ModelMetadataKind.Parameter:
logger.Log(LogLevel.Debug,
new EventId(44, "AttemptingToBindParameterModel"),
$"Attempting to bind parameter '{modelMetadata.ParameterName}' of type '{modelMetadata.ModelType}' using the name '{bindingContext.ModelName}' in request data ...");
break;
case ModelMetadataKind.Property:
logger.Log(LogLevel.Debug,
new EventId(13, "AttemptingToBindPropertyModel"),
$"Attempting to bind property '{modelMetadata.ContainerType}.{modelMetadata.PropertyName}' of type '{modelMetadata.ModelType}' using the name '{bindingContext.ModelName}' in request data ...");
break;
case ModelMetadataKind.Type:
logger.Log(LogLevel.Debug,
new EventId(24, "AttemptingToBindModel"),
$"Attempting to bind model of type '{bindingContext.ModelType}' using the name '{bindingContext.ModelName}' in request data ...");
break;
}
}
public static void FoundNoValueInRequest(this ILogger logger, ModelBindingContext bindingContext)
{
if (!logger.IsEnabled(LogLevel.Debug))
return;
var modelMetadata = bindingContext.ModelMetadata;
switch (modelMetadata.MetadataKind)
{
case ModelMetadataKind.Parameter:
logger.Log(LogLevel.Debug,
new EventId(16, "FoundNoValueForParameterInRequest"),
$"Could not find a value in the request with name '{bindingContext.ModelName}' for binding parameter '{modelMetadata.ParameterName}' of type '{bindingContext.ModelType}'.");
break;
case ModelMetadataKind.Property:
logger.Log(LogLevel.Debug,
new EventId(15, "FoundNoValueForPropertyInRequest"),
$"Could not find a value in the request with name '{bindingContext.ModelName}' for binding property '{modelMetadata.ContainerType}.{modelMetadata.PropertyName}' of type '{bindingContext.ModelType}'.");
break;
case ModelMetadataKind.Type:
logger.Log(LogLevel.Debug,
new EventId(46, "FoundNoValueInRequest"),
$"Could not find a value in the request with name '{bindingContext.ModelName}' of type '{bindingContext.ModelType}'.");
break;
}
}
public static void DoneAttemptingToBindModel(this ILogger logger, ModelBindingContext bindingContext)
{
if (!logger.IsEnabled(LogLevel.Debug))
return;
var modelMetadata = bindingContext.ModelMetadata;
switch (modelMetadata.MetadataKind)
{
case ModelMetadataKind.Parameter:
logger.Log(LogLevel.Debug,
new EventId(45, "DoneAttemptingToBindParameterModel"),
$"Done attempting to bind parameter '{modelMetadata.ParameterName}' of type '{modelMetadata.ModelType}'.");
break;
case ModelMetadataKind.Property:
logger.Log(LogLevel.Debug,
new EventId(14, "DoneAttemptingToBindPropertyModel"),
$"Done attempting to bind property '{modelMetadata.ContainerType}.{modelMetadata.PropertyName}' of type '{modelMetadata.ModelType}'.");
break;
case ModelMetadataKind.Type:
logger.Log(LogLevel.Debug,
new EventId(25, "DoneAttemptingToBindModel"),
$"Done attempting to bind model of type '{bindingContext.ModelType}' using the name '{bindingContext.ModelName}'.");
break;
}
}
}
}
using System;
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
/// <summary>
/// Adds additional behavior to the modelbinding for numeric properties.
/// </summary>
[HtmlTargetElement("input", Attributes = "asp-for")]
public class NumberInputTagHelper : InputTagHelper
{
/// <summary>
/// Initializes a new instance of the <see cref="NumberInputTagHelper"/> class.
/// </summary>
/// <param name="generator">The HTML generator.</param>
public NumberInputTagHelper(IHtmlGenerator generator)
: base(generator)
{ }
/// <inheritdoc />
public override void Process(TagHelperContext context, TagHelperOutput output)
{
base.Process(context, output);
var types = new[] {
typeof(byte), typeof(sbyte),
typeof(ushort), typeof(short),
typeof(uint), typeof(int),
typeof(ulong), typeof(long),
typeof(float),
typeof(double),
typeof(decimal)
};
var typeAttributes = output.Attributes
.Where(a => a.Name == "type")
.ToList();
string typeAttributeValue = typeAttributes.First().Value as string;
Type modelType = For.ModelExplorer.ModelType;
Type nullableType = Nullable.GetUnderlyingType(modelType);
// the type itself or its nullable wrapper matching and
// the type attribute is number or there is only one type attribute
// IMPORTANT TO KNOW: if the type attribute is set in the view, there are two attributes with same value.
if ((types.Contains(modelType) || types.Contains(nullableType)) && (typeAttributeValue == "number" || typeAttributes.Count == 1))
{
var culture = CultureInfo.InvariantCulture;
string min = "";
string max = "";
string step = "";
string value = "";
if (modelType == typeof(byte) || nullableType == typeof(byte))
{
min = byte.MinValue.ToString(culture);
max = byte.MaxValue.ToString(culture);
if (For.Model != null)
{
byte val = (byte)For.Model;
value = value.ToString(culture);
}
}
else if (modelType == typeof(sbyte) || nullableType == typeof(sbyte))
{
min = sbyte.MinValue.ToString(culture);
max = sbyte.MaxValue.ToString(culture);
if (For.Model != null)
{
sbyte val = (sbyte)For.Model;
value = val.ToString(culture);
}
}
else if (modelType == typeof(ushort) || nullableType == typeof(ushort))
{
min = ushort.MinValue.ToString(culture);
max = ushort.MaxValue.ToString(culture);
if (For.Model != null)
{
ushort val = (ushort)For.Model;
value = val.ToString(culture);
}
}
else if (modelType == typeof(short) || nullableType == typeof(short))
{
min = short.MinValue.ToString(culture);
max = short.MaxValue.ToString(culture);
if (For.Model != null)
{
short val = (short)For.Model;
value = val.ToString(culture);
}
}
else if (modelType == typeof(uint) || nullableType == typeof(uint))
{
min = uint.MinValue.ToString(culture);
max = uint.MaxValue.ToString(culture);
if (For.Model != null)
{
uint val = (uint)For.Model;
value = val.ToString(culture);
}
}
else if (modelType == typeof(int) || nullableType == typeof(int))
{
min = int.MinValue.ToString(culture);
max = int.MaxValue.ToString(culture);
if (For.Model != null)
{
int val = (int)For.Model;
value = val.ToString(culture);
}
}
else if (modelType == typeof(ulong) || nullableType == typeof(ulong))
{
min = ulong.MinValue.ToString(culture);
if (For.Model != null)
{
ulong val = (ulong)For.Model;
value = val.ToString(culture);
}
}
else if (modelType == typeof(long) || nullableType == typeof(long))
{
if (For.Model != null)
{
long val = (long)For.Model;
value = val.ToString(culture);
}
}
else if (modelType == typeof(float) || nullableType == typeof(float))
{
step = "any";
if (For.Model != null)
{
float val = (float)For.Model;
value = val.ToString(culture);
}
}
else if (modelType == typeof(double) || nullableType == typeof(double))
{
step = "any";
if (For.Model != null)
{
double val = (double)For.Model;
value = val.ToString(culture);
}
}
else if (modelType == typeof(decimal) || nullableType == typeof(decimal))
{
step = "any";
if (For.Model != null)
{
decimal val = (decimal)For.Model;
value = val.ToString(culture);
}
}
output.Attributes.SetAttribute(new TagHelperAttribute("type", "number"));
output.Attributes.SetAttribute(new TagHelperAttribute("value", value));
if (!string.IsNullOrWhiteSpace(min) && !output.Attributes.ContainsName("min"))
output.Attributes.SetAttribute(new TagHelperAttribute("min", min));
if (!string.IsNullOrWhiteSpace(max) && !output.Attributes.ContainsName("max"))
output.Attributes.SetAttribute(new TagHelperAttribute("max", max));
if (!string.IsNullOrWhiteSpace(step) && !output.Attributes.ContainsName("step"))
output.Attributes.SetAttribute(new TagHelperAttribute("step", step));
}
}
}
}
@eversoncoutinho
Copy link

Thank you very much. May all your goals be doubled.

@N1K1TAS95
Copy link

Thank you very much.

@biryazilim
Copy link

I want to use a nice nursery rhyme I remember from a movie for you: YOU ARE A MAN AMONGST THE MEN...

@iamzm
Copy link

iamzm commented Jul 25, 2023

Thanks mate you saved my day !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment