Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fluent Validation not accepting numbers with thousands separator

I have an ASP.NET MVC 5 project with Fluent Validation for MVC 5. I am also using a jQuery masking plugin to automatically add thousands to double values.

In the model I have:

    [Display(Name = "Turnover")]
    [DisplayFormat(ApplyFormatInEditMode = true,ConvertEmptyStringToNull =true,DataFormatString ="#,##0")]
    public double? Turnover { get; set; }

In the view I have:

<th class="col-xs-2">
    @Html.DisplayNameFor(model=>model.Turnover)
</th>
<td class="col-xs-4">
    @Html.TextBoxFor(model => model.Turnover, new { @class = "form-control number", placeholder="Enter number. Thousands added automatically" })
</td>
<td class="col-xs-6">
    @Html.ValidationMessageFor(model => model.Turnover, "", new { @class = "text-danger" })
</td>

A fluent validator is defined for the containing model but it contains no rules. I am using server side validation only.

public class MyModelValidator: AbstractValidator<MyModel>
{
    public MyModelValidator()
    {

    }
}

Unfortunately I get a validation error for turnover as follows: enter image description here

I have tried using Model Binding to solve this problem. But the break point in model binder never gets hit - fluent validation seems to block the value from reaching the model binder.

like image 713
gls123 Avatar asked Nov 01 '16 17:11

gls123


People also ask

Is fluent validation good?

FluentValidation provides a great alternative to Data Annotations in order to validate models. It gives better control of validation rules and makes validation rules easy to read, easy to test, and enable great separation of concerns.

What is FluentValidation C#?

FluentValidation is a . NET library for building strongly-typed validation rules. FluentValidation 11 supports the following platforms: . NET Core 3.1.

What is the difference between data annotation and fluent API?

Fluent API is an advanced way of specifying model configuration that covers everything that data annotations can do in addition to some more advanced configuration not possible with data annotations.

How does fluent validation works?

FluentValidation is a server-side library and does not provide any client-side validation directly. However, it can provide metadata which can be applied to the generated HTML elements for use with a client-side framework such as jQuery Validate in the same way that ASP. NET's default validation attributes work.


2 Answers

Few things to mention:

  • The issue has nothing in common with Fluent Validation. I was able to reproduce/fix it with or without Fluent Validation.
  • The DataFormatString used is incorrect (missing the value placeholder). It should really be "{0:#,##0}".
  • The ModelBinder approach from the link actually works. I guess you forgot that it is written for decimal data type, while your model is using double?, so you have to write and register another one for double and double? types.

Now on the subject. There are actually two solutions. Both of them use the following helper class for the actual string conversion:

using System;
using System.Collections.Generic;
using System.Globalization;

public static class NumericValueParser
{
    static readonly Dictionary<Type, Func<string, CultureInfo, object>> parsers = new Dictionary<Type, Func<string, CultureInfo, object>>
    {
        { typeof(byte), (s, c) => byte.Parse(s, NumberStyles.Any, c) },
        { typeof(sbyte), (s, c) => sbyte.Parse(s, NumberStyles.Any, c) },
        { typeof(short), (s, c) => short.Parse(s, NumberStyles.Any, c) },
        { typeof(ushort), (s, c) => ushort.Parse(s, NumberStyles.Any, c) },
        { typeof(int), (s, c) => int.Parse(s, NumberStyles.Any, c) },
        { typeof(uint), (s, c) => uint.Parse(s, NumberStyles.Any, c) },
        { typeof(long), (s, c) => long.Parse(s, NumberStyles.Any, c) },
        { typeof(ulong), (s, c) => ulong.Parse(s, NumberStyles.Any, c) },
        { typeof(float), (s, c) => float.Parse(s, NumberStyles.Any, c) },
        { typeof(double), (s, c) => double.Parse(s, NumberStyles.Any, c) },
        { typeof(decimal), (s, c) => decimal.Parse(s, NumberStyles.Any, c) },
    };

    public static IEnumerable<Type> Types { get { return parsers.Keys; } }

    public static object Parse(string value, Type type, CultureInfo culture)
    {
        return parsers[type](value, culture);
    }
}

Custom IModelBinder

This is a modified version of the linked approach. It's a single class that handles all the numeric types and their respective nullable types:

using System;
using System.Web.Mvc;

public class NumericValueBinder : IModelBinder
{
    public static void Register()
    {
        var binder = new NumericValueBinder();
        foreach (var type in NumericValueParser.Types)
        {
            // Register for both type and nullable type
            ModelBinders.Binders.Add(type, binder);
            ModelBinders.Binders.Add(typeof(Nullable<>).MakeGenericType(type), binder);
        }
    }

    private NumericValueBinder() { }

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        var modelState = new ModelState { Value = valueResult };
        object actualValue = null;
        if (!string.IsNullOrWhiteSpace(valueResult.AttemptedValue))
        {
            try
            {
                var type = bindingContext.ModelType;
                var underlyingType = Nullable.GetUnderlyingType(type);
                var valueType = underlyingType ?? type;
                actualValue = NumericValueParser.Parse(valueResult.AttemptedValue, valueType, valueResult.Culture);
            }
            catch (Exception e)
            {
                modelState.Errors.Add(e);
            }
        }
        bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
        return actualValue;
    }
}

All you need is to register it in your Application_Start:

protected void Application_Start()
{
    NumericValueBinder.Register();  
    // ...
}

Custom TypeConverter

This is not specific to ASP.NET MVC 5, but DefaultModelBinder delegates string conversion to the associated TypeConverter (similar to other NET UI frameworks). In fact the issue is caused by the fact that the default TypeConverter classes for numeric types do not use Convert class, but Parse overloads with NumberStyles passing NumberStyles.Float which excludes NumberStyles.AllowThousands.

Fortunately System.ComponentModel provides extensible Type Descriptor Architecture which allows you to associate a custom TypeConverter. The plumbing part is a bit complicated (you have to register a custom TypeDescriptionProvider in order to provide ICustomTypeDescriptor implementation that finally returns custom TypeConverter), but with the help of the provided base classes that delegate most of the stuff to the underlying object, the implementation looks like this:

using System;
using System.ComponentModel;
using System.Globalization;

class NumericTypeDescriptionProvider : TypeDescriptionProvider
{
    public static void Register()
    {
        foreach (var type in NumericValueParser.Types)
            TypeDescriptor.AddProvider(new NumericTypeDescriptionProvider(type, TypeDescriptor.GetProvider(type)), type);
    }

    readonly Descriptor descriptor;

    private NumericTypeDescriptionProvider(Type type, TypeDescriptionProvider baseProvider)
        : base(baseProvider)
    {
        descriptor = new Descriptor(type, baseProvider.GetTypeDescriptor(type));
    }

    public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
    {
        return descriptor;
    }

    class Descriptor : CustomTypeDescriptor
    {
        readonly Converter converter;
        public Descriptor(Type type, ICustomTypeDescriptor baseDescriptor)
            : base(baseDescriptor)
        {
            converter = new Converter(type, baseDescriptor.GetConverter());
        }
        public override TypeConverter GetConverter()
        {
            return converter;
        }
    }

    class Converter : TypeConverter
    {
        readonly Type type;
        readonly TypeConverter baseConverter;
        public Converter(Type type, TypeConverter baseConverter)
        {
            this.type = type;
            this.baseConverter = baseConverter;
        }
        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            return baseConverter.CanConvertTo(context, destinationType);
        }
        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            return baseConverter.ConvertTo(context, culture, value, destinationType);
        }
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return baseConverter.CanConvertFrom(context, sourceType);
        }
        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            if (value is string)
            {
                try { return NumericValueParser.Parse((string)value, type, culture); }
                catch { }
            }
            return baseConverter.ConvertFrom(context, culture, value);
        }
    }
}

(Yeah, a lot of boilerplate code in order to add one essential line! From the other side, there is no need to handle nullable types because DefaultModelBinder already does that :)

Similar to the first approach, all you need is to register it:

protected void Application_Start()
{
    NumericTypeDescriptionProvider.Register();  
    // ...
}
like image 138
Ivan Stoev Avatar answered Oct 28 '22 16:10

Ivan Stoev


The problem is not with FluentValidation, but MVC's model binding to double type. MVC's default model binder cannot parse the number and assigns false to IsValid.

The problem was solved after I included the following code, credits to this post.

public class DoubleModelBinder : System.Web.Mvc.DefaultModelBinder {
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
        var result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (result != null && !string.IsNullOrEmpty(result.AttemptedValue)
            && (bindingContext.ModelType == typeof(double) || bindingContext.ModelType == typeof(double?))) {
            double temp;
            if (double.TryParse(result.AttemptedValue, out temp)) return temp;
        }
        return base.BindModel(controllerContext, bindingContext);
    }
}

And include the lines below in Application_Start:

ModelBinders.Binders.Add(typeof(double), new DoubleModelBinder());
ModelBinders.Binders.Add(typeof(double?), new DoubleModelBinder());

Also consider explicitly stating the current culture as in this post.

like image 25
Gokhan Kurt Avatar answered Oct 28 '22 18:10

Gokhan Kurt