Click here to Skip to main content
15,879,239 members
Articles / Web Development / HTML

A Custom Model Binder for Passing Complex Objects with Query Strings to Web API Methods

Rate me:
Please Sign up or sign in to vote.
4.74/5 (21 votes)
17 Jan 2018CPOL13 min read 157.9K   3K   51   24
Custom model binder for passing query strings as nested objects or collections to Web API GET or POST methods, also updated for ASP.NET Core
In this article, the custom FieldValueModelBinder class has been presented which can be efficiently used for passing complex objects with query strings to Web API methods. It’s simple to use, especially for a GET method receiving a query string as a nesting object. For a nesting collection object, the FieldValueModelBinder class provides an option when using query string sources for any GET, POST, or PUT methods.

Introduction

Update Note (1/18/2018): The FieldValueModelBinder source code for ASP.NET Core 2.0 is attached. Please see the section Migrated to ASP.NET Core for details.

A query string having field-value data pairs is the standard form of transferring messages in a URI, or a request body with the default application/x-www-form-urlencoded content type. The latest Web API 2 and ASP.NET MVC 5, when using query string data sources in a URI or request body, only support passing a simple object which consists of only primitive, non-class, or System.String type properties. For any complex object containing nested objects or collections, the only available choice is to pass the serialized JSON or XML data in the request body.

When I ported an existing nesting object model for search, paging, and sorting requests from a WCF web service to a Web API application, I would have liked to pass this complex object with a query string to a GET method, but couldn’t find any feasible solution. I finally created my own model binder that works effectively for passing all practical patterns of complex objects with query strings in either URI or request body.

Query String Fields for Complex Objects

For clear descriptions, I define these terms and use them throughout the article.

  • Simple Property: any property with a primitive, non-class, or System.String type
  • Complex Property: any property with class type but excluding the System.String
  • Simple Object: any object consisting of only simple properties
  • Nesting Object: any object containing one or more complex properties, but no collection
  • Nesting Collection Object: any nesting object with one or more collections

For a nesting object, the query string can look the same as that for a simple object. Field names may not be prefixed with parent object names since there is no collection in the object tree. The model binder should also resolve the simple property names in all nested objects even if there are same simple property names from different objects.

Shown below is an example of a nesting object model for the searching and paging request, and the corresponding query string source data. This is probably one of the most frequently used scenarios for a web application. The structure also includes a nested object with enum type. To simplify the demo, I use the CategoryId as a hard-coded search field. The real search request could be another nested object containing an enum SearchField containing additional items such as CategoryName, ProductName, ProductStatus, etc., and a string SearchText property.

The example of request model classes:

C#
public class NestSearchRequest
{
    public int CategoryId { get; set; }
    public PagingRequest PagingRequest { get; set; }        
}
public class PagingRequest
{        
    public int PageIndex { get; set; }
    public int PageSize { get; set; }
    public Sort Sort { get; set; }
}
public class Sort
{
    public string SortBy { get; set; }
    public SortDirection SortDirection { get; set; }  
}
public enum SortDirection
{
    Ascending,
    Descending
}

The query string for the above request model:

CategoryId=3&PageIndex=0&PageSize=8&SortBy=ProductName&SortDirection=Descending

For a nesting collection object, field names should be prefixed by complex property names with indexes. We also don’t want to embed a JSON or XML object-like structure to any value. Instead, the last part of each field name in a field-value pair should always point to a simple property.

Request model classes for test and demo of a nesting collection object:

C#
public class ComplexSearchRequest
{
    public int CategoryId { get; set; }
    public List<PagingSortRequest> PagingRequest { get; set; }        
    public string Test { get; set; }
}  

public class PagingSortRequest
{
    public int PageIndex { get; set; }
    public int PageSize { get; set; }
    public Sort[] Sort { get; set; }               
}

The query string data for the above request model:

CategoryId=3&PagingRequest[0]PageIndex=1&PagingRequest[0]PageSize=8&PagingRequest[0]
Sort[0]SortBy=ProductName&PagingRequest[0]Sort[0]SortDirection=descending&PagingRequest[0]
Sort[1]SortBy=CategoryID&PagingRequest[0]Sort[1]SortDirection=0&PagingRequest[1]
PageIndex=2&PagingRequest[1]PageSize=5&PagingRequest[1]Sort[0]
SortBy=CategoryID&PagingRequest[1]Sort[0]SortDirection=0&PagingRequest[1]Sort[1]
SortBy=ProductName&PagingRequest[1]Sort[1]SortDirection=Descending&Test=OK

The list of field-name pairs retrieved to the model binder is shown below:

Image 1

Use and Test FieldValueModelBinder Class

To see the custom model binder in action, you need to download the source code and recompile the solution using the Visual Studio 2012 or 2013. Be sure to have the Internet connection on your machine since all package files need to be automatically downloaded from the NuGet. To use the FieldValueModelBinder class in other projects, you can copy the class file in the SM.General.Api project or use the assembly SM.General.Api.dll. In the Web API controller code, just replace the [FromUri] or [FromBody] attribute from the GET or PUT method with this setting:

C#
[ModelBinder(typeof(SM.General.Api.FieldValueModelBinder))]

The test application is a Web API class library hosted by the local IIS Express. When you run the test app to open the HTML page, enter the query string into the parameter input text box, and click a link to pass the string to one of the API methods, the model binder will convert the query string to an object tree based on the model structure. The object will then be sent back from the response and displayed on the page. The code for calling a test method is straightforward.

JQuery code:

JavaScript
var input = $("#txaInput").val();
$.ajax({
    url: 'api/nvpstonestcollectionget?' + input,
    type: "GET",
    dataType: "json",    
    success: function (data) {
        //Display data on HTML page
        ... 
    },
    ...
});

Or AngularJS code:

JavaScript
$scope.nvpsToNestCollectionGet = function () {
    $http({
       url: 'api/nvpstonestcollectionget?' + $scope.txaInput,
       method: "GET"
    }).
    success(function (data, status, headers, config) {
        //Display data on HTML page
        ...             
    }).
    error(function (data, status, headers, config) {
        ...
    });
}

Server-side API method:

C#
[Route("~/api/nvpstonestcollectionget")]
public ComplexSearchRequest Get_NvpsToNestCollection
       ([ModelBinder(typeof(FieldValueModelBinder))] ComplexSearchRequest request)
{
    return request;
}

There is a test data string for the nesting object in the input text box by default. The default test string for the nesting collection object can be loaded into the box by clicking the Load default test input string link. The data in the input string must match the model type set for the API input argument. Otherwise, only the data pieces with matched field and property names are filled to the model. For example, if you use the default data string for the nesting collection object but click the Pass for Nesting Object to Get link, you will get the model with only the first object item in the collection because the property defined in the model class has no collection type.

Below is the demo screenshot for passing the query string to API method Get_NvpsToNestCollection():

Image 2

We can also check the .NET model object details in the Visual Studio 2012/2013 debugging windows.

Image 3

How Does FieldValueModelBinder Work?

The custom model binder deserializes the input data and populates the object with the type defined in the API method argument. What we need to do in the code is to implement the only member, BindModel method, in the System.Web.Http.ModelBinding.IModelBinder. The method receives two types of class objects that are needed for the data deserialization.

  1. System.Web.Http.Controllers.HttpActionContext contains all of the source http data info.
  2. System.Web.Http.ModelBinding.ModelBindingContext contains all of the target object model information. It also has the ValueProvider property for accessing any registered value providers for the source data.

Here is the main workflow inside the FieldValueModelBinder class:

  • Obtain the source field-value pair string and convert the data to a working list of key-value pair items.
  • Iterate through each property of an object in the hierarchy.
  • If the current iterated item is a complex property, recursively iterate through its properties.
  • If the complex property is a collection type, create a group working list for the source data. Otherwise, use a single working list for the source data.
  • Iterate through the working list of the source data.
  • If the source field name matches the property name, set the value for either a simple or a complex property.
  • Remove the worked item from the original data source list and refresh the working source data list after each iteration has successfully been done.
  • Finally, set the top level object to the target model and return it.

Please see the code and comment lines from the download source for details. Something particular will further be discussed in the following sections.

Obtaining Source Fields and Values

The FieldValueModelBinder class calls the HttpActionContext directly to obtain the source data without using value providers because the default QueryStringValueProvider only gets the data from the URI, not the request body. It’s also unable to handle collections as the recursive iterations require. More importantly, I need to use a working source data list for any real iteration process (see below). Although I may create a custom value provider, using my own List<KeyValuePair<string, string>> is more flexible and efficient.

Here is the code to obtain the original source data:

C#
//Define original source data list
List<KeyValuePair<string, string>> kvps;

//Check and get source data from uri 
if (!string.IsNullOrEmpty(actionContext.Request.RequestUri.Query))
{    
    kvps = actionContext.Request.GetQueryNameValuePairs().ToList(); 
}
//Check and get source data from body
else if (actionContext.Request.Content.IsFormData())
{                
    var bodyString = actionContext.Request.Content.ReadAsStringAsync().Result;
    kvps = ConvertToKVP(bodyString);
}
...

A working copy of the kvps will be created for each iteration process. For a nesting collection object, it's also needed to create a list of item objects in a collection:

C#
//Set KV Work List for each real iteration process
List<KeyValuePair<string, string>> kvpsWork = new List<KeyValuePair<string, string>>(kvps);

//KV Work For each object item in collection
List<KeyValueWork> kvwsGroup = new List<KeyValueWork>();            

//KV Work for collection
List<List<KeyValueWork>> kvwsGroups = new List<List<KeyValueWork>>();

Using the working source list can free the original source list from iteration loops so that an item can be deleted from the original list after the item has successfully been done, leaving only unworked items for remaining processes. Any working list will then be refreshed from the original list before a new iteration starts for the next property.

C#
kvps.Remove(item.SourceKvp); 

Matching Field Parts to Object Properties

As mentioned before, we can use field names without object prefixes for a nesting object. The model binder also handles two situations for this pattern.

  1. Correctly matching items when the same property names exist from different objects in the hierarchy. This feature benefits from using the refreshed working source data list. Since any worked source field-value pair has been removed, there is only unworked item in the candidate list for matching with the next iterated property. To test this, change the SortDirection property line in SM.Store.Api.Sort class to:
    C#
    public int PageIndex { get; set; }

    After running the test application, replace the &SortDirection=Descending with &PageIndex=2 in the source query string input box, and then click the Pass for Nesting Object to Get link. The result is shown below:

    Image 4

  2. Ignore any parent name prefix if it exists in the field name parts, such as “PagingRequest[0]Sort[0]SortBy=ProductName”. The code uses the regular expression Split() function to get only the last part of the field name.

    C#
    //Ignore any bracket in a name key 
    var key = item.Key;
    var keyParts = Regex.Split(key, @"\[\d*\]");
    if (keyParts.Length > 1) key = keyParts[keyParts.Length - 1];

For a nesting collection object, the regular expression Match() method is used to extract the brackets and index value for the last parent name. The field name string is then split based on the parent brackets to get the last part of the prefixed field name.

C#
//Get parts from current KV Work
regex = new Regex(parentProp.Name + @"\[([^}])\]");
match = regex.Match(item.Key);
var brackets = match.Value.Replace(parentProp.Name, "");
var objIdx = match.Groups[1].Value;

//Get parts array from Key
var keyParts = item.Key.Split(new string[] { brackets }, 
               StringSplitOptions.RemoveEmptyEntries);

//Get last part from prefixed name
Key = keyParts[keyParts.Length - 1]; 

Only knowing the last part of the field name is not enough for a nesting collection object. If there is no correct index passed to, and checked at, the child level, a field-value pair will not be mapped to the correct child object property. For this reason, the parent object index value in the pParentObjIndex parameter will be passed to the recursion method for processing the child object.

C#
RecurseNestedObj(tempObj, prop, pParentName: group[0].ParentName, 
                 pParentObjIndex: group[0].ObjIndex);

The method for processing the child object will then refresh the working source list that includes only the items for which the current iterated parent index value matches the passed pParentObjIndex value.

C#
//Get data only from parent-parent for linked child KV Work
if (pParentName != "" & pParentObjIndex != "")
{
    regex = new Regex(pParentName + RexSearchBracket);
    match = regex.Match(item.Key);
    if (match.Groups[1].Value != pParentObjIndex)
        break;
}

Resolving Enumeration Type

The enumeration type using the keyword enum is a special type consisting of a list of constant enumerators. A property having the enum type is also a simple property and doesn’t need the recursion process. The code searches the enum item values first. If the value is not matched, then search the default int type value by matching the enum index position. Thus the input data works for either enum value text or integer input for an index position. The code also makes the input enum value text case-insensitive.

C#
if (prop.PropertyType.IsEnum)
{
    var enumValues = prop.PropertyType.GetEnumValues();
    object enumValue = null;
    bool isFound = false;
                
    //Try to match enum item name first
    for (int i = 0; i < enumValues.Length; i++)
    {                    
        if (item.Value.ToLower() == enumValues.GetValue(i).ToString().ToLower())
        {
            enumValue = enumValues.GetValue(i);
            isFound = true;
            break;
        }
    }
    //Try to match enum default underlying int value if not matched with enum item name
    if(!isFound)
    {
        for (int i = 0; i < enumValues.Length; i++)
        {
            if (item.Value == i.ToString())
            {
                enumValue = i;                            
                break;
            }
        }
    }                
    prop.SetValue(obj, enumValue, null);
}

Supported Collection Types

The NameValueModelBinder class supports the generic List<> and System.Array types. In the test examples, the collection with the complex type PagingSortRequests in the model can be defined using either following form:

  1. Directly declaring a generic List<> type.
    C#
    public List<PagingSortRequest> PagingRequest { get; set; }
  2. Declaring a class object that inherits the base of List<> type.
    C#
    public PagingSortRequests PagingRequest { get; set; }

    The code for the class:

    C#
    public class PagingSortRequests : List<PagingSortRequest> {}
  3. Declaring an array of the object:

    C#
    public PagingSortRequest[] PagingRequest { get; set; };

When the model binder processes a collection type, it needs to dynamically instantiate the collection object. For an array type, we also need to know the element count before instantiating the array. In our case, the count info can be obtained from the item count of the working groups source list. Here are the code lines:

C#
//Initiate List or Array
IList listObj = null;
Array arrayObj = null;
if (parentProp.PropertyType.IsGenericType || parentProp.PropertyType.BaseType.IsGenericType)
{
    listObj = (IList)Activator.CreateInstance(parentProp.PropertyType);
}
else if (parentProp.PropertyType.IsArray)
{
    arrayObj = Array.CreateInstance(parentProp.PropertyType.GetElementType(), kvwsGroups.Count);
} 

Maximum Recursion Limit

The model binder sets the default maximum recursion limit to 100 at the class level.

C#
private int maxRecursionLimit = 100;

In the model binder, any complex property in the object tree will add one into the recursion counter. Any nested collection under a parent object will use one recursion regardless of the number of item objects in the collection since all collection items are processed under the same PropertyInfo array and completed in one recursion. If the parent object is a collection, however, a collection object under this parent will do multiple recursions based on the number of items in the parent collection. The previously described test example of nesting collection object would have three recursion counts, one for the PagingRequest collection and two for Sort collections, respectively, since there are two item objects in the PagingRequest collection.

Image 5

You can change maximum limit value by setting the item in the Web.config or App.config file of the calling project.

XML
<appSettings>
   <add key="MaxRecursionLimit" value="120"/> 
   . . .
</appSettings>

The default maximum recursion limit setting is usually meets the needs of common applications. Increasing the limit number and processing excessive nested objects or collections may deplete the machine memories and cause system to fail. In addition, the input string size will also be limited when passing the data from the URI to the GET methods. Thus for a query string in a URI, it’s impossible and inappropriate to process a large number of nested objects or collections.

Resolving Issue of Passing String List or Array

The source code downloaded from the original post would render the "no parameterless constructor defined" error when passing any string list or array to the Web API. The reason is that the model binder dynamically creates any child List<> or Array object for any content type with parameterless constructor whereas the System.String class doesn't have the parameterless constructor. To resolve the issue, the model binder creates a temporary physical List<string> or string[] object since the string is the known content type, and then adding the string item values directly to the List<> or Array object. The similar code pieces need to be placed in both top and recursive iterations to make passing string list or array work in the root and/or nested levels.

C#
//Check if List<string> or string[] type and 
//assign string value directly to list or array item.    
if (prop.ToString().Contains("[System.String]") || 
    prop.ToString().Contains("System.String[]"))
{
    var strList = new List<string>();
    foreach (var item in kvpsWork)
    {
        //Remove any brackets and enclosure from Key.
        var itemKey = Regex.Replace(item.Key, RexBrackets, "");
        if (itemKey == prop.Name)
        {
            strList.Add(item.Value);
            kvps.Remove(item);
        }
    }
    //Add list to parent property.                        
    if (prop.PropertyType.IsGenericType) prop.SetValue(obj, strList);
    else if (prop.PropertyType.IsArray) prop.SetValue(obj, strList.ToArray()); 
} 

The test request object models could be demonstrated as these:

C#
//For test of passing string list or array.
public class PagingSortRequest2
{
    public int PageIndex { get; set; }
    public int PageSize { get; set; }
    public string[] RootStrings { get; set; }
    public Sort2[] Sort2 { get; set; }
}
public class Sort2
{
    public string SortBy { get; set; }
    public SortDirection SortDirection { get; set; }
    public List<string> InStrings { get; set; }        
}

Then the test input field-value pair parameters should be like this (displayed as split lines):

 PageIndex=1
&PageSize=8
&RootStrings[0]=OK
&RootStrings[1]=Yes
&RootStrings[2]=456
&Sort2[0]SortBy=ProductName
&Sort2[0]SortDirection=descending
&Sort2[0]InStrings[0]=Search
&Sort2[0]InStrings[1]=Find
&Sort2[1]SortBy=CategoryID
&Sort2[1]SortDirection=0
&Sort2[1]InStrings[0]=Here
&Sort2[1]InStrings[1]=Also

The results from running the test item Pass for String List or Array Object with Get are shown here:

Image 6

Please note that only the System.String, not primitive types, is supported as the content type of the List<> or Array. If you try to pass something to request model with List<int>, it won't render any error but the values passed are not correct. This could also be fixed but passing string list or array is enough for targeted purposes. If needed, you can pass the string content type for any other content types in the List<> or Array to the Web API and then convert the types there.

Migrated to ASP.NET Core

The ASP.NET Core 2.0 version of the FieldValueModelBinder source code file is attached to this post. In the ASP.NET Core, the IModelBinder interface type comes from the Microsoft.AspNetCore.Mvc.ModelBinding namespace whereas in the ASP.NET Web API 2.0, it is a member of the System.Web.Http.ModelBinding. It's a major change since the HttpContext is now composed by a set of request features via the Kestrel web server, which breaks the compatibility to previous versions. You can add the FieldValueModelBinder.cs file into your ASP.NET Core 2.0 project with changing in the namespace and then use the same attribute type in the method arguments.

There are also detailed descriptions, model binder test cases, and even an entire sample application in my other article ASP.NET Core: A Multi-Layer Data Service Application Migrated from ASP.NET Web API. You can download the source code there with the test case file, TestCasesForModelBinder.txt, and run these test cases in a full-structured ASP.NET Core data service application.

Summary

The custom FieldValueModelBinder class presented in this article can be efficiently used for passing complex objects with query strings to Web API methods. It’s simple to use, especially for a GET method receiving a query string as a nesting object. For a nesting collection object, the FieldValueModelBinder class provides an option when using query string sources for any GET, POST or PUT methods.

History

  • 26th December, 2013
    • Original post
  • 4th May, 2015
  • 18th January, 2018
    • Added model binder source code file and section for ASP.NET Core 2.0

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
Shenwei is a software developer and architect, and has been working on business applications using Microsoft and Oracle technologies since 1996. He obtained Microsoft Certified Systems Engineer (MCSE) in 1998 and Microsoft Certified Solution Developer (MCSD) in 1999. He has experience in ASP.NET, C#, Visual Basic, Windows and Web Services, Silverlight, WPF, JavaScript/AJAX, HTML, SQL Server, and Oracle.

Comments and Discussions

 
SuggestionSuggested Changes Pin
Member 1305870714-Mar-17 10:50
Member 1305870714-Mar-17 10:50 
GeneralMy vote of 5 Pin
Ehsan Sajjad23-Feb-17 3:26
professionalEhsan Sajjad23-Feb-17 3:26 
Question很好的文章,大侠,能不能提供一个mvc的版本或者如果修改一下。 Pin
qiubo28-Nov-15 5:40
qiubo28-Nov-15 5:40 
AnswerRe: 很好的文章,大侠,能不能提供一个mvc的版本或者如果修改一下。 Pin
Shenwei Liu28-Nov-15 19:17
Shenwei Liu28-Nov-15 19:17 
QuestionExcelent Pin
Aladár Horváth6-Aug-15 21:57
professionalAladár Horváth6-Aug-15 21:57 
QuestionWeb API can handle complex objects too! Pin
Hiigara17-May-15 21:48
Hiigara17-May-15 21:48 
AnswerRe: Web API can handle complex objects too! Pin
Shenwei Liu19-May-15 8:23
Shenwei Liu19-May-15 8:23 
GeneralRe: Web API can handle complex objects too! Pin
Alaric_12-Apr-16 4:27
professionalAlaric_12-Apr-16 4:27 
GeneralRe: Web API can handle complex objects too! Pin
Shenwei Liu25-Apr-16 8:44
Shenwei Liu25-Apr-16 8:44 
AnswerRe: Web API can handle complex objects too! Pin
thummala_8426-Aug-18 2:53
thummala_8426-Aug-18 2:53 
Questionhow to bind only a single member of the ComplexSearchRequest class Pin
gaurish thakkar20-Mar-15 7:40
gaurish thakkar20-Mar-15 7:40 
awsome article ..... great help.How do we only control single member ComplexSearchRequest
C#
public List<PagingSortRequest> PagingRequest { get; set; }

to be modified by the ModelBinder class.

I have a ModelBinderClass and i want to populate a specific member which is a complex nested Object
Microsoft Certified Technology Specialist

AnswerRe: how to bind only a single member of the ComplexSearchRequest class Pin
Shenwei Liu4-May-15 9:24
Shenwei Liu4-May-15 9:24 
QuestionList<string> or string[] ? Pin
Crack^pT15-Oct-14 3:11
Crack^pT15-Oct-14 3:11 
AnswerRe: List<string> or string[] ? Pin
Shenwei Liu15-Oct-14 7:03
Shenwei Liu15-Oct-14 7:03 
GeneralRe: List<string> or string[] ? Pin
maccaron329-Apr-15 1:09
maccaron329-Apr-15 1:09 
GeneralRe: List<string> or string[] ? Pin
Shenwei Liu4-May-15 9:06
Shenwei Liu4-May-15 9:06 
Questionim using .net 4.0. but i got problem when converting ur code from 4.5 to 4.0 Pin
farisyusry29-Jun-14 15:46
farisyusry29-Jun-14 15:46 
AnswerRe: im using .net 4.0. but i got problem when converting ur code from 4.5 to 4.0 Pin
Shenwei Liu2-Jul-14 9:54
Shenwei Liu2-Jul-14 9:54 
QuestionThanks so much, there is some question. Pin
bit2001.lee21-Mar-14 1:59
bit2001.lee21-Mar-14 1:59 
QuestionGetting "does not implement the IModelBinder interface" error Pin
jeeshenlee15-Feb-14 20:32
jeeshenlee15-Feb-14 20:32 
AnswerRe: Getting "does not implement the IModelBinder interface" error Pin
Shenwei Liu16-Feb-14 6:15
Shenwei Liu16-Feb-14 6:15 
GeneralRe: Getting "does not implement the IModelBinder interface" error Pin
Member 114320685-Feb-15 22:58
Member 114320685-Feb-15 22:58 
SuggestionQuery String Limit and Hashcode Security ! Pin
darshan joshi29-Dec-13 20:25
darshan joshi29-Dec-13 20:25 
GeneralRe: Query String Limit and Hashcode Security ! Pin
Shenwei Liu30-Dec-13 5:49
Shenwei Liu30-Dec-13 5:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.