Custom Model Binding in ASP.NET Core Web API

Custom Model Binding in ASP.NET Core Web API

In this article, I will discuss How to Implement Custom Model Binding in ASP.NET Core Web API Application with Examples. Please read our previous article discussing Model Binding Using FromBody in ASP.NET Core Web API with Examples.

What is Custom Model Binding in ASP.NET Core Web API?

Custom Model Binding in ASP.NET Core Web API is a mechanism that allows developers to control how data from HTTP requests is bound to action method parameters. When a client makes a request to a Web API, data can be sent in various parts of the request, such as in the URL, query string, headers, or body. The ASP.NET Core model binding system automatically maps this data to the parameters of the action methods in controllers. However, there are scenarios where the default model binding behavior does not meet the specific requirements of your application. In such cases, custom model binders can be implemented to provide custom logic for mapping request data to action method parameters.

Why Custom Model Binding in ASP.NET Core Web API?

By default, ASP.NET Core uses a series of built-in model binders to convert incoming request data into .NET types and pass them to action methods. This process is mostly transparent to the developer and works well for standard scenarios. Despite its flexibility, the default model binding might not be suitable for certain complex scenarios, such as:

  • Binding from custom sources or in a custom format not supported out of the box (e.g., encrypted data in a request body or custom header formats).
  • Handling large files or streams in a specialized manner.
  • Binding data from multiple sources into a single complex object.

Custom model binding is useful in the above scenarios when you need to process data in a way that the default model binders do not support.

Implementing Custom Model Binding in ASP.NET Core Web API

To implement Custom Model Binding in ASP.NET Core Web API, we typically need to create a class that implements either the IModelBinder interface for simple scenarios or the IModelBinderProvider interface for more complex scenarios where you might need to select a binder based on the type of the model or other criteria.

  • IModelBinder: Requires implementing the BindModelAsync method, which contains the logic to bind data from the request to the model object.
  • IModelBinderProvider: Used to provide a model binder based on the context, allowing more control over which binder is used for which model.
Binding a Complex Object from a Query String

Binding a complex object from a query string in ASP.NET Core Web API can be achieved by implementing a Custom Model Binder. ASP.NET Core uses model binding to convert client request data (from query strings, route data, headers, etc.) into objects that controllers can handle. However, for complex types, especially when they need to be bound from the query string, the default model binder may not suffice. In such cases, creating a custom model binder is the solution.

Define Your Complex Object

First, define the complex object you want to bind from the query string. So, create a class file named FilterModel.cs and then copy and paste the following code:

namespace ModelBinding.Models
{
    public class FilterModel
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public List<string> Interests { get; set; } = new List<string>();
    }
}
Implement a Custom Model Binder in ASP.NET Core Web API

Create a custom model binder by implementing the IModelBinder interface. This interface contains a single method, BindModelAsync, that you need to override. So, create a class file named FilterModelBinder.cs and copy and paste the following code. The following FilterModelBinder class is a custom model binder designed to bind query string parameters to a complex object in ASP.NET Core Web API. This class implements the IModelBinder interface, which requires the implementation of the BindModelAsync method.

using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace ModelBinding.Models
{
    public class FilterModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            // Retrieve the query string
            var queryString = bindingContext.HttpContext.Request.Query;

            // Instantiate the target object
            var model = new FilterModel();

            // Bind properties
            if (queryString.TryGetValue("Name", out var name))
            {
                model.Name = name;
            }

            if (queryString.TryGetValue("Age", out var age) && int.TryParse(age, out var ageValue))
            {
                model.Age = ageValue;
            }

            if (queryString.TryGetValue("Interests", out var interests))
            {
                model.Interests = interests.ToString().Split(",").ToList();
            }

            // Set the result
            bindingContext.Result = ModelBindingResult.Success(model);

            return Task.CompletedTask;
        }
    }
}

Let us understand the above class code in detail:

Class Definition:

public class FilterModelBinder : IModelBinder: This line defines the FilterModelBinder class and implements the IModelBinder interface, indicating that this class must provide a custom binding logic for converting query string values into a .NET object.

BindModelAsync Method:

public Task BindModelAsync(ModelBindingContext bindingContext): The BindModelAsync is an asynchronous method defined by the IModelBinder interface. This method is responsible for binding data from the HTTP request to an object. It takes a ModelBindingContext parameter, which provides access to the model metadata, state, and other details about the model binding process.

Argument Validation
if (bindingContext == null)
{
    throw new ArgumentNullException(nameof(bindingContext));
}

This check ensures that the bindingContext argument is not null, throwing an ArgumentNullException if it is. This is a defensive programming practice to catch errors early.

Query String Retrieval:

var queryString = bindingContext.HttpContext.Request.Query;: This line retrieves the query string parameters from the current HTTP request context.

Instantiation of Target Object:

var model = new FilterModel();: A new instance of the FilterModel object is created. This is the object that will be populated with values from the query string and then passed to the controller action.

Binding Properties

The following blocks of code attempt to retrieve values from the query string and assign them to the properties of the FilterModel object:

if (queryString.TryGetValue("Name", out var name))
{
    model.Name = name;
}

if (queryString.TryGetValue("Age", out var age) && int.TryParse(age, out var ageValue))
{
    model.Age = ageValue;
}

if (queryString.TryGetValue("Interests", out var interests))
{
    model.Interests = interests.ToString().Split(",").ToList();
}

Here,

  • Name: Checks if the “Name” parameter exists in the query string and sets the Name property of FilterModel.
  • Age: Retrieves the “Age” parameter, converts it to an integer, and sets the Age property.
  • Interests: Splits the “Interests” parameter by commas to create a list of strings and sets the Interests property.
Setting the Result:

bindingContext.Result = ModelBindingResult.Success(model);: This line sets the Result property of the ModelBindingContext to a successful binding result, passing in the populated FilterModel object. This indicates that model binding was successful and provides the controller action with the bound object.

Completion:

return Task.CompletedTask;: The method returns a completed task, signaling the end of the asynchronous operation.

Create a Model Binder Provider

Create a class, implement the IModelBinderProvider interface, and provide your custom model binder for the specific type when necessary. So, create a class file named FilterModelBinderProvider.cs and copy and paste the following code. This class is designed to integrate custom model binding logic into the ASP.NET Core framework by specifying when the FilterModelBinder should bind data to model objects.

using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace ModelBinding.Models
{
    public class FilterModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(FilterModel))
            {
                return new FilterModelBinder();
            }

            return null;
        }
    }
}

Understanding the above class code in detail:

Class Definition:

public class FilterModelBinderProvider : IModelBinderProvider: This line defines the FilterModelBinderProvider class and indicates that it implements the IModelBinderProvider interface. Implementing this interface requires the class to provide a method that returns an instance of a model binder based on the context of the binding request.

GetBinder Method:

public IModelBinder GetBinder(ModelBinderProviderContext context)

  • Purpose: The GetBinder method is responsible for returning an instance of a model binder based on the given ModelBinderProviderContext. This context contains information about the model and the binding scenario, allowing the provider to decide whether its model binder should be used.
  • Parameters: The method takes a single parameter, context, of type ModelBinderProviderContext, which provides access to the metadata associated with the model being bound.
Argument Validation
if (context == null)
{
    throw new ArgumentNullException(nameof(context));
}

This check ensures that the context argument is not null, throwing an ArgumentNullException if it is. This is a standard practice to avoid null reference exceptions later in the method.

Determining Whether to Use the Custom Model Binder
if (context.Metadata.ModelType == typeof(FilterModel))
{
    return new FilterModelBinder();
}

Here,

  • Logic: This line checks if the type of the model being bound matches the FilterModel type. If it does, the method returns a new instance of the FilterModelBinder. This means the custom model binder is used whenever a controller action expects a parameter of type FilterModel.
  • Return Value: If the condition is met, an instance of FilterModelBinder is returned, indicating to the framework that this binder should be used for the current model. If the condition is not met, the method returns null, indicating that this provider does not have a suitable model binder for the current context. When null is returned, the model binding system continues to search through the remaining providers in its list until it finds a suitable one or defaults to the standard model binding behavior.
Returning Null for Unmatched Models

return null;

If the model type does not match FilterModel, the method returns null, signaling that this model binder provider does not apply to the current model binding context. This allows the ASP.NET Core model binding system to continue its search for an appropriate model binder among other registered providers.

Register the Custom Model Binder Provider

In the Program.cs file, add your custom model binder provider to the MVC configuration in the Main method. So, add the following code:

builder.Services.AddControllers(options =>
{
    // Add our custom model binder at the beginning of the collection
    options.ModelBinderProviders.Insert(0, new FilterModelBinderProvider());
});
Understanding the above Code Explanation
  • builder.Services.AddControllers: This method call adds services for MVC controllers to the application’s dependency injection container. MVC controllers are responsible for handling HTTP requests and returning responses.
  • Lambda Function options => {}: The lambda function passed as an argument to AddControllers allows for configuring options related to MVC controllers. The options parameter provides access to various settings that can be modified to customize the behavior of controllers and model binding.
  • options.ModelBinderProviders: This property is a collection of IModelBinderProvider instances. Each provider in this collection is responsible for providing a model binder based on the context of a binding request. Model binders are used to bind incoming request data to action method parameters.
  • .Insert(0, new FilterModelBinderProvider()): This method call inserts a new instance of FilterModelBinderProvider at the beginning of the ModelBinderProviders collection. The 0 index specifies that the custom provider should be added at the very start of the collection, ensuring it is considered before any built-in model binder providers.
Why at the Beginning?

Adding the custom model binder provider at the beginning of the collection gives it higher priority over the default providers. When the model binding process is executed, the providers are consulted in order to find a suitable model binder for a given model type. By placing the custom provider first, you ensure it gets the first chance to provide a model binder for the types it supports, like the FilterModel in this context.

Use the Complex Type in Your Controller

Now, you can use your complex type in your controller actions, and it will be bound from the query string by your custom model binder. So, modify the User Controller as follows:

using Microsoft.AspNetCore.Mvc;
using ModelBinding.Models;
namespace ModelBinding.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class UserController : ControllerBase
    {
        [HttpGet]
        public IActionResult GetResource([ModelBinder] FilterModel filter)
        {
            // Use the bound complex object
            return Ok(filter);
        }
    }
}
Testing the API:

Now, run the application and access the above endpoint using query string ?Name=ABC&Age=25&Interests=footbal,cricket,hockey, and you should get the output as expected, as shown in the below image:

URL: https://localhost:7121/api/User?Name=ABC&Age=25&Interests=footbal,cricket,hockey

Method: GET

How to Implement Custom Model Binding in ASP.NET Core Web API Application with Examples

Binding Data from Multiple Sources using Custom Model Binder:

Imagine an API endpoint where you need to bind a model from both the query string and route. ASP.NET Core does not support this scenario, but you can achieve it with custom model binding.

Define the Model

First, define the model class that you will be binding to. This class should represent the data structure you expect from the request. So, create a class file named MyCompositeDataModel.cs and then copy and paste the following code:

namespace ModelBinding.Models
{
    public class MyCompositeDataModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

I want to populate the ID from the Route data and Name from the query string.

Define a Custom Model Binder

Create a class file by implementing IModelBinder to bind data from different sources. So, create a class file named CompositeObjectModelBinder.cs and then copy and paste the following code:

using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace ModelBinding.Models
{
    public class CompositeObjectModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            //Make sure the bindingContext is not null
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            //Here, routeData and queryData represent the data collected from the route and query string, respectively.
            //These collections are used to retrieve the specific pieces of data needed to populate the model.
            var routeData = bindingContext.HttpContext.GetRouteData().Values;
            var queryData = bindingContext.HttpContext.Request.Query;

            //If required, then you can also access the header
            var headerData = bindingContext.HttpContext.Request.Headers;

            //Creating and Populating the Model
            var compositeObject = new MyCompositeDataModel
            {
                // These keys must be exist in route data and query string
                Id = Convert.ToInt32(routeData["Id"]),
                Name = queryData["Name"]
            };

            // Setting the Model Binding Result
            bindingContext.Result = ModelBindingResult.Success(compositeObject);
            return Task.CompletedTask;
        }
    }
}
Apply the Model Binder:

Without creating the Custom Model Binder Provider, we can also directly use the above Custom Model Binder in our action method. So, modify the User Controller as follows:

using Microsoft.AspNetCore.Mvc;
using ModelBinding.Models;
namespace ModelBinding.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class UserController : ControllerBase
    {
        [HttpGet("{Id}")]
        public IActionResult GetResource([ModelBinder(BinderType = typeof(CompositeObjectModelBinder))] MyCompositeDataModel compositeObject)
        {
            // Use the compositeObject
            return Ok(compositeObject);
        }
    }
}
In this example:

[ModelBinder(BinderType = typeof(CompositeObjectModelBinder))]

  • [ModelBinder]: This is an attribute applied to the action method parameter. It specifies that a custom model binder should bind the parameter rather than relying on ASP.NET Core’s default model binding mechanism.
  • BinderType = typeof(CompositeObjectModelBinder): This part of the attribute explicitly sets which custom model binder to use for binding the parameter. In this case, it specifies CompositeObjectModelBinder, indicating that this binder contains the logic to create and populate an instance of CompositeObject.
Testing:

Now, you can pass the Id value in Route and Name in the query string (100?name=James) as shown in the below image:

Binding Data from Multiple Sources using Custom Model Binder

When Should We Use Custom Model Binding in ASP.NET Core Web API?

Custom Model binding in ASP.NET Core Web API is a powerful feature that allows developers to control how HTTP request data is mapped to action method parameters. It is useful in situations where you need to handle complex data types, perform custom validation, or deal with data sources that do not directly map to your model properties. Here are several scenarios where implementing custom model binding might be appropriate:

  • Complex Data Types: If your action method needs to accept parameters not easily represented or bound by the default model binders, such as JSON within a query string or a complex object constructed from multiple sources in the request.
  • Custom Validation or Transformation: When you need to perform custom validation or transformation of the input data before it reaches your action method. For example, you might want to normalize phone numbers, parse strings into complex types, or decrypt data received in the request.
  • Handling Large Files or Streams: When dealing with large file uploads or streams that should not be fully loaded into memory, a custom model binder can provide a more efficient way to process these files incrementally.
  • Integrating External Data: If your action methods need to incorporate data from external sources (such as a database or a web service) based on identifiers provided in the request, custom model binding can abstract away the external data fetching logic.
  • Legacy System Integration: When integrating with legacy systems, you might encounter scenarios where the data format or protocol does not align with the conventions expected by ASP.NET Core. Custom model binders can bridge this gap.
  • Custom Binding for Specific Content Types: If you need specialized binding logic for specific content types not well-supported by the built-in model binders, implementing a custom model binder can provide the necessary flexibility.

In the next article, I will discuss How to Apply Binding Attributes to Model Properties in ASP.NET Core Web API with Examples. In this article, I try to explain Custom Model Binding in ASP.NET Core Web API with Examples. I hope you enjoy this article, “Custom Model Binding Using FromBody in ASP.NET Core Web API.”

Leave a Reply

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