'In asp.net core mvc, the model binding for decimals does not accept thousand separators

For a model with a decimal property, if the value from client contains commas as thousand separator, the model binding will fail.

How can we solve this? Any solution (globally, controller/action local or model/property local) is good.

I have a workaround, which is to have a string property that reads and writes to the decimal one. But I'm looking for a cleaner solution.



Solution 1:[1]

If your application needs to support only a specific format (or culture), you could specify it in your Configure method as follows:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    var cultureInfo = new CultureInfo("en-US");
    cultureInfo.NumberFormat.NumberGroupSeparator = ",";

    CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
    CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;

   [...]
}

If you want to support several cultures and to automatically select the right one for each request, you can use the localization middleware instead, e.g.:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    [...]  

    var supportedCultures = new[]
    {
        new CultureInfo("en-US"),
        new CultureInfo("es"),
    };

    app.UseRequestLocalization(new RequestLocalizationOptions
    {
        DefaultRequestCulture = new RequestCulture("en-US"),
        // Formatting numbers, dates, etc.
        SupportedCultures = supportedCultures,
        // Localized UI strings.
        SupportedUICultures = supportedCultures
    });

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseCookiePolicy();

    app.UseMvc();
}

More info here: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-2.2

Edit - Decimal binder

If everything above fails, you could also roll your own model binder, e.g.:

public class CustomBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.Metadata.ModelType == typeof(decimal))
        {
            return new DecimalModelBinder();
        }

        return null;
    }
}

public class DecimalModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        if (valueProviderResult == null)
        {
            return Task.CompletedTask;
        }

        var value = valueProviderResult.FirstValue;

        if (string.IsNullOrEmpty(value))
        {
            return Task.CompletedTask;
        }

        // Remove unnecessary commas and spaces
        value = value.Replace(",", string.Empty).Trim();

        decimal myValue = 0;
        if (!decimal.TryParse(value, out myValue))
        {
            // Error
            bindingContext.ModelState.TryAddModelError(
                                    bindingContext.ModelName,
                                    "Could not parse MyValue.");
            return Task.CompletedTask;
        }

        bindingContext.Result = ModelBindingResult.Success(myValue);
        return Task.CompletedTask;
    }
}

Don't forget to register the custom binder in your ConfigureServices method:

 services.AddMvc((options) =>
 {
     options.ModelBinderProviders.Insert(0, new CustomBinderProvider());
 });

Now every time you use a decimal type in any of your models, it will be parsed by your custom binder.

Solution 2:[2]

In case if localization and custom model binder didn't work for you, as it didn't for me. You can extend serialization settings with custom JsonConverter which will serialize and deseralize all decimal values.

private class CultureInvariantDecimalConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteValue(value);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
        JsonSerializer serializer)
    {
        //your custom parsing goes here
    }

    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(decimal) || objectType == typeof(decimal?));
    }
}

And apply it with with this extension method

public static IMvcBuilder AddInvariantDecimalSerializer(this IMvcBuilder builder)
{
    return builder.AddJsonOptions(options =>
        options.SerializerSettings.Converters.Add(new CultureInvariantDecimalConverter()));
}

Solution 3:[3]

At the heart of this problem is the fact that decimal.TryParse() relies entirely on the local computer settings. No matter how many Decimal custom binders you code...
Example from the Custom Decimal Binder execution:
value = "11.2"
?decimal.TryParse(value, out decimalValue)
false
value = "11,2"
?decimal.TryParse(value, out decimalValue)
true
So even when the other commas is removed which seems its done by the native DecimalBinder then it still fails when it parses it to decimal in the tryparse method...
Use the tryparse method in this way:
decimal.TryParse(value,System.Globalization.NumberStyles.Any, CultureInfo.InvariantCulture ,out decimalValue)

Solution 4:[4]

you must put CultureInfo.InvariantCulture when you want to convert double:

public class CustomBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(double))
            {
                return new DoubleModelBinder();
            }

            return null;
        }
    }

    public class DoubleModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

            if (valueProviderResult == null)
            {
                return Task.CompletedTask;
            }

            var value = valueProviderResult.FirstValue;

            if (string.IsNullOrEmpty(value))
            {
                return Task.CompletedTask;
            }

            // Remove unnecessary commas and spaces
            value = value.Replace(",", string.Empty).Trim();

            double myValue = 0;
            try
            {
             myValue = Convert.ToDouble(value, CultureInfo.InvariantCulture);
                bindingContext.Result = ModelBindingResult.Success(myValue);
                return Task.CompletedTask;
            }
            catch (Exception m)
            {
                return Task.CompletedTask;                
            }
           
        }
    }

and in startup.cs

services.AddMvc(options => {
                options.ModelBinderProviders.Insert(0, new CustomBinderProvider());
            });

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Paulik
Solution 3
Solution 4 sajaf