Custom Model Binding in ASP.Net Core

Early on in my career I was an accountant, which meant a lot of dollar signs and commas in the numbers I worked with in my daily tasks. The typical website or application I used, if built according to normal expectations, gave a nice message telling me what I’d done wrong when I accidentally entered a currency formatted value in a text input expecting a decimal. That was frustrating when I might be copy/pasting a value into the input from another system, or simply entering a value and absent-mindedly adding a dollar sign and comma. How smart is that software if it can’t understand that $1,340.12 == 1340.12?

For web applications, the first place most developers go when solving this issue is JavaScript. Possibly a CSS class added to the input identifying it as a currency value, with an event handler attached to the form submit that scrubs the input. Or when generating an Ajax call you can scrub it before adding it to the post data. But what if you want to stick with ASP.Net Core MVC  functionality and aren’t planning to use JavaScript for standard user input validation? Or if you are using a 3rd-party control to generate your user interface which doesn’t allow easily hooking into the form submit or Ajax calls? The solution we’re going to look at here is a custom model binder which will add currency input scrubbing by simply decorating a model property with an attribute.

The code in this article can be found in a public GitHub repository at https://github.com/djhaley/InputScrubbingModelBinder, and consists mostly of an MVC app – ASP.NET Core Web Application (.NET Framework) – with no authentication. The following items were also added to facilitate demoing our custom model binder:

  • Session state and in-memory caching in project.json and Startup.cs so we can use a session variable to store list entries.
  • Account model with two properties – Name and Balance.
  • AccountController and two views – Index and Create.

Our Account model will start out with Required attributes for each property.

At this point if you attempt to enter $1,340.12 in the Balance field you will receive this message, which is returned when the SimpleTypeModelBinder fails to convert the string “$1,340.12” to a decimal.

not_valid_value_currency

The result we are looking for is to validate the input as currency rather than a decimal, but we may have other needs for scrubbing input before model binding takes place so we’ll use an interface that describes our Scrub action.

Next we’ll add our CurrencyScrubberAttribute that will do the work of parsing the user input to see if it is a valid currency format. C#’s decimal.TryParse has an overload that takes a NumberStyle and CultureInfo, which is how we’ll do our currency validation. You’ll notice this only works with US currency ($) at this time, but would just require setting CultureInfo to handle other currencies. Our Scrub method has an out parameter that sends back a boolean value indicating success or failure. The model binder will use this parameter to indicate whether or not the binding for this property has succeeded.

Using our new CurrencyScrubberAttribute, the Balance property now looks like this:

Next we’ll need to add a model binder.  In .Net Core 1.0 model binding is achieved by sending the model to a list of IModelBindingProviders until it finds one that will provide an IModelBinder to handle the binding.  In the case of a strongly typed model like Account, the ComplexTypeModelBinderProvider accepts the challenge, then creates a binder for each property.  In our case, both Name and Balance will be handled by the SimpleTypeModelBinder.  Pre-RTM, the binders were called sequentially until one of the binders dealt with the binding.  In the RTM release the IModelBindingProvider needs to make a decision up front whether it will handle the value, and if it does then it handles all binding work.

For our input scrubber our IModelBindingProvider looks like the following.  We will be looking for non-complex types which have an IScrubberAttribute.  If those conditions aren’t met we return null and the framework moves on to the next provider.

Our model binder will be handling simple types that have an IScrubberAttribute, but if for any reason we aren’t going to deal with the binding we will pass it to a SimpleTypeModelBinder to handle it.  If we do handle the model binding and the call to Scrub is successful then we’ll pass back the new value and indicate the Task has completed.

In Startup.cs we’ll need to tell the framework about our IModelBinderProvider, and to make sure we get first choice on binding the model we’ll put ours first.

While there are many ways on the client to scrub user input, it’s not as easy on the server. Custom model binders give you this power and allow you to handle for yourself the conversion from a string input to a model property. With the above approach, you can deal with any custom model binding needs your business rules may require and continue to use the latest MVC functionality without mixing in JavaScript.

Join the Conversation

  1. Dan Haley

12 Comments

Your email address will not be published. Required fields are marked *

  1. I enjoyed reading the article you posted. There were some really great explain Custom Model Binding in ASP.Net Core!
    I hope you will keep sharing more such informative articles, have a nice day!

  2. Awesome article! But just wondering how complex types (a list of strings for example) should be handled?
    And as Ryan pointed: IScrubberAttribute cannot be used as type parameter for GetCustomAttributes. The framework expects a type which inherits from System.Attribute or typeof(System.Attribute) itself.

  3. I am using v1.1 and this code no longer seems to work. I have a web method:

    [HttpPut(“{id}”)]
    public IActionResult Update(string id, [FromBody] Test item)

    and in the ScrubbingModelBinderProvider.GetBinder, I get called twice:
    1. with a string, the id form the Update method
    2. with a Test instance from the Update method

    In the GetBinder, the call for the id crashes because the PropertyName is null, the call for the Test instance does nothing because it is a complex type and the GetBinder method ignores complex types.

    1. That being the case, I really can’t determine how I would get a single IModelBinder to return since there could be multiple attributes in the Model that need to be scrubbed.

    2. Michae,

      The code in the GitHub repo is targeting v1.1 – SDK 1.0.0 Preview 2.1-003177, so it is the latest code.

      The example doesn’t have any other parameters it is binding to, so it doesn’t need to check for PropertyName. In your case I would just check if context.Metadata.PropertyName != null.

      For your second comment, the IModelBinderProvider will get called for everything that is bound – in your case “id”, “item”, and all of item’s properties. Your provider gets to choose which of those it wants to handle the binding for, and for everything else it will return null. In the example it is only handling the binding for properties of the model with an IScrubberAttribute. Everything else is ignored.

      Hope that helps.

      Dan

  4. Awesome article. Just a quick update:

    On the ScrubbingModelBinderProvider, line 14:
    var attribute = propInfo.GetCustomAttributes(typeof(IScrubberAttribute), false).FirstOrDefault();

    This throws an error. It should be CurrencyScrubberAttribute instead of IScrubberAttribute.

    You may also want to throw in some error handling to make sure propName and propInfo are not null before you get to attribute. It was throwing errors for me before I put that in.

    1. Thanks Ryan. Yes, the code definitely is not complete, but as-is should work. I just installed .Net Core 1.0.1 and the tooling on a machine without any .Net Core components, and the project works. What kind of error are you getting on the line in ScrubbingModelBinderProvider? Are you on version 1.1?

      As for changing it to CurrencyScrubberAttribute, the binder provider is written to work with any attribute that implements IScrubberAttribute, so it should be typeof(IScrubberAttribute).

  5. Thank you Dan Haley,

    Your post helped me to resolve an error and understand the model binding behaviors.