Click here to Skip to main content
Click here to Skip to main content

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

, 25 Dec 2013
Rate this:
Please Sign up or sign in to vote.
Presenting a custom model binder for passing query strings as nesting objects or collections to Web API GET or POST methods.

Download Source (495 KB

Introduction  

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

Below shows the 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 containng 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: 

 

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 appliction 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 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
        ... 
    },
    ...
});

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()


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

 

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: containing all of the source http data info.   
  2. System.Web.Http.ModelBinding.ModelBindingContext: containing all of the target object model info. 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 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. 

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


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.

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.  

License

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

About the Author

Shenwei Liu

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

 
Questionim using .net 4.0. but i got problem when converting ur code from 4.5 to 4.0 Pinmemberfarisyusry29-Jun-14 15:46 
AnswerRe: im using .net 4.0. but i got problem when converting ur code from 4.5 to 4.0 PinpremiumShenwei Liu2-Jul-14 9:54 
QuestionThanks so much, there is some question. Pinmemberbit2001.lee21-Mar-14 1:59 
QuestionGetting "does not implement the IModelBinder interface" error Pinmemberjeeshenlee15-Feb-14 20:32 
AnswerRe: Getting "does not implement the IModelBinder interface" error PinmemberShenwei Liu16-Feb-14 6:15 
SuggestionQuery String Limit and Hashcode Security ! Pinmemberdarshan joshi29-Dec-13 20:25 
GeneralRe: Query String Limit and Hashcode Security ! PinmemberShenwei Liu30-Dec-13 5:49 

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

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

| Advertise | Privacy | Mobile
Web03 | 2.8.140718.1 | Last Updated 26 Dec 2013
Article Copyright 2013 by Shenwei Liu
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid