Sdílet prostřednictvím


How to create a custom value provider in WebAPI

Here’s how you can easily customize WebAPI parameter binding to include values from source other than the url.  The short answer is that you add a custom ValueProvider and use Model Binding, just like in MVC.

ValueProviders are used to provide values for simple types and match on parameter name.   ValueProviders serve up raw pieces of information and feed into the Model Binders. Model Binders compose that information (eg, building collections or complex types) and do type coercion (eg, string to int, invoke type converters, etc). 

Here’s a custom value provider that extracts information from the request headers.  in this case, our action will get the userAgent and host from the headers. This doesn’t interfere with other parameters, so you can still get the id from the  query string as normal and read the body.

     public class ValueProviderTestController : ApiController
    {
        [HttpGet]
        public object GetStuff(string userAgent, string host, int id)
        {   
            // userAgent and host are bound from the Headers. id is bound from the query string. 
            // This just echos back. Do something interesting instead.
            return string.Format(
@"User agent: {0},
host: {1}
id: {2}", userAgent, host, id);
        }
    }

 

So when I run it and hit it from a browser, I get a string back like so:

User agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0),

host: localhost:8080

id: 45

Note that the client needs to set the headers in the request. Browsers will do this. But if you just call directly with HttpClient.GetAsync(), the headers will be empty.

 

We define a HeaderValueProviderFactory class (source below), which derives from ValueProviderFactory and supplies model binding with the information from the header.

We need to register the header value provider.  We can do it globally in the config, like so:

             // Append our custom valueprovider to the list of value providers.
            config.Services.Add(typeof(ValueProviderFactory), new HeaderValueProviderFactory());

Or we can do it just on a specific parameter without touching global config by using the [ValueProvider] attribute, like so:

     public object GetStuff([ValueProvider(typeof(HeaderValueProviderFactory))] string userAgent)

The [ValueProvider] attribute just derives from the [ModelBinder] attribute and says “use the default model binding, but supply these value providers”.

What’s happening under the hood? For refresher reading, see How WebAPI does parameter binding. In this case, it sees the parameter is a simple type (string), and so it will bind via Model Binding. Model binding gets a list of value providers (either from the attribute or the configuration), and then looks at the name of the parameter (userAgent, host, id) from that list.  Model Binding will also do composition and coercion.

 

 

Sources

WebAPI is an open source project and some of this may need the post-beta source. For example, service resolver has been cleaned up since beta, so it’s now easier to add new services.

Here’s the source for a test client: It includes a loop so that you can hit the service from a browser.

         static void TestHeaderValueProvider()
        {
            string prefix = "https://localhost:8080";
            HttpSelfHostConfiguration config = new HttpSelfHostConfiguration(prefix);
            config.Routes.MapHttpRoute("Default", "{controller}/{action}");

            // Append our custom valueprovider to the list of value providers.
            config.Services.Add(typeof(ValueProviderFactory), new HeaderValueProviderFactory());
            
            HttpSelfHostServer server = new HttpSelfHostServer(config);
            server.OpenAsync().Wait();

            try
            {
                // HttpClient will make the call, but won't set the headers for you. 
                HttpClient client = new HttpClient();
                var response = client.GetAsync(prefix + "/ValueProviderTest/GetStuff?id=20").Result;

                // Browsers will set the headers. 
                // Loop. You can hit the request via: https://localhost:8080/Test2/GetStuff?id=40
                while (true)
                {
                    Thread.Sleep(1000);
                    Console.Write(".");
                }
            }
            finally
            {
                server.CloseAsync().Wait();
            

 

Here’s the full source for the provider.

 

 using System.Globalization;
using System.Net.Http.Headers;
using System.Reflection;
using System.Web.Http.Controllers;
using System.Web.Http.ValueProviders;

namespace Basic
{
    // ValueProvideFactory. This is registered in the Service resolver like so:
    //    config.Services.Add(typeof(ValueProviderFactory), new HeaderValueProviderFactory());
    public class HeaderValueProviderFactory : ValueProviderFactory
    {
        public override IValueProvider GetValueProvider(HttpActionContext actionContext)
        {
            HttpRequestHeaders headers = actionContext.ControllerContext.Request.Headers;
            return new HeaderValueProvider(headers);
        }
    }

    // ValueProvider for extracting data from headers for a given request message. 
    public class HeaderValueProvider : IValueProvider
    {
        readonly HttpRequestHeaders _headers;

        public HeaderValueProvider(HttpRequestHeaders headers)
        {
            _headers = headers;
        }

        // Headers doesn't support property bag lookup interface, so grab it with reflection.
        PropertyInfo GetProp(string name)
        {
            var p = typeof(HttpRequestHeaders).GetProperty(name, 
                    BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
            return p;
        }

        public bool ContainsPrefix(string prefix)
        {
            var p = GetProp(prefix);
            return p != null;
        }

        public ValueProviderResult GetValue(string key)
        {
            var p = GetProp(key);
            if (p != null)
            {
                object value = p.GetValue(_headers, null);
                string s = value.ToString(); // for simplicity, convert to a string
                return new ValueProviderResult(s, s, CultureInfo.InvariantCulture);
            }
            return null; // none
        }
    }
}

Comments

  • Anonymous
    June 28, 2018
    Note that you may need to use config.Services.Insert(typeof(ValueProviderFactory), 0, new MyFactory()) instead of .Add() if you need to intercept requests which the framework-provided provider traps and handles incorrectly.Also, to get your provider to run, you may need to mark your factory with IUriValueProviderFactory.Could this information be integrated into the post and explained? Thanks.