CHAPTER 16

image

Binding Complex Data Types Part I

In Chapter 15, I showed you how to bind simple data types from the request URL using value providers. In this chapter, I describe model binders, which build on the foundation of value providers to allow binding of complex types. I describe the built-in model binders, which bind a comprehensive range of types, in this chapter. I also explain how to create and apply a custom model binder for dealing with types that the built-in binders cannot deal with.

Model binders work only with value providers, meaning that data values are obtained from the URL. It is media type formatters that are responsible for create complex types from the data in the request body. I introduced media type formatters in Chapter 12 when I showed you how data objects are serialized from action method results. In Chapter 17, I explain how media type formatters are used to deserialize data and create model objects for action method parameters. Table 16-1 summarizes this chapter.

Table 16-1. Chapter Summary

Problem

Solution

Listing

Bind an object from the URL.

Format the URL or query string so that the properties it contains correspond to the properties of the model class.

1–8

Broaden the source of values for model binding.

Use the ModelBinder interface to include value providers that implement the IUriValueProviderFactory interface.

9–12

Bind arrays of simple types.

Format the data so that the routing or query string properties have the same name.

13–17

Bind key-value pairs.

Format the data to use array-style indexers.

18–19

Create a custom model binder.

Implement the IModelBinder interface.

20–24

Apply a custom model binder.

Use the ModelBinder attribute and, optionally, add the binder to the services collection. You can also create a parameter binding rule to apply the model binder.

25–31

Instantiate a model class using data expressed in a single routing or query string property.

Create a type converter and apply it with the TypeConverter attribute.

32–35

Preparing the Example Project

I am going to continue working with the ExampleApp project I have been using throughout this part of the book. To prepare for this chapter, I have removed the statement in the WebApiConfig.cs file that I used in Chapter 17 to create a parameter binding rule. Listing 16-1 shows WebApiConfig.cs after I removed the statements.

Listing 16-1. The WebApiConfig.cs File

using System.Web.Http;
using ExampleApp.Infrastructure;
using System.Web.Http.ValueProviders;
using System.Net.Http.Headers;

namespace ExampleApp {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            config.DependencyResolver = new NinjectResolver();

            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "Binding Example Route",
                routeTemplate: "api/{controller}/{action}/{first}/{second}"
            );

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            config.Services.Add(typeof(ValueProviderFactory),
                new HeaderValueProviderFactory());
        }
    }
}

Image Tip  Remember that you don’t have to create the example project yourself. You can download the source code for every chapter for free from Apress.com.

Without the binding rule, Web API won’t be able to find a value for the accept parameter on the SumNumbers action method of the Binding controller. I will be focused on binding classes in this chapter, so I removed the parameter, as shown in Listing 16-2.

Listing 16-2. Removing an Action Method Parameter in the BindingsController.cs File

using System.Web.Http;
using ExampleApp.Models;
using System.Web.Http.ValueProviders;
using ExampleApp.Infrastructure;

namespace ExampleApp.Controllers {
    public class BindingsController : ApiController {
        private IRepository repo;

        public BindingsController(IRepository repoArg) {
            repo = repoArg;
        }

        [HttpGet]
        [HttpPost]
        public string SumNumbers([FromUri]Numbers numbers) {
            return string.Format("{0}", numbers.First + numbers.Second);
        }
    }
}

Notice that I have applied the FromUri attribute to the numbers parameter. I start this chapter by showing you how to bind complex type arguments from the URL, so I need to specify that the data values for the Numbers object should not be obtained from the request body.

The final preparation I need to make for this chapter is to change the Ajax request made in the bindings.js file so that it uses the GET verb and includes the model data in the query string, as shown in Listing 16-3.

Listing 16-3. Changing the Ajax Request Verb in the bindings.js File

var viewModel = ko.observable({ first: 2, second: 5 });
var response = ko.observable("Ready");
var gotError = ko.observable(false);

var sendRequest = function (requestType) {
    $.ajax("/api/bindings/sumnumbers", {
        type: "GET",
        data: viewModel(),
        success: function (data) {
            gotError(false);
            response("Total: " + data);
        },
        error: function (jqXHR) {
            gotError(true);
            response(jqXHR.status + " (" + jqXHR.statusText + ")");
        }
    });
};

$(document).ready(function () {
    ko.applyBindings();
});

To test the changes before proceeding to the rest of the chapter, start the application and use the browser to navigate to the /Home/Bindings URL. Click the Send Request button; the values in the input elements will be sent to the web services, and the results will be displayed at the top of the browser window, as illustrated in Figure 16-1.

9781484200865_Fig16-01.jpg

Figure 16-1. Preparing the example application

Using the Built-in Model Binders

In Chapter 15, I showed you how value providers are able to obtain data values from the URL to bind simple type parameters. Model binders build on the foundation of value providers to combine data values from the request into instances of complex types.

Image Tip  As a reminder, the simple types are TimeSpan, DateTime, Guid, string, char, bool, int, uint, byte, sbyte, short, ushort, long, ulong, float, double, and decimal. Any other type is a complex type, including arrays and collections of simple types.

Web API comes with a set of built-in model binders that can bind objects in the most common situations. The built-in binders are comprehensive enough that most applications don’t need customizations at all. In this section, I explain how the built-in model binders work and how you can adapt their behavior if you need customized binding support.

Table 16-2 lists the built-in binder classes that you will encounter most often. You don’t need to work directly with these classes, but they can be useful as a foundation when creating custom binders, as I describe in the “Working with Custom Model Binders” section later in this chapter. The classes listed in Table 16-2 are defined in the System.Web.Http.ModelBinding.Binders namespace.

Table 16-2. The Built-in Model Binder Classes

Name

Description

ArrayModelBinder

Binds an array of objects. See the “Binding Collections and Arrays” section for details.

CollectionModelBinder

Binds a strongly typed List or Enumerable. See the “Binding Collections and Arrays” section for details.

DictionaryModelBinder

Binds key-value pairs to a strongly typed Dictionary. See the “Binding Key-Value Pairs” section for details.

MutableObjectModelBinder

Binds objects. See the “Binding Objects” section for details.

TypeConverterModelBinder

Binds objects using a type converter, which I describe in the “Using Type Converters” section.

Later in this chapter, I explain how the overall model binding feature works and show you how to create and apply a custom model binder. Table 16-3 puts the default model binders in context.

Table 16-3. Putting the Default Model Binders in Context

Question

Answer

What are they?

The built-in model binders are used by Web API to instantiate classes, arrays, and collections using request data values obtained from value providers.

When should you use them?

The built-in model binders are used when the FromUri or ModelBinder attribute is used. See the “Broadening the Source of Binding Values” section for details of the difference between these two attributes.

What do you need to know?

Web API includes default model binders that can deal with instantiating and populating most classes. You should need to create a custom binder only when a class requires special care to instantiate.

Binding Objects

I am going to start by describing how Web API binds a single instance of a class to a parameter, not least because this is what is already happening when the SumNumbers action method is invoked in the example application.

I used the FromUri attribute in Chapter 15 to enable the binding of simple type parameters using value type providers. As I explained in the previous section, model binders build on value providers to get multiple values to create an object, and the FromUri attribute can enable this feature for complex type arguments, which is why I applied it to the numbers parameter.

...
public string SumNumbers([FromUri]Numbers numbers) {
...

The FromUri attribute isn’t a model binder, which is a class that is responsible for creating a specific type of object. Instead, FromUri is a model binding attribute, which tells Web API to use the model binder classes to create an instance of the parameter type, which is Numbers in this case.

A model binder is a class responsible for using one or more values from the value providers to create an instance of the model type, which is used as an argument when invoking the action method. The built-in model binder that deals with objects works in two steps:

  1. Use the parameterless constructor to create a new instance of the model type.
  2. Set each property defined by the model type using a value from the value providers.

These two steps are the reason why most model types are just a collection of automatically implemented properties: there is no point in defining a constructor with parameters because it will prevent the model binder from creating an instance and because methods and get-only properties will be ignored by the model binder. The Numbers class is a good, although simple, example.

...
public class Numbers {
    public int First { get; set; }
    public int Second { get; set; }
}
...

Image Note  Throughout this chapter, I will make changes to the bindings.js file to send different kinds of requests to the web service, but I don’t change the corresponding HTML in the Razor view because it is the format of the request that is important, not the ability of the user to change the data values used in the request. To test the changes, start the application, navigate to the /Home/Bindings URL, and click the Send Request button.

Binding Multiple Objects

By default, the object model binder tries to use the name of the parameter as a prefix when asking the value providers for values for each of the properties. In the case of my example, the parameter is called numbers, which means that the model binder will try to obtain values for numbers.first and numbers.second in the request.

If the value provides can’t obtain values for the prefixed names, then the model binder will ask for values without the prefix: first and second. This is the behavior I have been relying on in my examples.

Prefixes are useful because they allow a client to send data for multiple objects of the same type in the same URL. Listing 16-4 shows how I have changed the bindings.js file so that values for two Numbers objects are sent in the request query string.

Listing 16-4. Changing the Request Query String in the bindings.js File

var viewModel = ko.observable({ first: 2, second: 5, third: 10, fourth: 100 });
var response = ko.observable("Ready");
var gotError = ko.observable(false);

var sendRequest = function (requestType) {
    $.ajax("/api/bindings/sumnumbers", {
        type: "GET",
        data: {
            "numbers1.first": viewModel().first,
            "numbers1.second": viewModel().second,
            "numbers2.first": viewModel().third,
            "numbers2.second": viewModel().fourth
        },
        success: function (data) {
            gotError(false);
            response("Total: " + data);
        },
        error: function (jqXHR) {
            gotError(true);
            response(jqXHR.status + " (" + jqXHR.statusText + ")");
        }
    });
};

$(document).ready(function () {
    ko.applyBindings();
});

Image Tip  Notice that I have quoted the property names in the data settings object. The dot notation required to express a prefix can’t be used as a literal property name, but JavaScript is flexible enough to be able to define properties as quoted strings.

I have added two properties to the view model and used them to create an object for the data setting that groups them into two prefixed sets. The changes in Listing 16-4 will send a request with the following URL:

/api/bindings/sumnumbers?numbers1.first=2&numbers1.second=5&numbers2.first=10
    &numbers2.second=100

Image Tip  If you test the changes now, you will see that everything seems to work but that the result returned from the web service is zero. The problem is that the model binder has created an instance of the Numbers class, which has initialized the two int properties to zero, which is their default value. The binder then tries to find numbers.first and numbers.second values, which are not in the request. The binder drops the prefix and looks for first and second values, which are not in the request either. At this point, the binder gives up, and the action method is invoked with a Numbers object whose properties are set to zero. The binder makes a best-effort attempt to get values, and it assumes that it isn’t a problem when they don’t exist. You must use the model validation feature if you want to ensure that the request contains certain values. See Chapter 18 for details.

I need to update the action method so that it has two Numbers parameters whose names correspond to the prefixed included in the request URL, as shown in Listing 16-5.

Listing 16-5. Changing the Action Method Parameters in the BindingsController.cs File

...
[HttpGet]
[HttpPost]
public string SumNumbers([FromUri] Numbers numbers1, [FromUri] Numbers numbers2) {
    return string.Format("{0}", numbers1.First + numbers1.Second
        + numbers2.First + numbers2.Second);
}
...

Image Tip  I have used parameter names numbers1 and numbers2, but that isn’t required. You can use any parameter names you like and they will be used as prefixes when the binder is looking for property values.

Binding Nested Objects

Prefixes can also be used to define the structure of more complex objects. To demonstrate how this works, I have added a property to the Numbers class that is a complex type—in this case, an Operation object that is the other class defined in the BindingModels.cs file, as shown in Listing 16-6.

Listing 16-6. Adding a Property to the Numbers Class in the BindingModels.cs File

namespace ExampleApp.Models {

    public class Numbers {
        public int First { get; set; }
        public int Second { get; set; }
        public Operation Op { get; set; }
    }

    public class Operation {
        public bool Add { get; set; }
        public bool Double { get; set; }
    }
}

In Listing 16-7, you can see how I have modified the jQuery request so that it contains the prefixed values needed to create an instance of the modified Numbers class.

Listing 16-7. Changing the Request Query String in the bindings.js File

var viewModel = ko.observable({ first: 2, second: 5, add: true, double: true });
var response = ko.observable("Ready");
var gotError = ko.observable(false);

var sendRequest = function (requestType) {
    $.ajax("/api/bindings/sumnumbers", {
        type: "GET",
        data: {
            "numbers.first": viewModel().first,
            "numbers.second": viewModel().second,
            "numbers.op.add": viewModel().add,
            "numbers.op.double": viewModel().double
        },
        success: function (data) {
            gotError(false);
            response("Total: " + data);
        },
        error: function (jqXHR) {
            gotError(true);
            response(jqXHR.status + " (" + jqXHR.statusText + ")");
        }
    });
};

$(document).ready(function () {
    ko.applyBindings();
});

These changes create a request like this:

api/bindings/sumnumbers?numbers.first=2&numbers.second=5&numbers.op.add=true
    &numbers.op.double=true

Listing 16-8 shows the changes that I made to the action method to receive the data values from the query string via the model binder.

Listing 16-8. Changing the Action Method Parameters in the BindingsController.cs File

...
[HttpGet]
[HttpPost]
public string SumNumbers([FromUri] Numbers numbers) {
    var result = numbers.Op.Add ? numbers.First + numbers.Second
        : numbers.First - numbers.Second;
    return string.Format("{0}", numbers.Op.Double ? result * 2: result);
}
...

I don’t need to take any special steps to ensure that the model binder populates the properties of the nested Operations object because the model binder tries to locate values for it automatically.

Broadening the Source of Binding Values

A model binding attribute is a broker between a set of value provider factories and the model binding classes that can create different types. In using the FromUri attribute, I activated the model binding process, but I did so with a subset of the available value providers. As I explained in Chapter 15, the FromUri attribute filters out any value provider factory that doesn’t implement the IUriValueProviderFactory interface. I worked around this in Chapter 15 by implementing the interface in my custom value provider factory so that I could bind simple type parameters from request headers, but there is an alternative approach: you can use the ModelBinder attribute, from which the FromUri attribute is derived.

The only difference between the ModelBinder and FromUri attributes is that ModelBinder uses all of the available value provider factories. In this section, I demonstrate how to use the ModelBinder attribute so that values from individual model type properties can come from a broader range of value provider factories.

The first change is to add a new property to the model class that will correspond to a request header. Listing 16-9 shows the addition of an Accept property to the Numbers class. (Ignore that there is no good reason to mix headers with the int values in the Numbers class—it is the technique that is important in this example.)

Listing 16-9. Adding a Property to a Model Class in the BindingModels.cs File

...
public class Numbers {
    public int First { get; set; }
    public int Second { get; set; }
    public Operation Op { get; set; }
    public string Accept { get; set; }
}
...

I removed the IUriValueProviderFactory interface from the HeaderValueProviderFactory at the end of Chapter 15, but I still need to make some changes to the class. When I created the HeaderValueProviderFactory class, I implemented the GetValueProvider method so that it would return an instance of the HeaderValueProvider class only for POST requests. I am working with GET requests in this chapter, so I have removed statements from the GetValueProvider method so that the request verb isn’t checked, as shown in Listing 16-10.

Listing 16-10. Removing the HTTP Method Restriction in the HeaderValueProviderFactory.cs File

using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.ValueProviders;

namespace ExampleApp.Infrastructure {

    public class HeaderValueProviderFactory : ValueProviderFactory {
        public override IValueProvider GetValueProvider(HttpActionContext context) {
            //if (context.Request.Method == HttpMethod.Post) {
                return new HeaderValueProvider(new HeadersMap(context.Request.Headers));
            //} else {
            //    return null;
            //}
        }
    }
}

I also need to modify the HeaderValueProvider class so that is able to cope with prefixes. Listing 16-11 shows the changes.

Listing 16-11. Adding Prefix Support in the HeaderValueProvider.cs File

using System.Globalization;
using System.Web.Http.ValueProviders;
using System.Linq;

namespace ExampleApp.Infrastructure {
    public class HeaderValueProvider : IValueProvider {
        private HeadersMap headers;

        public HeaderValueProvider(HeadersMap map) {
            headers = map;
        }

        public ValueProviderResult GetValue(string key) {
            string value = headers[key.Split('.').Last()];
            return value == null
                ? null
                : new ValueProviderResult(value, value, CultureInfo.InvariantCulture);
        }

        public bool ContainsPrefix(string prefix) {
            return false;
        }
    }
}

The binder starts by calling the GetPrefix method of the value providers to see whether any of them can process requests with the prefix numbers. Since the request data contains this prefix, the value provider responsible for managing the query string returns true, and the binder requests values for the numbers.first and numbers.second. The binder then repeats the process for the numbers.Op prefix. Finally, the binder tries to get a value for numbers.Accept.

As you can see in Listing 16-11, I used the Add method to register the HeaderValueProvider class with the services collection, and that means the built-in value providers are queried before my custom class. The effect of this is that the ContainsPrefix method isn’t called because the query string value provider is asked first and is able to provide all of the values that the binder needs, with the exception of numbers.Accept. The change I made to the GetValue method splits up the request property name and extracts the last component so that I can match it to a header, providing the header with the information it needs.

Image Tip  You might wonder why my GetValue method is asked for numbers.Accept when the ContainsPrefix method always returns false. This happens because the model binder is given access to only a single value provider, so Microsoft has defined a composite provider that consolidates the results from all of the registered value providers. The model binder is told by the consolidated provider that it can produce values with the numbers prefix because the query string value provider says it can—and that affirmation is therefore applied to all of the value providers.

The final step is to modify the controller so that it uses the ModelBinder attribute, and the result it returns to the client includes the value of the Accept request header, as shown in Listing 16-12.

Listing 16-12. Updating the Action Method in the BindingsController.cs File

using System.Web.Http;
using ExampleApp.Models;
using System.Web.Http.ValueProviders;
using ExampleApp.Infrastructure;
using System.Web.Http.ModelBinding;

namespace ExampleApp.Controllers {
    public class BindingsController : ApiController {
        private IRepository repo;

        public BindingsController(IRepository repoArg) {
            repo = repoArg;
        }

        [HttpGet]
        [HttpPost]
        public string SumNumbers([ModelBinder] Numbers numbers) {
            var result = numbers.Op.Add
                ? numbers.First + numbers.Second
                : numbers.First - numbers.Second;

            return string.Format("{0} (Accept:{1})",
                numbers.Op.Double ? result * 2 : result, numbers.Accept);
        }
    }
}

The use of the ModelBinder attribute means that all of the value provider factories are used to obtain sources of data, including the custom provider that provides access to the request headers.

Image Tip  The name of the action method parameter is used as the prefix by default, but you can use the Name property when applying the ModelBinder attribute to specify another prefix. See the “Applying a Custom Model Binder” section for more information on using the ModelBinder attribute.

Binding Collections and Arrays

The built-in model binders are able to bind multiple related values to create collections and arrays. In Listing 16-13, you can see that I have changed the query string data sent by the client so that it includes a sequence of numeric values.

Listing 16-13. Changing the Request Data in the bindings.js File

var viewModel = ko.observable({ first: 2, second: 5, third: 100});
var response = ko.observable("Ready");
var gotError = ko.observable(false);

var sendRequest = function (requestType) {
    $.ajax("/api/bindings/sumnumbers", {
        type: "GET",
        data: {
            numbers: [viewModel().first, viewModel().second, viewModel().third]
        },
        success: function (data) {
            gotError(false);
            response("Total: " + data);
        },
        error: function (jqXHR) {
            gotError(true);
            response(jqXHR.status + " (" + jqXHR.statusText + ")");
        }
    });
};

$(document).ready(function () {
    ko.applyBindings();
});

These changes produce a request that targets the following URL:

/api/bindings/sumnumbers?numbers[]=2&numbers[]=5&numbers[]=100

Image Tip  The [ and ] characters are escaped when the request is sent in this format and replaced with the %5B and %5D sequences.

You can omit the square brackets by setting the jQuery traditional Ajax setting to true, which will send the request in this format (both are accepted by Web API).

/api/bindings/sumnumbers?numbers=2&numbers=5&numbers=100

I have changed the SumNumbers action method to receive the array of data values, as shown in Listing 16-14.

Listing 16-14. Binding Request Data As an Array in the BindingsController.cs File

using System.Web.Http;
using ExampleApp.Models;
using System.Web.Http.ValueProviders;
using ExampleApp.Infrastructure;
using System.Web.Http.ModelBinding;
using System.Linq;

namespace ExampleApp.Controllers {
    public class BindingsController : ApiController {
        private IRepository repo;

        public BindingsController(IRepository repoArg) {
            repo = repoArg;
        }

        [HttpGet]
        [HttpPost]
        public string SumNumbers([ModelBinder] int[] numbers) {
            return numbers.Sum().ToString();
        }
    }
}

The process of creating and populating the array is handled by the model binder and passed to the action method. You can elect to receive the same data as a strongly typed List, as shown in Listing 16-15.

Listing 16-15. Binding Request Data As a Strongly Typed Collection in the BindingsController.cs File

using System.Web.Http;
using ExampleApp.Models;
using System.Web.Http.ValueProviders;
using ExampleApp.Infrastructure;
using System.Web.Http.ModelBinding;
using System.Linq;
using System.Collections.Generic;

namespace ExampleApp.Controllers {
    public class BindingsController : ApiController {
        private IRepository repo;

        public BindingsController(IRepository repoArg) {
            repo = repoArg;
        }

        [HttpGet]
        [HttpPost]
        public string SumNumbers([ModelBinder] List<int> numbers) {
            return numbers.Sum().ToString();
        }
    }
}

Image Tip  You can also bind to a strongly typed Enumerable, such as Enumerable<T>, by changing the type of the action method parameter.

Binding Arrays and Lists of Complex Types

The approach I used in the previous section can be combined with the use of prefixes to bind arrays of complex types. Listing 16-16 shows the changes I made to the bindings.js file so that jQuery sends properties that will correspond to an array of Numbers objects.

Listing 16-16. Changing the Request Data in the bindings.js File

var viewModel = ko.observable({ first: 2, second: 5, third: 100, fourth: 200});
var response = ko.observable("Ready");
var gotError = ko.observable(false);

var sendRequest = function (requestType) {
    $.ajax("/api/bindings/sumnumbers", {
        type: "GET",
        data: {
            "numbers": [{ first: viewModel().first, second: viewModel().second },
                    { first: viewModel().third, second: viewModel().fourth }],
        },
        success: function (data) {
            gotError(false);
            response("Total: " + data);
        },
        error: function (jqXHR) {
            gotError(true);
            response(jqXHR.status + " (" + jqXHR.statusText + ")");
        }
    });
};

$(document).ready(function () {
    ko.applyBindings();
});

I have set the data property to an object that has a property called numbers, which in turn is set to an array of objects with first and second properties. The result is a request in this format:

/api/bindings/sumnumbers?numbers[0][first]=22&numbers[0][second]=5
   &numbers[1][first]=100&numbers[1][second]=200

The built-in binders work out the relationships between the different data items and use them to create an array of objects. Listing 16-17 shows the corresponding changes to the action method to receive an array of Numbers objects.

Listing 16-17. Receiving an Array of Complex Objects in the BindingsController.cs File

using System.Web.Http;
using ExampleApp.Models;
using System.Web.Http.ValueProviders;
using ExampleApp.Infrastructure;
using System.Web.Http.ModelBinding;
using System.Linq;
using System.Collections.Generic;

namespace ExampleApp.Controllers {
    public class BindingsController : ApiController {
        private IRepository repo;

        public BindingsController(IRepository repoArg) {
            repo = repoArg;
        }

        [HttpGet]
        [HttpPost]
        public string SumNumbers([ModelBinder] Numbers[] numbers) {
            return numbers.Select(x => x.First + x.Second).Sum().ToString();
        }
    }
}

Image Caution  You must ensure that there are no gaps in the index values for array items. The binder stops looking for data when it fails to get a value for a specific index. If your data jumps from numbers[1] to numbers[3], for example, then the binder will fail to get a value for numbers[2] and never ask for numbers[3] or any subsequent item.

Binding Key-Value Pairs

The built-in binders are able to create a strongly typed Dictionary that contains key-value pairs. Listing 16-18 shows the changes that I made to the binders.js file to send a request with data in the format that the Dictionary binder looks for.

Listing 16-18. Sending Key-Value Request Data in the bindings.js File

var viewModel = ko.observable({ first: 2, second: 5, third: 100, fourth: 200 });
var response = ko.observable("Ready");
var gotError = ko.observable(false);

var sendRequest = function (requestType) {
    $.ajax("/api/bindings/sumnumbers", {
        type: "GET",
        data: { numbers: [{ key: "one", value: { first: viewModel().first,
                                second: viewModel().second }},
                          { key: "two", value: { first: viewModel().third,
                                second: viewModel().fourth }}]},
        success: function (data) {
            gotError(false);
            response("Total: " + data);
        },
        error: function (jqXHR) {
            gotError(true);
            response(jqXHR.status + " (" + jqXHR.statusText + ")");
        }
    });
};

$(document).ready(function () {
    ko.applyBindings();
});

The format of the object used for the data setting contains a number property (named so that the binder will match it to the action method parameter) that is set to an array of objects that has key and value properties. For this example, I am going to bind this data to a Dictionary<string, Numbers> object, and you can see that I have set the key and value properties accordingly. The key will be left as a string (although I could have used data that could be bound to any type), and the value is set to an object that has first and second properties so that it can be bound to a Numbers object. The changes in Listing 16-18 create a request with this format URL.

/api/bindings/sumnumbers?numbers[0][key]=one&numbers[0][value][first]=2
    &numbers[0][value][second]=52&numbers[1][key]=two&numbers[1][value][first]=100
    &numbers[1][value][second]=200

In Listing 16-19, you can see how I receive the dictionary in the action method.

Listing 16-19. Receiving Key-Value Pairs in the BindingsController.cs File

using System.Web.Http;
using ExampleApp.Models;
using System.Web.Http.ValueProviders;
using ExampleApp.Infrastructure;
using System.Web.Http.ModelBinding;
using System.Linq;
using System.Collections.Generic;

namespace ExampleApp.Controllers {
    public class BindingsController : ApiController {
        private IRepository repo;

        public BindingsController(IRepository repoArg) {
            repo = repoArg;
        }

        [HttpGet]
        [HttpPost]
        public string SumNumbers([ModelBinder] Dictionary<string, Numbers> numbers) {
            return numbers.Select(x => x.Value.First + x.Value.Second).Sum().ToString();
        }
    }
}

You can mix and match the techniques in this part of the chapter, and the binders will usually be able to figure it out. You can, for example, send a collection of key-value pairs where the value is an array of complex types that has a property that is an array of key-value pairs and so on.

Working with Custom Model Binders

As I demonstrated in the previous section, the built-in model binders are capable of dealing with a good range of bindings. But not every scenario is catered for; the main limitation is that classes can be instantiated only if they have a parameterless constructor, and data values can be set only through properties, for example. In the sections that follow, I explain how model binders work and show you how custom model binders can be used to address situations that the default binders are unable to deal with. Table 16-4 puts custom model binders in context.

Table 16-4. Putting Custom Model Binders in Context

Question

Answer

What are they?

Custom model binders allow classes that require special handling to be included in the model binding process.

When should you use them?

Use custom model binders for classes that don’t have parameterless constructors or require any kind of special handling.

What do you need to know?

Custom model binders are reasonably straightforward, but be careful when handling prefixes.

Preparing the Application

The main reason to create a custom binder is to instantiate a class that the built-in binders cannot handle. This is most often the case when there is no parameterless constructor or when a particular initialization process must be performed. I see this most often with some object-relational mapping (ORM) systems that need to create the objects they operate on so they can track changes to data values. I am going to create a model binder for the Numbers class. I want to make the example more realistic, and I made some changes to the class, as shown in Listing 16-20.

Listing 16-20. Changing the Numbers Classin the BindingModels.cs File

namespace ExampleApp.Models {

    public class Numbers {
        private int first, second;

        public Numbers(int firstVal, int secondVal) {
            first = firstVal; second = secondVal;
        }

        public int First {
            get { return first; }
        }

        public int Second {
            get { return second; }
        }

        public Operation Op { get; set; }
        public string Accept { get; set; }
    }

    public class Operation {
        public bool Add { get; set; }
        public bool Double { get; set; }
    }
}

I have added a constructor that requires parameters and changed two of the properties that are read-only. These changes will prevent the default model binder from being able to create instances of the application, as I will demonstrate shortly. In Listing 16-21, you can see how I revised the action method in the Bindings controller so that it receives a Numbers object as a parameter.

Listing 16-21. Changing the Action Method Parameters in the BindingsController.cs File

using System.Web.Http;
using System.Web.Http.ModelBinding;
using ExampleApp.Models;

namespace ExampleApp.Controllers {
    public class BindingsController : ApiController {
        private IRepository repo;

        public BindingsController(IRepository repoArg) {
            repo = repoArg;
        }

        [HttpGet]
        [HttpPost]
        public string SumNumbers([ModelBinder] Numbers numbers) {
            var result = numbers.Op.Add ? numbers.First + numbers.Second
                : numbers.First - numbers.Second;
            return string.Format("{0}", numbers.Op.Double ? result * 2 : result);
        }
    }
}

The final preparatory change I need to make is to change the data sent by jQuery in the Ajax request. Listing 16-22 shows how I have returned to sending first and second properties, both of which are prefixed with numbers, matching the name of the action method parameter. I have also included the numbers.op.sum and numbers.op.double properties so I can populate the nested Operation object.

Listing 16-22. Changing the Ajax Request Data in the bindings.js File

var viewModel = ko.observable({ first: 2, second: 5});
var response = ko.observable("Ready");
var gotError = ko.observable(false);

var sendRequest = function (requestType) {
    $.ajax("/api/bindings/sumnumbers", {
        type: "GET",
        data: {
            "numbers.first": viewModel().first,
            "numbers.second": viewModel().second,
            "numbers.op.add": true,
            "numbers.op.double": true
        },
        success: function (data) {
            gotError(false);
            response("Total: " + data);
        },
        error: function (jqXHR) {
            gotError(true);
            response(jqXHR.status + " (" + jqXHR.statusText + ")");
        }
    });
};

$(document).ready(function () {
    ko.applyBindings();
});

Testing the Preparations

These changes have created a binding situation that the built-in model binders can’t deal with: the SumNumbers action method has a Numbers parameter, but the Numbers class doesn’t follow the default pattern to be instantiated. You can see the effect by starting the application, navigating to /Home/Bindings in the browser, and clicking the Send Request button.

The response from the web service will be reported in the browser as a 500 (Internal Server Error), and if you look at the response in the browser F12 tools, you will see that the following problem has been reported:

No parameterless constructor defined for this object

I show you how to deal with errors in Chapter 25, but for the moment it is enough to have confirmed that the built-in model binders can’t instantiate the modified Numbers class.

Understanding Model Binders

Model binders implement the IModelBinder interface, which is defined in the System.Web.Http.ModelBinding namespace. Listing 16-23 shows the definition of the IModelBinder interface.

Listing 16-23. The IModelBinder Interface

using System.Web.Http.Controllers;

namespace System.Web.Http.ModelBinding {

    public interface IModelBinder {
        bool BindModel(HttpActionContext actionContext,
            ModelBindingContext bindingContext);
    }
}

The IModelBinder interface defines a single method called BindModel. The way this model works is a little convoluted. The result and the first argument are entirely standard: the bool result is used to indicate whether the model binder was able to create an instance of the require type, and the HttpActionContext object. describes the action method that defines the parameter that is to be bound. Table 16-5 shows the properties and methods that are defined by the HttpActionDescriptor class. that are useful in model binding; there are additional method and properties, but they are used when selecting and executing an action method, which I describe in Chapter 22.

Table 16-5. Selected Members Defined by the HttpActionDescriptor Class

Name

Description

ActionName

Returns the name of the action method

ReturnType

Returns the Type that the action method returns

SupportedHttpMethods

Returns a collection of HttpMethod objects that represent the HTTP verbs that can be used to target the action method

GetParameters()

Returns a collection of HttpParameterDescription objects that represent the action method parameters

The ModelBindingContext argument is different: it describes the parameter for which a value is required, but it also provides the means by which the value for the parameter is given to Web API and through which any errors are expressed. This will make more sense as I demonstrate the process of creating a custom model binder, but just bear in mind that the ModelBindingContext class provides information to the model binder and provides the parameter value to Web API so that the action method can be invoked. The ModelBindingContext class defines the properties shown in Table 16-6.

Table 16-6. Selected Properties Defined by the ModelBindingContext Class

Name

Description

FallbackToEmptyPrefix

Returns true if the model binder can ignore the binding prefix.

Model

Set by the model binder when it is able to create an instance of the model class.

ModelMetadata

Returns a ModelMetadata object that describes the type of the parameter that is to be bound.

ModelName

Returns the name of the parameter that is to be bound.

ModelState

Returns a ModelStateDictionary object that is used to perform validation. See Chapter 18 for details.

ModelType

Returns the type of the parameter that is bound.

PropertyMetadata

Provides a dictionary of ModelMetadata objects that describe each property defined by the model type, indexed by name.

ValidationNode

Returns a ModelValidationNode object used to perform validation. See Chapter 18 for details.

ValueProvider

Returns an IValueProvider that can be used to obtain individual data values from the request. The IValueProvider that is returned by default consolidates access to all the individual value providers that have been registered in the services container or via dependency injection.

The role of the model binder is to examine the action method and the parameter using the HttpActionDescriptor and ModelBindingContext and, if suitable data is available, create an instance of the class—the model—specified by the ModelBindingContext.Type property. The model is provided to Web API by setting the ModelBindingContext.Model property and returning true as the result from the BindModel method.

Image Tip  If suitable data isn’t available, then the ModelBinding.ModelState property is used to report errors. I explain how models are validated and how errors are handled in Chapter 18.

Creating a Custom Model Binder

There are two categories of model binders. The first is loosely coupled binders., which use the metadata in the HttpActionDescriptor and ModelBindingContext objects passed to the BindModel method to instantiate classes of which they have no prior knowledge. The built-in model binders are loosely coupled because they will try to bind any complex action method parameter, but the limitation of this approach is that they can’t deal with classes that have constructor parameters or require special configuration.

The other category is tightly coupled binders, which are written to handle a specific class. Tightly coupled binders have prior knowledge of the steps required to create and configure a particular class and usually don’t need to use the metadata in order to do so. The problem with tightly coupled classes is that they break when the class they operate on changes, but this is usually an acceptable trade-off in order to be able to use model binding for difficult classes. It is tightly coupled binders that most applications require and that I demonstrate in this section.

Image Caution  Loosely coupled classes are difficult to write and require thorough testing because they will be used to bind all sorts of odd classes that have characteristics that have not been foreseen. You should rely on the built-in binders unless you have expert-level understanding of .NET reflection and metadata and you are willing to set aside a substantial amount of time for making your binders work.

My tightly coupled binder will create instances of the Numbers class, which means I need to extract several values from the request and use them to create and populate a Numbers object, as described in Table 16-7.

Table 16-7. The Request Properties Required by a Numbers Model Binder

Name

Description

numbers.first

Required to set the First property defined by the Numbers class, set via the constructor

numbers.second

Required to set the Second property defined by the Numbers class, set via the constructor

numbers.op.sum

Required to set the Sum property of the Operation class, set via the Op property

numbers.op.double

Required to set the Double property of the Operation class, set via the Op property

numbers.accept

Required to set the Accept property defined by the Numbers class

I added a class file called NumbersBinder.cs to the Infrastructure folder and used it to define the model binder shown in Listing 16-24.

Listing 16-24. The Contents of the NumbersBinder.cs File

using System.Collections.Generic;
using System.Linq;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
using System.Web.Http.ValueProviders;
using ExampleApp.Models;

namespace ExampleApp.Infrastructure {

    public class NumbersBinder : IModelBinder {

        public bool BindModel(HttpActionContext actionContext,
                ModelBindingContext bindingContext) {

            string modelName = bindingContext.ModelName;

            Dictionary<string, ValueProviderResult> data
                = new Dictionary<string, ValueProviderResult>();

            data.Add("first", GetValue(bindingContext, modelName, "first"));
            data.Add("second", GetValue(bindingContext, modelName, "second"));
            data.Add("add", GetValue(bindingContext, modelName, "op", "add"));
            data.Add("double", GetValue(bindingContext, modelName, "op", "double"));
            data.Add("accept", GetValue(bindingContext, modelName, "accept"));

            if (data.All(x => x.Value != null)) {
                bindingContext.Model = CreateInstance(data);
                return true;
            }
            return false;
        }

        private ValueProviderResult GetValue (ModelBindingContext context,
                params string[] names) {

            for (int i = 0; i < names.Length -1; i++) {
                string prefix = string.Join(".",
                    names.Skip(i).Take(names.Length - (i + 1)));
                if (context.ValueProvider.ContainsPrefix(prefix)) {
                    return context.ValueProvider.GetValue(prefix + "." + names.Last());
                }
            }
            return context.ValueProvider.GetValue(names.Last());
        }

        private Numbers CreateInstance(Dictionary<string, ValueProviderResult> data) {
            return new Numbers(Convert<int>(data["first"]),
                    Convert<int>(data["second"])) {
                Op = new Operation {
                    Add = Convert<bool>(data["add"]),
                    Double = Convert<bool>(data["double"])
                },
                Accept = Convert<string>(data["accept"])
            };
        }

        private T Convert<T>(ValueProviderResult result) {
            try {
                return (T)result.ConvertTo(typeof(T));
            } catch {
                return default(T);
            }
        }
    }
}

Image Caution  Model binders can be used to service multiple requests. Don’t use instance variables when writing a model binder, but ensure that you write thread-safe code and reset the shared state after if you can’t avoid instance variables.

This binder is a little more complex than it needs to because I have structured the code to break up the steps a binder has to follow. In the sections that follow, I use that structure to explain each step.

Getting Model Property Values from the Value Provider

The first step that my binder takes is to try to locate values for each of the properties that it needs to create an instance of the Numbers class and that I listed in Table 16-7. I defined the GetValue method in the binder, which receives the ModelBindingContext and an array of strings as its arguments.

...
private ValueProviderResult GetValue (ModelBindingContext context,
        params string[] names) {

    for (int i = 0; i < names.Length -1; i++) {
        string prefix = string.Join(".",
            names.Skip(i).Take(names.Length - (i + 1)));
        if (context.ValueProvider.ContainsPrefix(prefix)) {
            return context.ValueProvider.GetValue(prefix + "." + names.Last());
        }
    }
    return context.ValueProvider.GetValue(names.Last());
}
...

In Chapter 15, I explained that value providers will return a ValueProviderResult object if they are able to provide a value and null if not. My first job is to try to gather the set of ValueProviderResult results that contains the values I need, and I need to do this in a way that deals with the prefixes that the client sends.

I have taken a different approach to dealing with prefixes than Microsoft has used in the built-in binders. Instead of checking each prefix just once, I handle each property independently and try to locate a value for multiple levels of prefix. So, for example, if I want the numbers.op.add property, I request the following:

  1. numbers.op.add
  2. op.add
  3. add

I receive the prefixes and name using a params argument, which makes it easy for me to use LINQ to generate the property name permutations I look for. I check these values with the value providers through the ModelBindingContext.ValueProvider property, which returns an IValueProvider that queries all of the value providers registered in the service collection. I terminate the search as soon as I get a ValueProviderResult object for one of the prefix/name permutations and return it as the result.

I call the GetValue method from the GetBinding method to create a dictionary of ValueProviderResult objects that are indexed by property name, like this:

...
data.Add("first", GetValue(bindingContext, modelName, "first"));
data.Add("second", GetValue(bindingContext, modelName, "second"));
data.Add("add", GetValue(bindingContext, modelName, "op", "add"));
data.Add("double", GetValue(bindingContext, modelName, "op", "double"));
data.Add("accept", GetValue(bindingContext, modelName, "accept"));
...

Checking Values

Once I have asked the value providers for each of the properties, I have a collection of responses that can be either ValueProviderResult objects (indicating that the provider located a value) or null (indicating that the provider could not locate a value). This is the point at which I have to decide whether I am able to bind the model and so I perform a basic check to ensure that I have not received any null responses, like this:

...
if (data.All(x => x.Value != null)) {
    bindingContext.Model = CreateInstance(data);
    return true;
}
return false;
...

I use LINQ to check for null values, and I return false if there are. A false result from the BindModel method tells Web API that the binder can’t create an instance of the model object.

Image Note  Web API provides a model validation mechanism that allows errors to be usefully reported to the user. I am focused solely on the binding process in this chapter, but I describe model validation and validation errors in Chapter 18.

Creating the Model Object

I use a method called CreateInstance if there are no null responses from the value providers. As its name suggests, the CreateInstance method is responsible for creating an instance of the Numbers class and populating it with data.

An important task when creating an instance of the model object is to convert the values from the ValueProviderResult objects into the types required for the constructor, methods, and properties. In the custom binder, I have separated this step into a strongly typed method called Convert, as follows:

...
private Numbers CreateInstance(Dictionary<string, ValueProviderResult> data) {
    return new Numbers(Convert<int>(data["first"]), Convert<int>(data["second"])) {
        Op = new Operation {
            Add = Convert<bool>(data["add"]),
            Double = Convert<bool>(data["double"])
        },
        Accept = Convert<string>(data["accept"])
    };
}
...

The CreateInstance method creates the Numbers object, but it gets its values by calling the Convert method and specifying the required type using a generic type parameter and the ValueProviderResult. The Convert method uses the ValueProviderResult.ConvertTo method to perform the type conversion.

...
private T Convert<T>(ValueProviderResult result) {
    try {
        return (T)result.ConvertTo(typeof(T));
    } catch {
        return default(T);
    }
}
...

The ConvertTo method will throw an exception if the value cannot be converted. Handling the conversion in a strongly typed method lets me use the default keyword to provide the caller with a default value for the required type. In Chapter 18, I show you how to report binding problems as part of the model validation process.

Applying a Custom Model Binder

Having created a custom model binder, I need to tell Web API to use it to bind Numbers action method parameters. There are several ways to apply a binder, depending on how widely you want to apply the binding process. Web API looks in three different places for a model binding instruction before using the built-in binders, in order:

  1. The ModelBinder attribute applied to the action method parameter
  2. The ModelBinder attribute applied to the model class
  3. A parameter binding rule

In the sections that follow, I explain each of the options and demonstrate their use.

Applying a Custom Binder Directly to the Parameter

The most direct way to apply a model binder is to specify the binder type to the action method parameter using the ModelBinder attribute.., which defines the configuration properties described in Table 16-8.

Table 16-8. The Properties Defined by the ModelBinder Attribute

Name

Description

BinderType

This property specifies the model binder class that will be used for the parameter.

Name

This property specifies the name that will be used as the top-level prefix, overriding the name of the parameter, which is used by default.

Image Tip  There is an additional property—SupressPrefixCheck—defined by the ModelBinder attribute, but its value is not checked by the other model binding classes.

Listing 16-25 shows how I applied the ModelBinder attribute and set the BinderType property to specify the NumbersBinder class for the action method parameter.

Listing 16-25. Applying a Custom Model Binder in the BindingsController.cs File

using System.Web.Http;
using System.Web.Http.ModelBinding;
using ExampleApp.Models;
using ExampleApp.Infrastructure;

namespace ExampleApp.Controllers {
    public class BindingsController : ApiController {
        private IRepository repo;

        public BindingsController(IRepository repoArg) {
            repo = repoArg;
        }

        [HttpGet]
        [HttpPost]
        public string SumNumbers([ModelBinder(BinderType=typeof(NumbersBinder))]
                Numbers numbers) {
            var result = numbers.Op.Add ? numbers.First + numbers.Second
                : numbers.First - numbers.Second;
            return string.Format("{0}", numbers.Op.Double ? result * 2 : result);
        }
    }
}

I have not set the Name property, so the metadata passed to the binder will specify that the name of the action method parameter will be used as a prefix. To test the custom model binder, start the application, use the browser to navigate to the /Home/Bindings URL, and click the Send Request button. The ModelBinder.BinderType property will override the use of the built-in binders and use my custom binder to instantiate the Numbers class, avoiding the problems with the constructor parameters and read-only properties.

Registering the Model Binder with the Services Collection

If you don’t want to use the BinderType property every time you apply the ModelBinder attribute, you can register the binder with the services collection. Listing 16-26 shows the change I made to the WebApiConfig.cs file to register the binder.

Listing 16-26. Registering a Model Binder in the WebApiConfig.cs File

using System.Web.Http;
using ExampleApp.Infrastructure;
using System.Web.Http.ValueProviders;
using System.Net.Http.Headers;
using System.Web.Http.ModelBinding;
using System.Web.Http.ModelBinding.Binders;
using ExampleApp.Models;

namespace ExampleApp {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            config.DependencyResolver = new NinjectResolver();

            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "Binding Example Route",
                routeTemplate: "api/{controller}/{action}/{first}/{second}"
            );

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            config.Services.Add(typeof(ValueProviderFactory),
                new HeaderValueProviderFactory());

            config.Services.Insert(typeof(ModelBinderProvider), 0,
                new SimpleModelBinderProvider(typeof(Numbers), new NumbersBinder()));
        }
    }
}

A subclass of the abstract ModelBinderProvider class. is required to register a model binder, but it is easier to use the SimpleModelBinderProvider class, defined in the System.Web.Http.ModelBinding.Providers namespace, when you want a binder to be used for all parameters of a specific type. The arguments to the SimpleModelBinderProvider constructor are the type the binder instantiates and an instance of the model binder class.

Image Tip  I have used the Insert method to register the custom model binder so that it is used in preference to the built-in binders, which will not be able to instantiate the Numbers class.

Having registered the binder, I am able to remove the type from the attribute applied to the action method parameter, as shown in Listing 16-27.

Listing 16-27. Removing the Model Binder Type in the BindingsController.cs File

...
[HttpGet]
[HttpPost]
public string SumNumbers([ModelBinder] Numbers numbers) {
    var result = numbers.Op.Add ? numbers.First + numbers.Second
        : numbers.First - numbers.Second;
    return string.Format("{0}", numbers.Op.Double ? result * 2 : result);
}
...

Applying a Binder to the Model Class

The ModelBinder attribute can also be applied to the model class, which has the effect of applying the model binder to every action method parameter of that type. Listing 16-28 shows the application of the ModelBinder attribute to the Numbers model class, with the BinderType property used to specify the custom model binder.

Listing 16-28. Using the ModelBinder Attribute in the BindingModels.cs File

using System.Web.Http.ModelBinding;
using ExampleApp.Infrastructure;

namespace ExampleApp.Models {

    [ModelBinder(BinderType = typeof(NumbersBinder))]
    public class Numbers {
        private int first, second;

        public Numbers(int firstVal, int secondVal) {
            first = firstVal; second = secondVal;
        }

        public int First {
            get { return first; }
        }

        public int Second {
            get { return second; }
        }

        public Operation Op { get; set; }
        public string Accept { get; set; }
    }

    public class Operation {
        public bool Add { get; set; }
        public bool Double { get; set; }
    }
}

Image Tip  I have shown the attribute with the BinderType property because I like to make it obvious which binder will be used. However, registering the binder with the services collection affects the ModelBinder attribute wherever it is used, and you can omit the BinderType property if you prefer.

There is no need to apply the attribute to the action method when the ModelBinder attribute is applied to the model class. Listing 16-29 shows how I removed the attribute from the controller.

Listing 16-29. Removing the ModelBinding Attribute in the BindingsController.cs File

...
[HttpGet]
[HttpPost]
public string SumNumbers(Numbers numbers) {
    var result = numbers.Op.Add ? numbers.First + numbers.Second
        : numbers.First - numbers.Second;
    return string.Format("{0}", numbers.Op.Double ? result * 2 : result);
}
...

Creating a Parameter Binding Rule

Applying the ModelBinder attribute to the model class affects all the action method parameters of that type, and that can be overreaching if you need to apply a custom binder only for some action method parameters. You can get more control over when the model binder is used by defining a parameter binding rule. However, the main benefit of a parameter binding rule for a model binder is to restrict the set of value providers that are used to obtain data values, which can be useful when you want to make sure that, say, routing data isn’t used in the model binding process.

Before I define a binding rule, I need to remove the ModelBinder attribute from the Numbers class, as shown in Listing 16-30. With the attribute in place, the binding process will not look for a parameter binding rule.

Listing 16-30. Removing the ModelBinder Attribute in the BinderModels.cs File

...
//[ModelBinder(BinderType = typeof(NumbersBinder))]
public class Numbers {
        private int first, second;
...

Listing 16-31 shows a parameter binding rule that I added to the WebApiConfig.cs file, which restricts the sources of data to the query string and request headers.

Listing 16-31. Defining a Parameter Model Binding in the WebApiConfig.cs File

using System.Web.Http;
using ExampleApp.Infrastructure;
using System.Web.Http.ValueProviders;
using System.Net.Http.Headers;
using System.Web.Http.ModelBinding;
using System.Web.Http.ModelBinding.Binders;
using ExampleApp.Models;
using System.Web.Http.ValueProviders.Providers;

namespace ExampleApp {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            config.DependencyResolver = new NinjectResolver();

            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "Binding Example Route",
                routeTemplate: "api/{controller}/{action}/{first}/{second}"
            );

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            config.Services.Add(typeof(ValueProviderFactory),
                new HeaderValueProviderFactory());

            config.Services.Insert(typeof(ModelBinderProvider), 0,
                new SimpleModelBinderProvider(typeof(Numbers), new NumbersBinder()));

            config.ParameterBindingRules.Add(x => {
                return x.ParameterType == typeof(Numbers)
                    ? new ModelBinderParameterBinding(x, new NumbersBinder(),
                        new ValueProviderFactory[] {
                        new QueryStringValueProviderFactory(),
                        new HeaderValueProviderFactory()})
                    : null;
            });
        }
    }
}

Image Tip  I described the built-in value providers in Chapter 15.

The ModelBinderParameterBinding class is derived from HttpParameterBinding and defines a constructor that receives an HttpParameterDescriptor object, an IModelBinder implementation, and an enumeration of ValueProviderFactory classes. In the listing, I created a rule that specifies the NumbersBinder binder class but limits the source of values to the query string and request headers.

Image Note  I described the HttpParameterBinding and HttpParameterDescriptor classes in Chapter 15.

Using Type Converters

Type converters are an oddity. They allow a complex type parameter to be created from the query string, using a mechanism that has been part of .NET since version 1.1. I have included them in this chapter for completeness, but the URL format they require isn’t especially useful in most web services. Table 16-9 puts type converters in context.

Table 16-9. Putting Type Converters in Context

Question

Answer

What are they?

Type converters integrate the .NET type conversion features into the Web API model binding process.

When should you use them?

Use type converters when all of the data required to instantiate a class is contained in a single query string property or routing segment variable.

What do you need to know?

Type converters don’t really fit into the rest of the model binding system, and I have yet to find them useful in a real project.

Understanding Type Converters

Type converters are responsible for creating an object from the URL, from a single query string property or routing segment. Type converters are derived from the System.ComponentModel.TypeConverter class and associated with action method parameters with the TypeConverter attribute.

The problem with type converters is that they require all the information necessary to create an instance of a model object to be expressed in a single query string parameter or routing segment. I need four values to create an instance of the Numbers model class; doing so using a type converter means sending a query string like this one:

api/bindings/sumnumbers?numbers=2,54,true,true

In fact, you can encode the data in any way that suits you, but to keep things simple, I have expressed the data values so they are separated by a comma.

Image Caution  Separating values with a single character is common when using a type converter, but be careful when using characters like + as separators. The string taken from the URL is assumed to be URL encoded, which means that the + character is replaced with a space.

Creating a Type Converter

The TypeConverter class—and, in fact, the entire System.ComponentModel namespace—is a general-purpose mechanism for managing and converting types, and most of its features have no role in Web API.

Creating a Web API type converter is relatively simple, especially since you can ignore all but the type and value parameters defined by the methods in Table 16-10, which are the only ones required to use a type converter in a Web API application.

Table 16-10. The Methods Required to Create a Web API Type Converter

Name

Description

CanConvertFrom(context, type)

Called to check whether the type converter is able to create an instance of its model object from a specified type. In Web API, implementations of this method should return true for strings and return false for all other types.

ConvertFrom(context, culture,value)

Called to create an instance of the model object from a request value, which will be a string. This method should return null if the data cannot be converted into a model object.

To demonstrate how to create a type converter, I added a class file called NumbersTypeConverter.cs to the Infrastructure folder and used it to define the class shown in Listing 16-32.

Listing 16-32. The Contents of the NumbersTypeConverter.cs File

using System;
using System.ComponentModel;
using System.Globalization;
using ExampleApp.Models;

namespace ExampleApp.Infrastructure {
    public class NumbersTypeConverter : TypeConverter {

        public override bool CanConvertFrom(ITypeDescriptorContext context,
                Type sourceType) {
            return sourceType == typeof(string);
        }

        public override object ConvertFrom(ITypeDescriptorContext context,
                CultureInfo culture, object value) {

            string valueToParse = value as string;
            string[] elements = null;
            if (valueToParse != null
                    && (elements = valueToParse.Split(',')).Length == 4) {

                int firstVal, secondVal; bool addVal, doubleVal;
                if (int.TryParse(elements[0], out firstVal)
                     && int.TryParse(elements[1], out secondVal)
                     && bool.TryParse(elements[2], out addVal)
                     && bool.TryParse(elements[3], out doubleVal)) {

                    return new Numbers(firstVal, secondVal) {
                        Op = new Operation {
                            Add = addVal,
                            Double = doubleVal
                        }
                    };
                }
            }
            return null;
        }
    }
}

The work of creating an object for an action method happens in the ConvertFrom method, which is passed the data from the request and must convert it into an instance of the model class. In the listing, I split the string I receive into an array and use the int.TryParse and bool.TryParse methods to convert individual values to the types I need to create an instance of the Numbers class. If I don’t receive data in the format that I expect, then I return null to indicate that I cannot create an instance of the model class.

Applying a Type Converter

Type converters are applied to the model class, rather than the action method parameter. The TypeConverter attribute, defined in the System.ComponentModel namespace, specifies the type converter class that is used to create instances of the model. Listing 16-33 shows how I have applied the attribute to the Numbers class.

Listing 16-33. Applying a Type Converter in the BindingModels.cs File

using System.Web.Http.ModelBinding;
using ExampleApp.Infrastructure;
using System.ComponentModel;

namespace ExampleApp.Models {

    //[ModelBinder(BinderType = typeof(NumbersBinder))]
    [TypeConverter(typeof(NumbersTypeConverter))]
    public class Numbers {
        private int first, second;

        public Numbers(int firstVal, int secondVal) {
            first = firstVal; second = secondVal;
        }

        public int First {
            get { return first; }
        }

        public int Second {
            get { return second; }
        }

        public Operation Op { get; set; }
        public string Accept { get; set; }
    }

    public class Operation {
        public bool Add { get; set; }
        public bool Double { get; set; }
    }
}

Type converters only read their data from the URL, which means I need to configure the client to send a GET request in the format I described in the previous section. Listing 16-34 shows the changes that I made to the bindings.js file to configure the jQuery Ajax request.

Listing 16-34. Configuring the Ajax Request in the bindings.js File

var viewModel = ko.observable({ first: 2, second: 5});
var response = ko.observable("Ready");
var gotError = ko.observable(false);

var sendRequest = function (requestType) {
    $.ajax("/api/bindings/sumnumbers", {
        type: "GET",
        data: "numbers=" + viewModel().first + ","
            + viewModel().second + "," + "true" + "," + true,
        success: function (data) {
            gotError(false);
            response("Total: " + data);
        },
        error: function (jqXHR) {
            gotError(true);
            response(jqXHR.status + " (" + jqXHR.statusText + ")");
        }
    });
};

$(document).ready(function () {
    ko.applyBindings();
});

I format the query string that the Ajax request will be sent to so that it uses the format that the type converter can process. The final step is to disable the parameter binding rule in the WebApiConfig.cs file, which preempts the TypeConverter attribute. Listing 16-35 shows the changes.

Listing 16-35. Disabling the Parameter Binding Rule in the WebConfig.cs File

using System.Web.Http;
using ExampleApp.Infrastructure;
using System.Web.Http.ValueProviders;
using System.Net.Http.Headers;
using System.Web.Http.ModelBinding;
using System.Web.Http.ModelBinding.Binders;
using ExampleApp.Models;
using System.Web.Http.ValueProviders.Providers;

namespace ExampleApp {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            config.DependencyResolver = new NinjectResolver();

            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "Binding Example Route",
                routeTemplate: "api/{controller}/{action}/{first}/{second}"
            );

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            config.Services.Add(typeof(ValueProviderFactory),
                new HeaderValueProviderFactory());

            config.Services.Insert(typeof(ModelBinderProvider), 0,
                new SimpleModelBinderProvider(typeof(Numbers), new NumbersBinder()));

            //config.ParameterBindingRules.Add(x => {
            //    return x.ParameterType == typeof(Numbers)
            //        ? new ModelBinderParameterBinding(x, new NumbersBinder(),
            //            new ValueProviderFactory[] {
            //            new QueryStringValueProviderFactory(),
            //            new HeaderValueProviderFactory()})
            //        : null;
            //});
        }
    }
}

To test the type converter, start the application and navigate to the /Home/Bindings URL. When you click the Send Request button, jQuery will send an Ajax request that contains a query string that contains all of the values in a single property, which will then be parsed by the type converter and passed as an argument to the action method. The result is shown in Figure 16-2.

9781484200865_Fig16-02.jpg

Figure 16-2. Using a type converter

Summary

In this chapter, I showed you how you can use Web API to bind complex types from data contained in the request URL using model binders. I demonstrated how to use the built-in model providers to bind objects, arrays, and collections and how to create and apply a custom model binder, which is useful when working with classes that require special handling. I focused on the query string, but the same techniques apply to routing data, which I detail in Chapters 20 and 21. I finished this chapter by explaining type converters, which are an odd adaptation of an old .NET feature into the Web API class framework. In the next chapter, I show you how media type formatters—which I introduced in Chapter 12 to serialize action method results—can be used to deserialize objects from the request body. I also show you how to completely replace the default parameter binding system with one of your own creation.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.91.254