Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

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

0.00/5 (No votes)
17 Jan 2018 1  
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:

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:

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:

[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:

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

Or AngularJS code:

$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:

[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:

//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:

//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.

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:
    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.

    //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.

//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.

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.

//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.

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.
    public List<PagingSortRequest> PagingRequest { get; set; }
  2. Declaring a class object that inherits the base of List<> type.
    public PagingSortRequests PagingRequest { get; set; }

    The code for the class:

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

    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:

//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.

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.

<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.

//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:

//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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here