Chapter 13. Formatters and Model Binding
Anyone can write code that a computer can understand. Only good programmers write code that humans can understand.
We previously discussed media types and their semantics as a way to represent concepts in the domain space of a system. Once we move to the implementation side, those abstractions must be translated somehow to a language that programmers speak. In the case of ASP.NET Web API, that final representation would be objects—or models, to give them a more precise name. Having said that, models represent a level of abstraction that developers use to map objects into media type representations or other different parts in an HTTP message.
The model binding infrastructure in ASP.NET Web API provides the necessary runtime services to perform many of these mappings for us. In that way, a developer can focus on the implementation details of Web API and leave all serialization concerns to the framework. There is an evident advantage in using this kind of architecture. The developer can work with a single level of abstraction, which is the model, and support a variety of media types according to the requirements of the different Web API consumers. For example, in our Issue Tracker application, we have a single model class representing an issue, which can optionally be converted to different media types like JSON or XML by the framework.
As part of this chapter, we will explore model binding in detail by looking at the different runtime components and extensibility hooks provided by the framework to customize or add new model binding functionality.
The Importance of Models in ASP.NET Web API
As a rule of thumb, controller actions that focus on single concerns are easier to test, extend, and maintain in the long run. Converting message representations into model objects is one of those concerns that you should try to move away from your action implementations in the first place. Consider Example 13-1, in which serialization concerns are mixed with the implementation of a Web API action.
public
HttpResponseMessage
Post
(
HttpRequestMessage
request
)
// <1>
{
int
id
=
int
.
Parse
(
request
.
RequestUri
.
ParseQueryString
().
Get
(
"id"
));
// <2>
var
values
=
request
.
Content
.
ReadAsFormDataAsync
().
Result
// <3>
var
issue
=
new
Issue
{
Id
=
id
,
Name
=
values
[
"name"
],
Description
=
values
[
"description"
]
};
// do something with the constructed issue
}
There are a few evident problems with this code:
-
The generic signature in the controller method makes it really hard to infer its purpose without looking at the implementation details. It also limits our ability to overload the
Post
method with different arguments for supporting multiple scenarios. - It is not checking whether the parameter in the query string really exists or can be converted to an integer.
-
It is tying the implementation to a single media type (
application/form-url-encoded
) and also blocking the execution thread for reading the body content synchronously. To this last point, invoking theResult
property directly on asynchronous tasks without checking if they are completed is not considered good practice and might prevent the execution thread from returning to the thread pool to attend to new requests.
We can easily rewrite this action to use a model class only and avoid all these issues, as illustrated in Example 13-2.
public
void
Post
(
Issue
issue
)
// <1>
{
// do something with the constructed issue
}
As you can see, all the serialization concerns literally disappeared from the implementation, leaving only what it matters most. The model binding infrastructure in the framework will take care of the rest at the moment of executing the action.
How Model Binding Works
At a very core level in the model binding infrastructure is a component called HttpParameterBinding
, which knows how to infer the value of a parameter from a request message and is demonstrated in Example 13-3. Every HttpParameterBinding
instance is tied to a single parameter, which is defined at the moment of the Web API with the HttpConfiguration
object. How that instance is tied to a parameter is determined by another configuration class called HttpParameterDescriptor
, which contains metadata for describing a parameter in terms of name, type, or any other attribute that could be used by the model binding infrastructure to select an HttpParameterBinding
.
public
abstract
class
HttpParameterBinding
{
protected
HttpParameterBinding
(
HttpParameterDescriptor
descriptor
);
public
abstract
Task
ExecuteBindingAsync
(
ModelMetadataProvider
metadataProvider
,
HttpActionContext
actionContext
,
CancellationToken
cancellationToken
);
// <1>
}
Example 13-3 shows the basic structure of an HttpParameterBinding
with the key method ExecuteBindingAsync
, which every implementation must provide to perform the binding for a parameter.
As happens with many of the runtime components in ASP.NET Web API, an HttpParameterBinding
also offers an asynchronous signature for its core method, ExecuteBindingAsync
. This would be useful if you had, for example, an implementation that does not necessarily rely on values obtained from the current request message and performs some I/O operations such as querying a database or reading a file. Example 13-4 shows a basic implementation of an HttpParameterBinding
for binding action parameters of the type CultureInfo
from the culture set in the executing thread.
public
class
CultureParameterBinding
:
HttpParameterBinding
{
public
CultureParameterBinding
(
HttpParameterDescriptor
descriptor
)
// <1>
:
base
(
descriptor
)
{
}
public
override
System
.
Threading
.
Tasks
.
Task
ExecuteBindingAsync
(
System
.
Web
.
Http
.
Metadata
.
ModelMetadataProvider
metadataProvider
,
HttpActionContext
actionContext
,
System
.
Threading
.
CancellationToken
cancellationToken
)
{
CultureInfo
culture
=
Thread
.
CurrentThread
.
CurrentCulture
;
// <2>
SetValue
(
actionContext
,
culture
);
// <3>
var
tsc
=
new
TaskCompletionSource
<
object
>();
// <4>
tsc
.
SetResult
(
null
);
return
tsc
.
Task
;
}
}
An instance of our HttpParameterBinding
is created with a descriptor. Our implementation just ignores that parameter, but other implementations might use some of its information <1>
.
The ExecuteBindingAsync
method gets the CultureInfo
instance from the current thread <2>
and uses it to set the binding with the help of the SetValue
method in the base class <3>
. As the last step in this method, a TaskCompletionSource
is created for returning a new task, already completed synchronously <4>
. In an asynchronous version of this method, SetValue
would probably be called as part of the returned task.
This CultureParameterBinding
can now be used to inject a CultureInfo
instance directly as a parameter of an action method, as shown in Example 13-5.
public
class
HomeController
:
ApiController
{
[HttpGet]
public
HttpResponseMessage
BindCulture
(
CultureInfo
culture
)
{
return
Request
.
CreateResponse
(
System
.
Net
.
HttpStatusCode
.
Accepted
,
String
.
Format
(
"BindCulture with name {0}."
,
culture
.
Name
));
}
Now you know what an HttpParameterBinding
is, but we haven’t discussed yet how it is configured and selected by the framework when an action is executed. This selection is made in one of the many pluggable services available in the System.Web.Http.Model
Binding.IActionValueBinder
framework, whose default implementation is System.Web.Http.ModelBinding.DefaultActionValueBinder
. An IActionValueBinder
is reponsible for returning an HttpActionBinding
instance, which mainly contains a collection of HttpParameterBinding
instances associated with a given controller action that can be cached across requests:
public
interface
IActionValueBinder
{
HttpActionBinding
GetBinding
(
HttpActionDescriptor
actionDescriptor
);
}
The built-in implementation in DefaultActionValueBinder
uses reflection to build a list of HttpParameterDescriptor
s, which is later used for querying the configuration and selecting the appropriate HttpParameterBinding
instances (see Figure 13-1).
This class currently supports two different ways to determine which HttpParameterBinding
instances are associated with an action. In the first one, the association is done through configuration with the property ParameterBindingRules
in the HttpConfiguration
object, which exposes a set of rules for choosing an binding instance for a given HttpParameterDescriptor
. Those rules take the shape of a delegate, Func<HttpParameterDescriptor
, HttpParameterBinding>
, that receives a descriptor as a parameter and returns a binding instance. That means you can either provide a method callback or a lambda expression to resolve the bindings. For our scenario with the CultureParameterBinding
, we need a rule that returns our binding for an HttpParameterDescriptor
associated with the type System.Globalization.CultureInfo
, as shown in Example 13-6.
config
.
ParameterBindingRules
.
Insert
(
0
,
(
descriptor
)
=>
// <1>
{
if
(
descriptor
.
ParameterType
==
typeof
(
System
.
Globalization
.
CultureInfo
))
// <2>
return
new
CultureParameterBinding
(
descriptor
);
return
null
;
});
The new rule is inserted with a lambda expression <1>
that checks for the ParameterType
property in the descriptor and returns the binding only when the type is equal to System.Globalization.CultureInfo
<2>
.
A second mechanism, which is more declarative, involves the use of an attribute, ParameterBindingAttribute
, that we need to derive (as we will explore in the next section).
If no mapping rule or ParameterBindingAttribute
is found, this binder uses a default policy, which binds simple types to URI segments or query string variables and complex types to the request body.
Built-In Model Binders
The framework ships with several built-in implementations, but only three of them deserve special attention from a developer: ModelBindingParameterBinder
, FormatterParameterBinder
, and HttpRequestParameterBinding
, which implement completely different ways of binding a message part to a model. The first one, ModelBindingParameterBinder
, uses an approach borrowed from ASP.NET MVC in which the model is composed of different parts in the message, as if they are Lego building blocks. The second one, FormatterParameterBinder
, relies on formatters that understand all the semantics and formatting of a given media type and know how to serialize or deserialize a model applying those semantics. Formatters represent a key part of content negotiation and are the preferred method for binding a message body to a model. Finally, the third one, HttpRequestParameterBinding
, is used for supporting scenarios with generic actions that use HttpRequestMessage
or HttpResponseMessage
instances directly as part of the method signature.
The ModelBindingParameterBinder Implementation
The ModelBindingParameterBinder
implementation reuses the same idea applied in ASP.NET MVC for doing model binding. It relies on value providers, which know how to obtain data from different parts of an HTTP message, and model binders for assembling those parts into a model.
This implementation is mainly focused on binding simple key/value pairs such as those found in HTTP headers, URL segments, query strings, or a body encoded with application/form-url-encoded
(the media type used for encoding an HTTP form). All these values are usually strings that can be found in the message and converted to primitive types. Modeling binders do not know anything specific about media types or how they can be interpreted; that’s the job of the formatters, which we will discuss in detail in the next section.
The framework ships with several built-in model binder implementations that assemble different small pieces found in an HTTP message into fairly complex models.To be more precise, those implementations also take care of converting strings into simple data types such as Timespan
, Int
, Guid
, Decimal
, or other types decorated with a type converter before hydrating the model. Two examples of these built-in implementations are ArrayModelBinder
and TypeConverterModelBinder
, both in the System.Web.Http.ModelBinding.Binders
namespace. It’s worth mentioning that model binders are mostly used for rehydrating simple types, or as building blocks for composing more complex types. These built-in implementations typically cover the most common scenarios, so you will have to think twice before you start writing a new model binder from scratch.
In Figure 13-2, the configured value providers first take care of decomposing the message into pieces for getting different values such as the issue ID from the query string and the rest of the fields from the message body, which were submitted as a HTTP PUT
with the URL form encoding media type. The selected model binder works closely with the value providers to request the data needed for initializing a new Issue
class instance.
Value Providers
Value providers provide a thin abstraction layer for decoupling model binders from any messaging details. They do this by aggregating values from different parts of an HTTP message, and providing an uniform interface to consume them.
At a very core level, every value provider implements the System.Web.Http.ValueProviders.IValueProvider
interface, as shown in Example 13-7.
public
interface
IValueProvider
{
bool
ContainsPrefix
(
string
prefix
);
// <1>
ValueProviderResult
GetValue
(
string
key
);
// <2>
}
The first method, ContainsPrefix
<1>
, returns a Boolean value indicating whether the value provider implementation can provide a value for a key with the prefix passed as an argument, which typically represents a property name in the model being deserialized.
The second method, and probably the most important, GetValue
<2>
, searches the key passed as an argument in the HTTP message and returns the associated value. The value is not returned as a raw string directly but as a ValueProviderResult
instance, which contains methods for getting the raw value or a value cast to an specific type.
You might want to create a new value provider or derive from an existing one for addressing new use cases such as searching values in the request message under specific name conventions or in other places such as custom cookies.
Example 13-8 shows a basic implementation of a value provider for searching headers with a vendor prefix X-
.
public
class
HeaderValueProvider
:
IValueProvider
{
public
const
string
HeaderPrefix
=
"X-"
;
private
HttpControllerContext
context
;
public
HeaderValueProvider
(
HttpControllerContext
context
)
// <1>
{
this
.
context
=
context
;
}
public
bool
ContainsPrefix
(
string
prefix
)
{
var
contains
=
context
.
Request
.
Headers
.
Any
(
h
=>
h
.
Key
.
Contains
(
HeaderPrefix
+
prefix
));
// <2>
return
contains
;
}
public
ValueProviderResult
GetValue
(
string
key
)
{
if
(!
context
.
Request
.
Headers
.
Any
(
h
=>
h
.
Key
==
HeaderPrefix
+
key
))
return
null
;
var
value
=
context
.
Request
.
Headers
.
GetValues
(
HeaderPrefix
+
key
).
First
();
// <3>
var
stringValue
=
(
value
is
string
)
?
(
string
)
value
:
value
.
ToString
();
// <4>
return
new
ValueProviderResult
(
value
,
stringValue
,
CultureInfo
.
CurrentCulture
);
// <5>
}
}
The HeaderValueProvider
implementation is constructed with an HttpControllerContext
instance that provides access to the execution context and also the request message <1>
. The ContainsPrefix
method returns true
for any key in the HTTP request headers starting with an X-
prefix <2>
, and the GetValue
method gets its value <3>
. That value is returned in a new ValueProviderResult
instance <5>
and also as a raw string <4>
.
A IValueProvider
implementation can be injected at runtime through a ValueProviderFactory
, which is a class that derives from the abstract class System.Web.Http.ValueProviders.ValueProviderFactory
and overrides the method GetValueProvider
for returning instances of the IValueProvider
implementation. You can find the corresponding value provider factory implementation for HeaderValueProvider
in Example 13-9.
public
class
HeaderValueProviderFactory
:
ValueProviderFactory
{
public
override
IValueProvider
GetValueProvider
(
HttpActionContext
actionContext
)
{
return
new
HeaderValueProvider
(
actionContext
.
ControllerContext
);
// <1>
}
}
The HeaderValueProviderFactory
implementation instantiates a new HeaderValueProvider
using the current HttpActionContext
as an argument in the constructor <1>
. We can register this factory in the HttpConfiguration
object using the global dependency resolver, as shown in Example 13-10.
public
static
void
RegisterValueProvider
(
HttpConfiguration
config
)
{
var
valueProviderFactories
=
config
.
ServiceResolver
.
GetValueProviderFactories
().
ToList
();
valueProviderFactories
.
Insert
(
0
,
new
HeaderValueProviderFactory
());
// <1>
config
.
ServiceResolver
.
SetServices
(
typeof
(
System
.
Web
.
Http
.
ValueProviders
.
ValueProviderFactory
),
valueProviderFactories
.
ToArray
());
// <2>
}
The factory is added to the existing list of factories in the first position <1>
, so it takes precedence when a value needs to be provided, and is injected afterward as a service with the dependency resolver <2>
.
The most important value providers shipped with the framework are System.Web.Http.ValueProviders.Providers.QueryStringValueProvider
and System.Web.Http.ValueProviders.Providers.RouteDataValueProvider
, and their correspoding factories, System.Web.Http.ValueProviders.Providers.QueryStringValueProviderFactory
and System.Web.Http.ValueProviders.Providers.RouteDataValueProvider
. While the first provider parses and provides values found in the query string, the second is responsible for obtaining values from the route parameters (i.e., the parameters that you define at the route level in the route configuration).
Model Binders
Model binders orchestrate all the actions for assembling a new model instance from the different data pieces requested to the configured value providers.
A model binder implements the interface System.Web.Http.ModelBinding.IModelBinder
, which contains only one method, BindModel
, where all the magic happens (see Example 13-11).
public
interface
IModelBinder
{
bool
BindModel
(
HttpActionContext
actionContext
,
ModelBindingContext
bindingContext
);
// <1>
}
The BindModel
method receives two objects <1>
, an HttpActionContext
instance with specific information about the current execution, and an instance of ModelBindingContent
representing the context of the model binding process. This method also returns a Boolean value indicating whether the implementation could successfully assemble a new model instance. There are two important properties available as part of the binding context, ModelState
and ModelMetadata
. The former is a property bag class used by the model binder for storing the results of the binding model process or any error that might happen in that process. The latter provides access to the discovered metadata associated to the model, such as available properties or any component model attribute for performing data validations. Although this interface looks very simple at first glance, it hides a good deal of the complexity required for implementing a model binder and providing the right behavior at runtime. For that reason, the following sequence describes in detail all the steps performed by an IModelBinder
implementation.
-
The implementation tries to get all the values it needs to assemble a new model from the value provider passed as part of the binding context. Although the binding context provides access to a single value provider, that instance usually represents a built-in value provider,
CompositeValueProvider
, which implements theIValueProvider
interface but internally delegates the method calls to all the configured value providers. -
A model is created and initialized with all the values obtained from the value provider. If some error happens during the model initialization, the exceptions are set on the binding context through the
ModelState
property. - The model is set on the binding context.
Example 13-12 shows a model binder implementation to create instances of the Issue
model class previously discussed in this chapter.
public
class
IssueModelBinder
:
IModelBinder
{
public
bool
BindModel
(
HttpActionContext
actionContext
,
ModelBindingContext
bindingContext
)
{
var
model
=
(
Issue
)
bindingContext
.
Model
??
new
Issue
();
var
hasPrefix
=
bindingContext
.
ValueProvider
.
ContainsPrefix
(
bindingContext
.
ModelName
);
var
searchPrefix
=
(
hasPrefix
)
?
bindingContext
.
ModelName
+
"."
:
""
;
int
id
=
0
;
if
(
int
.
TryParse
(
GetValue
(
bindingContext
,
searchPrefix
,
"Id"
),
out
id
)
{
model
.
Id
=
id
;
// <1>
}
model
.
Name
=
GetValue
(
bindingContext
,
searchPrefix
,
"Name"
);
// <2>
model
.
Description
=
GetValue
(
bindingContext
,
searchPrefix
,
"Description"
);
// <3>
bindingContext
.
Model
=
model
;
return
true
;
}
private
string
GetValue
(
ModelBindingContext
context
,
string
prefix
,
string
key
)
{
var
result
=
context
.
ValueProvider
.
GetValue
(
prefix
+
key
);
// <4>
return
result
==
null
?
null
:
result
.
AttemptedValue
;
}
}
This implementation uses the value provider available as part of the binding context <1>
for requesting data, and binds those values to the model’s properties afterward in <2>
, <3>
, and <4>
. This is not something you would likely do in a real application, but it provides a simple demonstration of how an IModelBinder
implementation might look.
A model binder implementation is finally configured and injected at runtime through a model binder provider, which works as a factory. A model binder provider derives from the base class System.Web.Http.ModelBinding.ModelBinderProvider
and implements the method GetBinder
for returning a new model binder instance, as shown in Example 13-13.
public
class
IssueModelBinderProvider
:
ModelBinderProvider
{
public
override
IModelBinder
GetBinder
(
HttpActionContext
actionContext
,
ModelBindingContext
bindingContext
)
{
return
new
IssueModelBinder
();
}
}
You can register this provider by using the dependency resolver available as part of the HttpConfiguration
object, or by decorating the model class with a System.Web.Http.ModelBinding.ModelBinderAttribute
, as shown in Examples 13-14 and 13-15.
[ModelBinder(typeof(IssueModelBinderProvider))]
public
class
Issue
{
public
int
Id
{
get
;
set
;
}
public
string
Name
{
get
;
set
;
}
public
string
Description
{
get
;
set
;
}
}
public
void
Post
([
ModelBinder
(
typeof
(
IssueModelBinderProvider
))]
Issue
issue
)
{
}
An interesting fact about the ModelBinderAttribute
is that it derives from the previously discussed attribute ParameterBindingAttribute
. This attribute was used to declaratively attach an HttpParameterBinding
instance to a parameter. In this case, the ModelBinderAttribute
initializes a new instance of ModelBindingParameterBinder
that internally uses the ModelBinderProvider
passed as an argument (IssueModelBinderProvider
, in our examples).
Model Binding Against URIs Only
The framework ships with another attribute, FromUriAttribute
, that derives from the ModelBinderAttribute
to force the runtime to perform the binding only against data available in the URL. This is useful for binding values found in the URL to properties in a model class, as the framework will bind values in the URL only against simple types by default.
Example 13-16 illustrates how the query string variables Lang
and Filter
are automatically mapped to the properties with the same name on the IssueFilters
model.
The FormatterParameterBinder Implementation
This implementation relies on formatters, which were introduced in ASP.NET Web API for supporting better content-negotiation scenarios with the use of media types.
In ASP.NET MVC, only the HTML (text/html
) and JSON (application/json
) media types were treated as first-class citizens and fully supported across the entire stack. Also, there was not a consistent model for supporting content negotiation. You could support different media types for the response messages by providing custom ActionResult
implementations, but it was not clear how a new media type could be introduced and handled by the framework. Developers typically solved this by leveraging the model binding infrastructure with new model binders or value providers.
Fortunately, this inconsistency has been solved in ASP.NET Web API with the introduction of formatters. A formatter now unifies all the serialization concerns by providing a single entry point for serializing or deserializing a model using the format expressed by a media type. The formatters to use for a given message will be determined by the content negotiation algorithm.
Every formatter derives from the base class MediaTypeFormatter
(see Example 13-17) and overrides the methods CanReadType
and ReadFromStreamAsync
for supporting deserialization, and CanWriteType
and WriteToStreamAsync
for supporting serialization of models following the semantics and format of a media type.
public
abstract
class
MediaTypeFormatter
{
public
Collection
<
Encoding
>
SupportedEncodings
{
get
;
}
public
Collection
<
MediaTypeHeaderValue
>
SupportedMediaTypes
{
get
;
}
public
Collection
<
MediaTypeMapping
>
MediaTypeMappings
{
get
;
}
public
abstract
bool
CanReadType
(
Type
type
);
public
abstract
bool
CanWriteType
(
Type
type
);
public
virtual
Task
<
object
>
ReadFromStreamAsync
(
Type
type
,
Stream
readStream
,
HttpContent
content
,
IFormatterLogger
formatterLogger
);
public
virtual
Task
WriteToStreamAsync
(
Type
type
,
object
value
,
Stream
writeStream
,
HttpContent
content
,
TransportContext
transportContext
);
}
The following list summarizes the principal characteristics of the MediaTypeFormatter
class:
-
The
CanReadType
andCanWriteType
methods receive a type as an argument, and must return a value indicating whether they can read or write an object of that type into an stream representing the message body. This means a formatter might know how to write a type but not how to read it from an stream, for example. -
The
SupportedMediaTypes
collection specifies the list of supported media types (e.g.,text/html
). This list is typically initialized in the formatter constructor method. The runtime will determine which formatter to use during the content negotiation handshake based on the value returned by theCanReadType
orCanWriteType
methods and the supported media types. It’s worth mentioning that a request message can mix different media types sometimes when theContent-Type
header is set to multipart, so every part defines its media type. The runtime can handle this scenario as well by selecting one or more formatters for all the present media types. -
A
MediaTypeFormatter
adheres to the Task Parallel Library (TPL) programming model for the read and write operations. Most implementations will still run synchronously, as they involve only serialization. -
The
MediaTypeMappings
collection allows a formatter to define how to look for the media type associated with a request message. (e.g., query string, HTTP header). For example, a client application might send the expected media type format for the response as part of the query string.
The framework includes a set of formatters out of the box for handling the most common media types such as form-encoded data (FormUrlEncodedMediaTypeFormatter
), JSON (JsonMediaTypeFormatter
), or XML (XmlMediaTypeFormatter
). For other media types, you will have to write your own implementation, or use one of the many implementations provided by the open source community.
Now we’ll discuss the implementation of a MediaTypeFormatter
for serializing a model as part of a RSS or ATOM feed (see Example 13-18).
public
class
SyndicationMediaTypeFormatter
:
MediaTypeFormatter
{
public
const
string
Atom
=
"application/atom+xml"
;
public
const
string
Rss
=
"application/rss+xml"
;
public
SyndicationMediaTypeFormatter
()
:
base
()
{
this
.
SupportedMediaTypes
.
Add
(
new
MediaTypeHeaderValue
(
Atom
));
// <1>
this
.
SupportedMediaTypes
.
Add
(
new
MediaTypeHeaderValue
(
Rss
));
}
public
override
bool
CanReadType
(
Type
type
)
{
return
false
;
}
public
override
bool
CanWriteType
(
Type
type
)
{
return
true
;
// <2>
}
public
override
Task
WriteToStreamAsync
(
Type
type
,
object
value
,
Stream
writeStream
,
HttpContent
content
,
TransportContext
transportContext
)
// <3>
{
var
tsc
=
new
TaskCompletionSource
<
AsyncVoid
>();
// <4>
tsc
.
SetResult
(
default
(
AsyncVoid
));
var
items
=
new
List
<
SyndicationItem
>();
if
(
value
is
IEnumerable
)
{
foreach
(
var
model
in
(
IEnumerable
)
value
)
{
var
item
=
MapToItem
(
model
);
items
.
Add
(
item
);
}
}
else
{
var
item
=
MapToItem
(
value
);
items
.
Add
(
item
);
}
var
feed
=
new
SyndicationFeed
(
items
);
SyndicationFeedFormatter
formatter
=
null
;
if
(
content
.
Headers
.
ContentType
.
MediaType
==
Atom
)
{
formatter
=
new
Atom10FeedFormatter
(
feed
);
}
else
if
(
content
.
Headers
.
ContentType
.
MediaType
==
Rss
)
{
formatter
=
new
Rss20FeedFormatter
(
feed
);
}
else
{
throw
new
Exception
(
"Not supported media type"
);
}
using
(
var
writer
=
XmlWriter
.
Create
(
writeStream
))
{
formatter
.
WriteTo
(
writer
);
writer
.
Flush
();
writer
.
Close
();
}
return
tsc
.
Task
;
// <5>
}
protected
SyndicationItem
MapToItem
(
object
model
)
// <6>
{
var
item
=
new
SyndicationItem
();
item
.
ElementExtensions
.
Add
(
model
);
return
item
;
}
private
struct
AsyncVoid
{
}
}
This implementation knows only how to serialize models according to the Atom and RSS media type definitions, so that is explicitly specified as part of the constructor <1>
. It also returns true
in the CanWrite
method to specify that the implementation is write only <2>
.
The WriteToStreamAsync
method implementation <3>
mainly relies on the syndication classes included with the WCF Web Programming model for serializing the models into Atom or RSS feeds. This programming model provides classes for constructing a syndication feed and all the associated entries, as well as the formatter classes for transforming those into a well-known syndication format such as Atom or RSS.
As we stated, the WriteToStreamAsync
and ReadFromStreamAsync
methods leverage the new Task Parallel Library for doing the asynchronous work. They both return a Task
instance that internally wraps the asynchronous work. However, most of the time, serialization is a safe operation that can be done synchronously. In fact, many of the serializer classes you will find in the .NET Framework do their job synchronously.
Creating a new task using the method Task.Factory.StartNew
for all the serialization work would be the easiest thing to do, but there are some collateral effects associated with that action. After we invoke the StartNew
method, a new job is scheduled, which might generate a thread context switch that hurts performance. The trick in this scenario is to use a TaskCompletionSource
<4>
. The TaskCompletionSource
is marked as complete so all the work is done synchronously afterward, and the resulting task associated with the TaskCompletionSource
is returned <5>
The method MapToItem
<6>
simply uses the model instance as the content for a syndication item.
One more thing we might want to support in a formatter is the ability to negotiate the media type from additional sources rather than the Accept
header.
This is a very common requirement these days for clients where the HTTP client stack is not correctly implemented (as happens with browsers in some older mobile devices). In those cases, a client might want to provide the accepted media type in the query string, such as http://…/Issues?format=atom
.
The MediaTypeFormatter
supports this scenario through the MediaMappings
property, which represents a collection of MediaMapping
instances indicating the locations where the media type can be found, such as query strings, headers, or URI segments. The framework provides several concrete implementations of the MediaMapping
abstract class for addressing the most common scenarios. The following list provides a brief description of these mappings:
- QueryStringMapping
-
This can be used to map the requested media type to a query string variable. For example, the format variable in the URL http://localhost/issues?format=atom would map to the
atom
media type. - UriPathExtensionMapping
-
This can be used to map a path in a URI to a media type. For example, http://localhost/issues.atom would map the path .atom to the
atom
media type. - RequestHeaderMapping
- This maps a request header to a media type. This would be useful in case you do not want to use any of the standard HTTP request headers.
The media type mappings are injected in a formatter through the constructor. Example 13-19 shows how the constructor was modified to use a QueryStringMapping
instance for searching the media type as part of a query string.
public
const
string
Atom
=
"application/atom+xml"
;
public
const
string
Rss
=
"application/rss+xml"
;
public
SyndicationMediaTypeFormatter
()
:
base
()
{
this
.
SupportedMediaTypes
.
Add
(
new
MediaTypeHeaderValue
(
Atom
));
this
.
SupportedMediaTypes
.
Add
(
new
MediaTypeHeaderValue
(
Rss
));
this
.
MediaTypeMappings
.
Add
(
new
QueryStringMapping
(
"format"
,
"atom"
,
new
MediaTypeHeaderValue
(
Atom
)));
// <1>
}
If the formatter found a query string variable format
with a value atom
, that would be mapped to the Atom
media type (application/atom+xml
) <1>
.
Default HttpParameterBinding Selection
By default, the model binding infrastructure will try to use FormatterParameterBinder
for complex type parameters, and ModelBindingParameterBinder
for simple .NET types. If you have multiple complex type arguments, the binding will fail unless you explicitly specify that any of these arguments must be bound from the URL or the body with the FromUriAttribute
or FromBodyAttribute
attributes, respectively. The FromBodyAttribute
is another mechanism you can use to force the use of a FormatterParameterBinder
for a given parameter. If an action contains multiple complex parameters, only one of them can be read from the request body via the FromBodyAttribute
. Otherwise, the runtime will throw an exception.
Model Validation
Model validation is another feature that you get in Web API with the model binding infrastructure. You can use this feature to either enforce business rules or to make sure the data sent by a client is correct. As the model validation is performed in a single place while the model is bound, this centralization results in code that’s easier to maintain and test.
Another important aspect of model validation is to inform clients about any possible errors in the data they sent with a chance to correct those errors. In practice, when this aspect is not enforced, developers will simply stop adopting the API as part of their applications.
As with all of the model binding infrastructure, model validation in ASP.NET Web API is also completely extensible. The framework ships with a general-purpose validator that uses attributes for validating the models. This validator works for most scenarios, and reuses the data annotation attributes included in the System.ComponentModel.DataAnnotations
namespace.
Several validation attributes are provided out of the box in that namespace—such as Required
to mark a property as required or RegularExpression
to validate a property value with a regular expression. You are also free to create your own custom data annotation attributes for use cases initially not covered by the built-in ones.
Applying Data Annotation Attributes to a Model
Suppose we want have some validations applied to our issue model. We can start using the data annotation attributes to decorate the model and enforce common validation scenarios without having to write much code, and more importantly, without requiring repetitive code. Example 13-20 shows how the issue model looks after we’ve applied some data annotation attributes.
public
class
Issue
{
[DisplayName("Issue Id")]
[Required(ErrorMessage = "The issue id is required")]
[Range(1, 1000, ErrorMessage = "The unit price must be between {1} and {2}")]
public
int
Id
{
get
;
set
;
}
[DisplayName("Issue Name")]
[Required(ErrorMessage = "The issue name is required")]
public
string
Name
{
get
;
set
;
}
[DisplayName("Issue Description")]
[Required(ErrorMessage = "The issue description is required")]
public
string
Description
{
get
;
set
;
}
}
All the properties in the model have been labeled with attributes that clearly state their intention. Some properties, such as Name
and Description
, have been marked as required, and the Id
property has been marked to require a value within a given range. The DisplayName
attribute is not used for validation but affects how the output messages are rendered.
Querying the Validation Results
Once the model binding infrastructure validates a model based on the attributes that were defined on it, the results will become available to be used by a controller implementation or in a more centralized manner with a filter.
Adding a few lines in an action implementation is by far the simplest way to check whether the model has been correctly bound and all the validation results were successful (see Example 13-21).
public
class
ValidationError
{
public
string
Name
{
get
;
set
;
}
public
string
Message
{
get
;
set
;
}
}
public
class
IssueController
:
ApiController
{
public
HttpResponseMessage
Post
(
Issue
product
)
{
if
(!
this
.
ModelState
.
IsValid
)
{
var
errors
=
this
.
ModelState
// <1>
.
Where
(
e
=>
e
.
Value
.
Errors
.
Count
>
0
)
.
Select
(
e
=>
new
ValidationError
// <2>
{
Name
=
e
.
Key
,
Message
=
e
.
Value
.
Errors
.
First
().
ErrorMessage
}).
ToArray
();
var
response
=
new
HttpResponseMessage
(
HttpStatusCode
.
BadRequest
);
response
.
Content
=
new
ObjectContent
<
ValidationError
[]>(
errors
,
new
JsonMediaTypeFormatter
());
return
response
;
}
// Do something
}
}
As illustrated in Example 13-21, all the validation results become available in a controller action through the ModelState
property <1>
. In that example, the action simply converts all the validation errors into a model class, ValidationError
<2>
, that we can serialize into the response body as JSON, and returns that with a Bad Request
status code.
This represents some generic code that you might want to reuse in multiple actions, so probably the best way to do that is to move it to a custom filter. Example 13-22 shows the same code in a filter implementation.
public
class
ValidationActionFilter
:
ActionFilterAttribute
{
public
override
void
OnActionExecuting
(
HttpActionContext
actionContext
)
{
if
(!
actionContext
.
ModelState
.
IsValid
)
{
var
errors
=
this
.
ModelState
.
Where
(
e
=>
e
.
Value
.
Errors
.
Count
>
0
)
.
Select
(
e
=>
new
ValidationError
{
Name
=
e
.
Key
,
Message
=
e
.
Value
.
Errors
.
First
().
ErrorMessage
}).
ToArray
();
var
response
=
new
HttpResponseMessage
(
HttpStatusCode
.
BadRequest
);
response
.
Content
=
new
ObjectContent
<
ValidationError
[]>(
errors
,
new
JsonMediaTypeFormatter
());
actionContext
.
Response
=
response
;
}
}
}
As you can see, the filter implementation is quite simple as well. When the filter detects that the model is not valid, the execution pipeline is automatically interrupted and a new response is sent to the consumer with the validation errors. If we send, for example, a message with an empty product name and invalid unit price, we will get the response shown in Example 13-23.
Request Message in JSON POST http://../Isssues HTTP/1.1 Content-Type: application/json { Id: 1, "Name":"", "Description": "My issue" } Response Message HTTP/1.1 400 Bad Request Content-Type: text/plain; charset=utf-8 [{ "Message": "The Issue Name is required.", "Name": "Name" }]
Conclusion
The model binding infrastructure acts as a mapping layer between HTTP messages and object instances known as models. It mainly relies on HttpParameterBinding
components for doing the parameter binding to different HTTP message parts like headers, query strings, or the body text. Two main implementations of HttpParameterBinding
are shipped out of the box in the framework: a ModelBindingParameterBinder
implementation that uses the traditional binding mechanism brought from ASP.NET MVC (in which the models are composed from small pieces found in the HTTP messages), and a FormatterParameterBinder
that uses formatters to convert a media type format to a model.
The ModelBindingParameterBinder
implementation uses IValueProvider
instances for collecting values from different parts in a HTTP message, and IModelBinder
instances to compose all those values into a single model.
The FormatterParameterBinder
implementation is a fundamental piece of content negotiation, as it understands how to transform the body of an HTTP message expressed with the semantics rules and format of a given media type into a model using formatters. A formatter derives from the base class MediaTypeFormatter
and typically knows how to manage a single media type.
In addition to parameter binding, the model binding infrastructure also offers an extensibility point for validating the models once they are deserialized. The models are validated out of the box through rules defined as data annotation attributes.
Get Designing Evolvable Web APIs with ASP.NET now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.