Original address: Parameter Binding in ASP. NET Web API
The translation is as follows:
When the Web API corresponds to a method of a controller, it must have a procedure for setting parameters called data binding. This article describes how the Web API binds parameters and how to customize the binding process.
In general, the Web API binding parameters conform to the following rules:
-
- If the argument is a simple type, the Web API attempts to get it from the URI. The simple parameter type contains. NET source Type (int,bool,double ...), plus timespan,datetime,guid,decimal and string. Plus any type that contains a string converter. (More about type converters later.)
- For complex types, the Web API attempts to read from the message body, using the Media-type type.
Examples of typical Web API controller methods:
Httpresponsemessage Put (int ID, Product item) {...}
The parameter ID is a simple type, so WEBAPI attempts to get the value from the URI. The parameter item is a complex type, so the Web API reads from the request body using the Media-type type.
Gets the value from the URI, and the Web API is fetched from the query parameters of the route or URI. The route data example is as follows:"Api/{controller}/public/{category}/{id}", for more details: Routing and Action Selection.
in the remainder of the article, I'll show you how to customize the parameter binding process. For complex types, it is of course best to consider media-type type conversions as much as possible. The key principle of an HTTP is that the resource is passed in the message body, using content negotiation To specify how the resource is represented. The Media-type type is designed to achieve this goal.
Use[Fromuri]
Adding the [Fromuri] property before the parameter forces the Web API to read the complex type from the URI. The following example defines the GeoPoint type and a controller method that obtains the GeoPoint from the URI.
public class geopoint{public double Latitude {get; set;} Public double longitude {get; set;}} Public valuescontroller:apicontroller{public httpresponsemessage Get ([Fromuri] GeoPoint location) {...}}
A client can pass two parameters to the Web API by querying the characters. Examples are as follows:
http://localhost/api/values/?Latitude=47.678558&Longitude=-122.130989
Use "Frombody"
Adding a [Frombody] property to a parameter forces the Web API to read simple parameters from the request body.
Public Httpresponsemessage Post ([frombody] string name) {...}
In this example WEBAPI will use a media-type converter to read the value of name from the request body.
POST http://localhost:5076/api/values http/1.1user-agent:fiddlerhost:localhost:5076content-type:application/ Jsoncontent-length:7 "Alice"
When a parameter is tagged [frombody] , the Web API selects the format via the Content-type header. In this example, the content type is "Application/json" and the request body is the original JSON string (not a JSON object).
With only one parameter allowed to be read from the message body, the following example will not succeed:
Caution:will not work! Public Httpresponsemessage Post ([frombody] int ID, [frombody] string name) {...}
The reason for this rule is that the request body may be stored in a non-buffered data stream that can be read only once.
Type Converters
Creating a TypeConverter and providing a string conversion allows the Web API to treat a class as if it were a simple type (so the Web API tries to get the bound data from the URI).
The following code shows a GeoPoint class representing geographic coordinates, and adding a typeconvert method to convert a string to a GeoPoint instance. The GeoPoint class added the [Typeconvert] property to make the type converter. (This example is taken from the blog post of Mike Stall How to bind to the custom objects in action signatures in MVC/WEBAPI)
[TypeConverter (typeof (Geopointconverter))]public class geopoint{public double Latitude {get; set;} Public double longitude {get; set;} public static bool TryParse (string s, out GeoPoint result) {result = null; var parts = s.split (', '); if (parts. Length! = 2) {return false; } double latitude, longitude; if (double. TryParse (Parts[0], out latitude) && double. TryParse (Parts[1], out longitude)) {result = new GeoPoint () {longitude = longitude, Latitude = Latitud e}; return true; } return false; }}class geopointconverter:typeconverter{public override bool CanConvertFrom (ITypeDescriptorContext context, Type sou Rcetype) {if (sourcetype = = typeof (String)) {return true; } return base. CanConvertFrom (context, sourcetype); } public override Object ConvertFrom (ITypeDescriptorContext context, CUltureinfo culture, Object value) {if (value is String) {GeoPoint-point; if (Geopoint.tryparse (string) value, out point)} {return point; }} return base. ConvertFrom (context, culture, value); }}
Now the Web API treats GeoPoint as a simple type, which means he tries to get the GeoPoint parameter from the URL without adding [Fromuri]before the argument.
Public Httpresponsemessage Get (GeoPoint location) {...}
The URI of the client request is like this:
http://localhost/api/values/?location=47.678558,-122.130989
Model Binders
Manipulating the type of conversion is more powerful than creating a custom object binding model Binder. Using model Binder, you can receive an HTTP request, an action, and the original value of a routed data.
To create a model Binder, you need to inherit the Imodelbinder interface, which defines only one method Bindmodel:
BOOL Bindmodel (Httpactioncontext actioncontext, Modelbindingcontext BindingContext);
The following is the model Binder for the GeoPoint object
public class geopointmodelbinder:imodelbinder{//List of known locations. private static concurrentdictionary<string, geopoint> _locations = new concurrentdictionary<string, GeoPoi Nt> (stringcomparer.ordinalignorecase); Static Geopointmodelbinder () {_locations["redmond"] = new GeoPoint () {Latitude = 47.67856, longitude = 122.13 1}; _locations["PARIS"] = new GeoPoint () {Latitude = 48.856930, longitude = 2.3412}; _locations["Tokyo"] = new GeoPoint () {Latitude = 35.683208, longitude = 139.80894}; } public bool Bindmodel (Httpactioncontext actioncontext, Modelbindingcontext bindingcontext) {if (Bindingcon Text. Modeltype! = typeof (GeoPoint)) {return false; } Valueproviderresult val = BindingContext.ValueProvider.GetValue (bindingcontext.modelname); if (val = = null) {return false; } string key = Val. RawValue As String; if (key= = null) {BindingContext.ModelState.AddModelError (Bindingcontext.modelname, "wrong valu E type "); return false; } GeoPoint result; if (_locations. TryGetValue (key, out result) | | Geopoint.tryparse (key, out result)) {Bindingcontext.model = result; return true; } bindingContext.ModelState.AddModelError (Bindingcontext.modelname, "cannot convert value to location") ; return false; }}
Model Binder Gets the raw input from value provider, which distinguishes two separate functions from the design:
- Value provider provides an HTTP request and populates it into a dictionary of key values.
- Model Binder fills the model with this dictionary.
Webapi Default value provider Gets the data through the query parameters of the route data and URI. If the URI is http://localhost/api/values/1?location=48,-122, value provider creates a key-value pair:
- Id= "1"
- location= "48,122"
(I assume the default route template is "Api/{controller}/{id}")
The name of the binding parameter is stored in the modelbindingcontext.modelname property, and Model Binder looks up the key and its value in the dictionary. If the value exists and can be converted to GeoPoint, the Model binder assigns the bound value to the Modelbindingcontext.model property.
Note: Model Binder is not limited to simple types. In this example, Model Binder first looks for a list of location types and uses type Conversion if it fails.
Set Model Binder
There are multiple paths to set model Binder. First, you can add the [Modelbinder] property to the parameter.
Public Httpresponsemessage Get ([Modelbinder (typeof (Geopointmodelbinder))] GeoPoint location)
You can also add the [Modelbinder] property on the type. The WEB API assigns model binders to all parameters of this type.
[Modelbinder (typeof (Geopointmodelbinder))]public class geopoint{ //...}
Finally, you can also add Model-binder Provider to the httpconfiguration . Model-binder provider is a simple factory class that creates a model binder. You can create a provider by inheriting the Modelbinderprovider class. However, if your model binder only handles one type, the simple approach is to embed Simplemodelbinderprovider, This is also the purpose of Simplemodelbinderprovider design. The following code shows how to do this:
public static class webapiconfig{public static void Register (httpconfiguration config) { var provider = new Simplemodelbinderprovider ( typeof (GeoPoint), New Geopointmodelbinder ()); Config. Services.insert (typeof (Modelbinderprovider), 0, provider); // ... }}
With model-binding provider, you still need to add the [Modelbinder] attribute to the parameter to tell Webapi that it should use model binder and not be a media-type type. But now you don't have to specify the type of model binder in the attribute:
Public Httpresponsemessage Get ([Modelbinder] GeoPoint location) {...}
Value Providers
I mentioned that model binder gets the value from value provider. Create a custom value provider by implementing the Ivalueprovider interface. The following sample shows how to pull a value from a request-requested cookie:
public class cookievalueprovider:ivalueprovider{Private dictionary<string, string> _values; Public Cookievalueprovider (Httpactioncontext actioncontext) {if (Actioncontext = = null) {THR ow new ArgumentNullException ("Actioncontext"); } _values = new dictionary<string, string> (stringcomparer.ordinalignorecase); foreach (Var cookie in actionContext.Request.Headers.GetCookies ()) {foreach (Cookiestate state in Cooki e.cookies) {_values[state. Name] = state. Value; }}} public bool Containsprefix (string prefix) {return _values. Keys.contains (prefix); } Public Valueproviderresult GetValue (string key) {string value; if (_values. TryGetValue (key, out value)) {return new Valueproviderresult (value, value, CultureInfo.InvariantCulture ); } return null; }}
You must also create a value provider factory that inherits from the Valueproviderfactory class.
public class cookievalueproviderfactory:valueproviderfactory{public override Ivalueprovider Getvalueprovider ( Httpactioncontext actioncontext) { return new Cookievalueprovider (Actioncontext);} }
Add value provider to httpconfiguration
public static void Register (Httpconfiguration config) { config. Services.add (typeof (Valueproviderfactory), New Cookievalueproviderfactory ()); // ...}
The Web API contains all of the value Provider, and when we call the Valueprovider.getvalue method, the model binder receives the first Provider that can produce it.
Another way is that you can use the Valueprovider property to set the Valueprovider factory at the parameter level, as shown here:
Public Httpresponsemessage Get ( [Valueprovider (typeof (Cookievalueproviderfactory))] GeoPoint location)
This tells the Web API to use the specified value provider factory factory instead of the other registered value provider.
Httpparameterbinding
Model binding is a more common example of a common mechanism. If you look at the [Modelbinder] attribute, you will find that he inherits from a static class Parameterbindingattribute. This static class defines only one method:getbinding. This method returns a httpparameterbinding object.
Public abstract class parameterbindingattribute:attribute{public abstract httpparameterbinding getbinding ( Httpparameterdescriptor parameter);}
The httpparameterbinding is responsible for converting the binding parameter to a value. If [Modelbinder]is set, the property returns a httpparameterbinding that implements the Imodelbinder interface to implement the actual binding. You can also achieve your own httpparameterbinding.
For example, suppose you want to get etags from if-match and if-none-match in a headers of a request. First we need to define the ETag class.
public class etag{Public string Tag {get; set;}}
Again we need to define an enumeration to determine whether the etag is obtained from the if-match or if-none-match in the header.
public enum etagmatch{ ifmatch, Ifnonematch}
Here is an example of the httpparameterbinding class that gets the etag from the header we need and binds it to the ETag type parameter:
public class etagparameterbinding:httpparameterbinding{Etagmatch _match; Public etagparameterbinding (httpparameterdescriptor parameter, Etagmatch match): base (parameter) {_mat ch = match; } public override Task Executebindingasync (Modelmetadataprovider metadataprovider, Httpactioncontext Actioncont Ext, CancellationToken cancellationtoken) {Entitytagheadervalue etagheader = null; Switch (_match) {case ETagMatch.IfNoneMatch:etagHeader = actionContext.Request.Headers. Ifnonematch.firstordefault (); Break Case ETagMatch.IfMatch:etagHeader = ActionContext.Request.Headers.IfMatch.FirstOrDefault (); Break The etag etag = null; if (Etagheader! = null) {ETag = new ETag {Tag = Etagheader.tag}; } Actioncontext.actionarguments[descriptor.parametername] = ETag; var TSC = new TaskcompletionsOurce<object> (); Tsc. Setresult (NULL); Return TSC. Task; }}
The Executebindingasync method executes the binding. This method allows you to add the bound parameter values to the actionargument dictionary in httpactioncontext .
Note: If your executebindingasync method reads a value from the body in the request connection. You need to override the Willreadbody property and let him return true. Because the body of the request may be a read-only, unbuffered data stream. So the Web API defines the rules for enforcement: Only one binding can read the requested body.
In order to implement a custom httpparameterbinding. You can define a property that inherits from Parameterbindingattribute . Two properties are defined in etagparameterbinding , one for if-match headers and the other for If-not-match headers. All two inherit from the static base class.
Public abstract class etagmatchattribute:parameterbindingattribute{ private Etagmatch _match; Public Etagmatchattribute (Etagmatch match) { _match = match; } public override httpparameterbinding getbinding (httpparameterdescriptor parameter) { if (parameter. ParameterType = = typeof (ETag)) { return new etagparameterbinding (parameter, _match); } Return parameter. Bindaserror ("wrong parameter Type");} } public class ifmatchattribute:etagmatchattribute{public Ifmatchattribute () : Base (Etagmatch.ifmatch) { }}public class ifnonematchattribute:etagmatchattribute{public Ifnonematchattribute () : Base (Etagmatch.ifnonematch) { }}
The following example shows how to use the [Ifnonematch] property in the controller's method:
Public Httpresponsemessage Get ([Ifnonematch] ETag etag) {...}
In addition to Parameterbindingattribute, there is another way to associate custom httpparameterbinding . In the httpconfiguration object, theparameterbindingrules property is a (httpparameterdescriptor - Httpparameterbinding) The collection of anonymous functions of type. For example, you can add a rule that all the get methods that contain the ETag parameter use the if-none-matchETagParameterBinding:
Config. Parameterbindingrules.add (P =>{ if (P.parametertype = = typeof (ETag) && p. ActionDescriptor.SupportedHttpMethods.Contains (httpmethod.get)) { return new etagparameterbinding (p, Etagmatch.ifnonematch); } else { return null;} });
Null is returned if the binding parameter is not appropriate.
Iactionvaluebinder
The entire parameter binding process is controlled by a pluggable service named Iactionvaluebinder . The default implementation of Iactionvaluebinder follows these steps:
-
- Find the parameterbindingattribute in the parameters. Contains [Frombody], [Fromuri], and [Modelbinder] or custom properties.
- Otherwise, look for a function that returns a non-empty httpparameterbinding from httpconfiguration.parameterbindingrules .
- Otherwise, use the default rules described above.
- If the argument is simple or has a type converter, it is bound to the URI. This is equivalent to adding the [Fromuri] property to the parameter.
- Otherwise, attempts to read the parameters from the body of the message. Equivalent to adding a [Frombody] property to the parameter.
If you want, you can use a custom implementation instead of the entire iactionvaluebinder service.
Related resources:
Custom Parameter Binding Sample
Mike stall Thanks for a great series of blog posts about Web API parameter bindings:
-
- How Web API does Parameter Binding
- MVC Style parameter binding for Web API
- How to bind to custom objects in action signatures in Mvc/web API
- How to create a custom value provider in Web API
- Web API Parameter binding under the hood
parameter binding for ASP [translate]