Custom Model Binding in ASP.NET Core MVC

Custom Model Binding in ASP.NET Core MVC

In this article, I will discuss Custom Model Binding in ASP.NET Core MVC with Examples. Please read our previous article discussing Model Binding in ASP.NET Core MVC with Complex Type.

Custom Model Binding in ASP.NET Core MVC

Custom model binding in ASP.NET Core MVC is the process of intercepting the standard model binding process to provide custom logic for converting request data into action method parameters. This is useful when you have unique data or formats that the built-in model binders don’t support. To create a custom model binder, you typically follow these steps:

  • Create the Custom Model Binder: Implement the IModelBinder interface. This interface requires you to implement the BindModelAsync method.
  • Create a Model Binder Provider: Implement the IModelBinderProvider interface. This helps ASP.NET decide when to use your custom binder.
  • Register Your Custom Binder: Add your custom binder to the MVC options during application startup.
Step-by-Step Implementation:

We want a custom binder that binds a comma-separated string from the query string to a list of integers. Let us see how we can implement this in ASP.NET Core MVC.

Creating a Custom Model Binder:

Let us first create the Custom Model Binder. So, create a class file named CommaSeparatedModelBinder.cs and then copy and paste the following code into it. As you can see, this class implements the IModelBinder interface and provides an implementation for the BindModelAsync method. As part of this method, we need to write our custom logic. Here, we have written the logic that will bind a comma-separated string from the query string to a list of integers. The following example code is self-explained, so please go through the comment lines for a better understanding.

using Microsoft.AspNetCore.Mvc.ModelBinding;
using ModelBindingDemo.Models;
namespace ModelBindingDemo.Models
{
    public class CommaSeparatedModelBinder : IModelBinder
    {
        //ModelBindingContext: A context that contains operating information for model binding and validation.
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            //Get the Query String Parameter
            var query = bindingContext.HttpContext.Request.Query;
            //fetch the values based on the key
            var Ids = query["Ids"].ToString();
            //Check if the value is null or empty
            if (string.IsNullOrEmpty(Ids))
            {
                return Task.CompletedTask;
            }

            //Splitting the comma separated values to list of integers
            var values = Ids.Split(',').Select(int.Parse).ToList();

            //Success: Creates a ModelBindingResult representing a successful model binding operation.
            bindingContext.Result = ModelBindingResult.Success(values);

            //Mark the Task Has Been completed successfully
            return Task.CompletedTask;
        }
    }
}
Create a Model Binder Provider:

A provider allows more granular control, determining when your binder should be used based on the context. So, create a class file named CommaSeparatedModelBinderProvider.cs and copy and paste the following code. In this case, we’re checking if the model type is a list of integers.

using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace ModelBindingDemo.Models
{
    public class CommaSeparatedModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            //ModelType: Gets the model type represented by the current instance
            if (context.Metadata.ModelType == typeof(List<int>))
            {
                return new CommaSeparatedModelBinder();
            }
            return null;
        }
    }
}
Registering the Custom Model Binder:

Register your custom model binder in the Startup.cs or Program.cs depending on your ASP.NET Core version. As I am developing the application using the .NET 6 version, I will register the custom model binder within the Main method of the Program class using the code below.

builder.Services.AddControllersWithViews(options =>
{
    // Add the provider at the top of the list to ensure it runs before default providers
    options.ModelBinderProviders.Insert(0, new CommaSeparatedModelBinderProvider());
});
Using Custom Binder:

Now that everything’s set up, you can use your custom binder in your action methods. So, modify the HomeController as follows to use Custom Model Binder.

using Microsoft.AspNetCore.Mvc;
using ModelBindingDemo.Models;

namespace ModelBindingDemo.Controllers
{
    public class HomeController : Controller
    {
        //[HttpGet("home/getdetails")]
        //public IActionResult GetDetails([ModelBinder] List<int> Ids)
        //{
        //    // Your logic here...
        //    return Ok(Ids);
        //}

        [HttpGet("home/getdetails")]
        public IActionResult GetDetails([ModelBinder(typeof(CommaSeparatedModelBinder))] List<int> Ids)
        {
            // Your logic...
            return Ok(Ids);
        }
    }
}

When you access this action with a URL like /home/getdetails?Ids=1,2,3. The IDS parameter will be a list containing the integers 1, 2, and 3.

So, custom model binding in ASP.NET Core provides flexibility in dealing with unique request data scenarios. It allows you to seamlessly integrate custom parsing logic into the framework’s model-binding process.

Custom Model Binding RealTime Example in ASP.NET Core MVC

Consider a real-time scenario for custom model binding in ASP.NET Core MVC: converting a date range string in the format “startDate-endDate” into a custom DateRange object. This is useful in reporting scenarios where users might provide a date range as part of their query parameters.

Step 1: Define the DateRange Class
namespace ModelBindingDemo.Models
{
    public class DateRange
    {
        public DateTime StartDate { get; set; }
        public DateTime EndDate { get; set; }
    }
}
Step 2: Implement the Custom Model Binder
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Globalization;

namespace ModelBindingDemo.Models
{
    public class DateRangeModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            CultureInfo provider = CultureInfo.InvariantCulture;

            //Get the Query String Parameter
            var query = bindingContext.HttpContext.Request.Query;
            //fetch the values based on the key
            var DateRangeQueryString = query["range"].ToString();
            //Check if the value is null or empty
            if (string.IsNullOrEmpty(DateRangeQueryString))
            {
                return Task.CompletedTask;
            }

            //Split the Values by -
            var dateValues = DateRangeQueryString.Split('-');

            if (dateValues.Length != 2)
            {
                bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Invalid Date Range Format.");
                return Task.CompletedTask;
            }

            if (dateValues.Length == 2 && DateTime.TryParseExact(dateValues[0], "MM/dd/yyyy", provider, DateTimeStyles.None, out DateTime startDate) && DateTime.TryParseExact(dateValues[1], "MM/dd/yyyy", provider, DateTimeStyles.None, out DateTime endDate))
            {
                var dateRange = new DateRange { StartDate = startDate, EndDate = endDate };
                bindingContext.Result = ModelBindingResult.Success(dateRange);

                return Task.CompletedTask;
            }
            else
            {
                bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Invalid Date Range Format.");
                return Task.CompletedTask;
            }
        }
    }
}
Step3: Create a Model Binder Provider:
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace ModelBindingDemo.Models
{
    public class DateRangeModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            //ModelType: Gets the model type represented by the current instance
            if (context.Metadata.ModelType == typeof(DateRange))
            {
                return new DateRangeModelBinder();
            }
            return null;
        }
    }
}
Step 3: Register the Custom Model Binder
builder.Services.AddControllersWithViews(options =>
{
    // Add the provider at the top of the list to ensure it runs before default providers
    options.ModelBinderProviders.Insert(0, new CommaSeparatedModelBinderProvider());
    options.ModelBinderProviders.Insert(1, new DateRangeModelBinderProvider());
});
Step 4: Use the Custom Binder in an Action
using Microsoft.AspNetCore.Mvc;
using ModelBindingDemo.Models;

namespace ModelBindingDemo.Controllers
{
    public class HomeController : Controller
    {
        //[HttpGet("home/getdata")]
        //public IActionResult GetData([ModelBinder] DateRange range)
        //{
        //    // Do something with range.StartDate and range.EndDate
        //    return Ok($"From {range.StartDate} to {range.EndDate}");
        //}

        [HttpGet("home/getdata")]
        public IActionResult GetData([ModelBinder(typeof(DateRangeModelBinder))] DateRange range)
        {
            // Do something with range.StartDate and range.EndDate
            return Ok($"From {range.StartDate} to {range.EndDate}");
        }
    }
}

When you access this action with a URL like /home/getdata?range=01/01/2023-12/31/2023, the range parameter will be populated with the correct start and end dates. If the format is incorrect, a model state error is added.

This real-time example showcases how custom model binding can deal with specific formats or data types that the default model binders might not handle.

Complex Object Creation using Custom Model Binder in ASP.NET Core MVC

Creating complex objects using custom model binding in ASP.NET Core MVC can be beneficial in scenarios where the default model binders aren’t sufficient to generate an object based on a combination of different parts of an HTTP request (e.g., headers, query parameters, route values, and body data).

Suppose we have an object, ComplexUser, that we want to create based on data from the request header, route, and query string.

1. Define the ComplexUser Class:
namespace ModelBindingDemo.Models
{
    public class ComplexUser
    {
        public string Username { get; set; }
        public int Age { get; set; }
        public string Country { get; set; }
        public string ReferenceId { get; set; }
    }
}

Where:

  • Username comes from a header named “X-Username”.
  • Age is derived from the query string.
  • Country is part of the route.
  • ReferenceId is also from the query string.
2. Implement the Custom Model Binder:
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace ModelBindingDemo.Models
{
    public class ComplexUserModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var headers = bindingContext.HttpContext.Request.Headers;
            var routeData = bindingContext.HttpContext.Request.RouteValues;
            var query = bindingContext.HttpContext.Request.Query;

            var user = new ComplexUser
            {
                Username = headers["X-Username"].ToString(),
                Country = routeData["country"].ToString(),
                Age = int.TryParse(query["age"].ToString(), out var age) ? age : 0,
                ReferenceId = query["refId"].ToString()
            };

            bindingContext.Result = ModelBindingResult.Success(user);
            return Task.CompletedTask;
        }
    }
}
Step3: Create a Model Binder Provider:
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace ModelBindingDemo.Models
{
    public class ComplexUserModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            //ModelType: Gets the model type represented by the current instance
            if (context.Metadata.ModelType == typeof(ComplexUser))
            {
                return new ComplexUserModelBinder();
            }
            return null;
        }
    }
}
3. Register the Custom Model Binder:

In the Startup.cs or Program.cs (depending on your ASP.NET Core version):

builder.Services.AddControllersWithViews(options =>
{
    // Add the provider at the top of the list to ensure it runs before default providers
    options.ModelBinderProviders.Insert(0, new CommaSeparatedModelBinderProvider());
    options.ModelBinderProviders.Insert(1, new DateRangeModelBinderProvider());
    options.ModelBinderProviders.Insert(2, new ComplexUserModelBinderProvider());
});
4. Use the Custom Binder in an Action:
using Microsoft.AspNetCore.Mvc;
using ModelBindingDemo.Models;

namespace ModelBindingDemo.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet("data/{country}")]
        public IActionResult GetComplexUserData([ModelBinder(typeof(ComplexUserModelBinder))] ComplexUser user)
        {
            // Your logic...
            return Ok(user);
        }
    }
}

With this setup, if you send a request to /data/India?age=25&refId=12345 with the header X-Username: Pranaya, the ComplexUser parameter in the action will be populated based on the custom binding logic.

Single Property Binding in ASP.NET Core MVC

We can also achieve the previous example using Single Property Binding in ASP.NET Core MVC. In ASP.NET Core MVC, single property binding refers to the capability to bind a specific property of an object differently from other properties. This can be achieved by using model binding attributes on individual properties.

We can use the built-in attributes like [FromRoute], [FromQuery], [FromBody], [FromForm], and [FromHeader] on individual properties to specify where the property value should come from. So, modify the ComplexUser class as follows.

using Microsoft.AspNetCore.Mvc;
namespace ModelBindingDemo.Models
{
    public class ComplexUser
    {
        [FromHeader(Name = "X-Username")]
        public string? Username { get; set; }

        [FromQuery(Name = "age")]
        public int Age { get; set; }

        [FromRoute(Name = "country")]
        public string? Country { get; set; }

        [FromQuery(Name = "refid")]
        public string? ReferenceId { get; set; }
    }
}

With the above changes in place, modify the Home Controller as follows:

using Microsoft.AspNetCore.Mvc;
using ModelBindingDemo.Models;

namespace ModelBindingDemo.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet("data/{country}")]
        public IActionResult GetComplexUserData(ComplexUser user)
        {
            // Your logic...
            return Ok(user);
        }
    }
}

In the next article, I will discuss Model Validations in ASP.NET Core MVC with Examples. In this article, I try to explain Custom Model Binding in ASP.NET Core MVC with Examples. I hope you enjoy this Custom Model Binding in ASP.NET Core MVC article.

Leave a Reply

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