Asp.Net Web API 2 Lesson 16 - Parameter Binding in ASP.NET W

Keywords: ASP.NET Attribute JSON

Navigation

Before reading this article, you can also view the Asp.Net Web API 2 series navigation. http://www.cnblogs.com/aehyok/p/3446289.html.

This article mainly explains the following contents:

  Preface

  Ⅰ,Using[FromUri]

  Ⅱ,Using[FromBody]

  Ⅲ,Type Converters

  Ⅳ,Model Binders

  Ⅴ,Value Providers

  Ⅵ,HttpParameterBinding

  Ⅶ,IActionValueBinder

Preface

Before reading this article, you can also view the Asp.Net Web API 2 series navigation. http://www.cnblogs.com/aehyok/p/3446289.html.

When the Web API calls a method in a controller, it must set a value for the parameter. This process is called binding. This article describes how the Web API binds parameters and how to customize the binding process.

By default, the Web API uses the following rules to bind parameters:

1. If the parameter is of a "simple" type, the Web API tries to get the value from the URI. Simple types include. NET primitive types (int,bool,double, etc.) plus TimeSpan, DateTime, Guid, decimal, and string, plus any type that can be converted from a string.

2. For complex types, the Web API attempts to use a media formatter http://www.cnblogs.com/aehyok/p/3460164.html Read the value from the body of the message.

For example, this is a typical Web API controller approach:

HttpResponseMessage Put(int id, Product item) { ... }

This "id" parameter is a "simple" type, so the Web API tries to get the parameter value from the requested URI. This "item" parameter is a complex type, so the Web API tries to use a media formatter to read the parameter value from the body of the request message.

To get values from URIs, the Web API looks at routing data and URI query strings. This routing data is populated when the routing system parses the URI and matches it to the routing. More information about routing: http://www.cnblogs.com/aehyok/p/3444710.html

In the remainder of this article, I'll show you how to customize the process of model binding. For complex types, use media formatters whenever possible. One of the main principles of HTTP is that resources are sent in the body of the message, using content negotiation. http://www.cnblogs.com/aehyok/p/3481265.html To specify the presentation of resources. Media formatter is designed for this purpose.

Using [FromUri]

To better enable the Web API to read complex types from URIs, add [FormUri] attributes to parameters. The following example defines a GeoPoint type, followed by a controller method that obtains the GetPoint parameter 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) { ... }
}

This client can put the values of Latitude and Longitude into the query string. The Web API will use these two parameters to construct a GeoPoint parameter. For example:

http://localhost/api/values/?Latitude=47.678558&Longitude=-122.130989

Using [FromBody]

To better enable the Web API to read a simple type from the body of the message. Add the [FromBody] attribute to the parameter:

public HttpResponseMessage Post([FromBody] string name) { ... }

In this example, the Web API will use the media formatter to read the name value in the body of the message. This is an example of a client request:

POST http://localhost:5076/api/values HTTP/1.1
User-Agent: Fiddler
Host: localhost:5076
Content-Type: application/json
Content-Length: 7

"Alice"

When a parameter has a FromBody property, the Web API uses Content-Type header to select a formatter. In this example, Content-Type is "application/json", and the body of the request is a raw Json string (not a Json object).

At most one parameter is allowed to read values from the body of the message. So the following paragraph will not work:

public HttpResponseMessage Post([FromBody] int id, [FromBody] string name) { ... }

The reason for this rule is that the request body is stored in a Non-buffer stream that can only be read once.

Type Converters

You can also make the Web API treat a class as a simple type by creating a TypeConverter and providing a string conversion.

The next code shows that a GeoPoint class is used to represent a geographical location. Add a TypeConverter to convert strings to GeoPoint instances. The GeoPoint class is decorated with a TypeConverter attribute and specifies the type of the TypeConverter.

    [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 = latitude };
                return true;
            }
            return false;
        }
    }
    public class GeoPointConverter:TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            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 can think of GeoPoint as a simple type. This means that it will be able to bind GeoPoint parameters from the URI. You don't need to add the FromUri attribute to the parameters.

Clients can call this method, such as the following URI:

http://localhost/api/values/?location=47.678558,-122.130989

Model Binders

A more flexible option than a type converter is to create a custom model binding. With model binding, you can use raw values like HTTP requests, Action descriptions, and routing data.

To create a Model Binder, you need to implement the IModelBinder interface, which defines a method, BindModel:

bool BindModel(ModelBindingExecutionContext modelBindingExecutionContext, ModelBindingContext bindingContext);

Next, create a Model Binder for the GeoPoint object.

    public class GeoPointModelBinder:IModelBinder
    {
        private static ConcurrentDictionary<string, GeoPoint> _locations
    = new ConcurrentDictionary<string, GeoPoint>(StringComparer.OrdinalIgnoreCase);

        static GeoPointModelBinder()
        {
            _locations["redmond"] = new GeoPoint() { Latitude = 47.67856, Longitude = -122.131 };
            _locations["paris"] = new GeoPoint() { Latitude = 48.856930, Longitude = 2.3412 };
            _locations["tokyo"] = new GeoPoint() { Latitude = 35.683208, Longitude = 139.80894 };
        }
        public bool BindModel(ModelBindingExecutionContext modelBindingExecutionContext, ModelBindingContext bindingContext)
        {
            if (bindingContext.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 value 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;
        }
    }

A model binder obtains the original input value from a value provider. The design is divided into two separate methods:

1. The value provider receives an HTTP request and fills in a dictionary of key-value pairs.

2. The model binder then fills the model with a dictionary of key-value pairs.

The default value provider in the Web API retrieves values from routing data and query strings. For example, such a URI:

http://localhost/api/values/1?location=48,-122

The value provider will create the following key-value pairs:

id = "1"
location = "48,122"

We assume that the default routing template is used.

The name of the bound parameter is stored on the property ModelBindingContext.ModelName. Model binder looks for the value of a key in the dictionary. If this value exists and can also be converted to GeoPoint, the model binder assigns this value to the ModelBindingContext.Model property.

Note: Model Binder does not restrict a simple type conversion. The model binder will first be found in a list of known locations, and if the search fails, the type converter will be used.

Setting the Model Binder

There are several ways to set Model Binder. First, you can add a Model Binder attribute to the parameter.

public HttpResponseMessage Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)

You can also add the Model Binder attribute to this parameter type. The Web API will specify the model binder to all parameters of this type.

[ModelBinder(typeof(GeoPointModelBinder))]
public class GeoPoint
{
    // ....
}

Finally, you can add a model-binder provider to HttpConfiguration. A model-binder provider is a simple factory class that can create a model binder. You can create a provider by deriving from ModelBinderProvider Class. In any case, if your model binder handles a single type, it is relatively easy to use the Simple Model BinderProvider that has been created.

 

The next code shows how to enable them:

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 a model-binding provider, you still need to add a [Model Binder] attribute to the parameter to tell the Web API that it should use model binder instead of media formatter. But now you don't need to specify the type of model binder on the property.

public HttpResponseMessage Get([ModelBinder] GeoPoint location) { ... }

Value Providers

I mentioned earlier that a model binder takes values from the value provider. Write a custom value provider to implement the IValueProvider interface. This example retrieves the value from the requested cookie.

    public class CookieValueProvider:IValueProvider
    {
        private Dictionary<string, string> _values;

        public CookieValueProvider(HttpActionContext actionContext)
        {
            if (actionContext == null)
            {
                throw new ArgumentNullException("actionContext");
            }

            _values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            foreach (var cookie in actionContext.Request.Headers.GetCookies())
            {
                foreach (CookieState state in cookie.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 also need to create a value provider factory by inheriting from the Value Provider Factory.

    public class CookieValueProviderFactory : ValueProviderFactory
    {
        public override IValueProvider GetValueProvider(HttpActionContext actionContext)
        {
            return new CookieValueProvider(actionContext);
        }
    }

Add the value provider factory to HttpConfiguration with the following code:

public static void Register(HttpConfiguration config)
{
    config.Services.Add(typeof(ValueProviderFactory), new CookieValueProviderFactory());

    // ...
}

The Web API combines all value providers, so when a model binder calls ValueProvider.GetValue, the model binder receives the value it can provide from the first value provider.

Alternatively, you can set the value provider factory at the parameter level by using the ValueProvider property, as follows:

public HttpResponseMessage Get(
    [ValueProvider(typeof(CookieValueProviderFactory))] GeoPoint location)

This tells the Web API model binding to use the specified value provider factory and not to use any other registered value provider.

HttpParameterBinding

Model binding is a feature instance of a more general mechanism. If you see this [Model Binder] attribute, you will see that it is derived from the ParameterBindingAttribute Abstract class. This class defines a separate method and returns an HttpParameterBinding object:

public abstract class ParameterBindingAttribute : Attribute
{
    public abstract HttpParameterBinding GetBinding(HttpParameterDescriptor parameter);
}

The HttpParameterBinding object is responsible for binding a value to a parameter. In the case of [Model Binder] modification, this property returns an implementation of HttpParameterBinding, which uses an I ModelBinder to show the real binding. You can also implement your own HttpParameterBinding.

For example, suppose you want to get ETags from the header of the request if-match and if-none-match. We will start by defining a class instead of ETags.

public class ETag
{
    public string Tag { get; set; }
}

We also define an enumeration to indicate whether ETag is obtained from the header of if-match and if-none-match.

public enum ETagMatch
{
    IfMatch,
    IfNoneMatch
}

Here is HttpParameterBinding, which takes the ETag from the required header and binds it to the ETag of the type parameter:

public class ETagParameterBinding : HttpParameterBinding
    {
        ETagMatch _match;

        public ETagParameterBinding(HttpParameterDescriptor parameter, ETagMatch match)
            : base(parameter)
        {
            _match = match;
        }

        public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
            HttpActionContext actionContext, 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;
            }

            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 handles binding. In this method, parameter values are added to the ActionArgument dictionary and in the HttpActionContext.

If your Execute BindingAsync method reads the request body. Rewrite the WillReadBody property to return true. This message body may be an unbuffered stream that can only be read once. Therefore, the Web API enforces a rule that at most one binding can read the message body.

Applying a custom HTTP Parameter Binding, you can define an attribute derived from the Parameter Binding Attribute. For ETag Parameter Binding, we will define two attributes: one for if-match Header and the other for if-none-match Header. Both derive from an abstract 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)
    {
    }
}

This is a controller method that uses the [IfNoneMatch] attribute.

public HttpResponseMessage Get([IfNoneMatch] ETag etag) { ... }

In addition to ParameterBindingAttribute, there is another hook for adding a custom HTTP ParameterBinding. On the HttpConfiguration object, ParameterBindingRules is a collection of anonymous method types (HttpParameterDescriptor - > HttpParameterBinding). For example, you can add a rule that any ETag parameter in the Get request method uses ETagParameterBinding with if-none-match.

config.ParameterBindingRules.Add(p =>
{
    if (p.ParameterType == typeof(ETag) && 
        p.ActionDescriptor.SupportedHttpMethods.Contains(HttpMethod.Get))
    {
        return new ETagParameterBinding(p, ETagMatch.IfNoneMatch);
    }
    else
    {
        return null;
    }
});

This method should return null for parameters that are not applicable to binding.

IActionValueBinder

The whole parameter binding process is controlled by a pluggable service, IActionValueBinder. The default implementation of IActionValueBinder will perform the following actions:

1. View ParameterBindingAttribute on parameters, including [FromBody], [FromUri], and [Model Binder], or custom attributes.

2. Otherwise, look at a function's HttpConfiguration.ParameterBindingRules, which returns a non-null HttpParameterBinding.

3. Otherwise, use the default rules I described earlier.

(1) If the parameter type is "simple" or has a type converter, it will be bound from the URI. It is equivalent to adding [FromUri] attributes to parameters.

(2) Otherwise, an attempt is made to read parameters from the body of the message, which is equivalent to adding [FromBody] attributes to the parameters.

If you need, you can replace the entire IActionValueBinder with a custom implementation.

summary

This article mainly talks about parameter binding, but it can also be seen from the above that there are many knowledge points involved, but they are very practical, and the examples are clear. However, it still needs to be applied in the project in order to better learn and master the link of parameter binding.

The reference link for this article is http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api 

Reference page:

http://www.yuanjiaocheng.net/webapi/create-crud-api-1-delete.html

http://www.yuanjiaocheng.net/webapi/Consume-web-api.html

http://www.yuanjiaocheng.net/webapi/mvc-consume-webapi-get.html

http://www.yuanjiaocheng.net/webapi/mvc-consume-webapi-post.html

http://www.yuanjiaocheng.net/webapi/mvc-consume-webapi-put.html

it learning materials

Posted by dspeer on Mon, 24 Dec 2018 17:21:06 -0800