While writing the previous blog post I noticed that Outlook sends an additional header “IfModifiedSince” when updating its subscription of the iCal feed. It would be nice to support this additional parameter in the API to retrieve appointments. Instead of always returning the entire list of appointments, an additional filter will be used to limit the appointments to those that were changed since our last synchronization.
While we could just read the header value inside of our controller action, it would be much nicer if our action would receive it as a parameter. The extension points we need in this case fall under the model binding category and while it shares the same idea and goals with ASP.NET MVC, there are some differences between ASP.NET MVC and WebAPI. There is a great MSDN article which covers most of the things we need.
The two concepts we need to grasp are model binders and value providers. Value providers are an abstraction over, well, values. For instance there’s a query string value provider that reads the query string and will allow those values to be used as parameters in your actions or by model binders. Model binders actually do something with values, they will use value providers to retrieve i.e. a first name and last name value and create a more complex instance.
So in this case we need to read a value from a header inside our HttpRequestMessage. Let’s implement a value provider for our IfModifiedSince header by implementing the IValueProvider interface.
public class IfModifiedValuesProvider : IValueProvider { private HttpRequestMessage _request; private const string header = "IfModifiedSince"; public IfModifiedValuesProvider(HttpRequestMessage requestMessage) { _request = requestMessage; } public bool ContainsPrefix(string prefix) { bool found = false; if (string.Equals(header, prefix, StringComparison.OrdinalIgnoreCase)) { found = _request.Headers.Any(x => x.Key == prefix); } return found; } public ValueProviderResult GetValue(string key) { var headerValue = _request.Headers.IfModifiedSince; ValueProviderResult result = null; if (headerValue.HasValue) { result = new ValueProviderResult(headerValue, headerValue.ToString(), CultureInfo.InvariantCulture); } return result; } } |
The two methods we need to implement are ContainsPrefix and GetValue. The ContainsPrefix is of no importance in this case, GetValue is where the magic happens. In this method we read the the value from the header and, if it exists, return a ValueProviderResult populated with the current values. ValueProviders are always accompanied by ValueProviderFactories. It’s the responsibility of the factory to create and setup the value provider. In this case we want to supply our value provider with a reference to the current HttpRequestMessage.
public class IfModifiedValuesProviderFactory : ValueProviderFactory { public override IValueProvider GetValueProvider(HttpActionContext actionContext) { return new IfModifiedValuesProvider(actionContext.Request); } } |
Inheriting from the abstract base ValueProviderFactory allows us to override the GetValueProvider method where we initialize our value provider. We now have enough infrastructure to go back to our AppointmentController.
public IEnumerable<AppointmentModel> Get([ValueProvider(typeof(IfModifiedValuesProviderFactory))]DateTimeOffset? ifModifiedSince = null) { IEnumerable<AppointmentModel> models = null; using (var context = new AppointmentsEntities()) { IQueryable appointments = context.Appointments; if (ifModifiedSince.HasValue) { appointments = appointments.Where(x => x.LastModifiedDate >= ifModifiedSince.Value); } models = MapAppointents(appointments); } return models; } |
By decorating the ifModifiedSince parameter with the ValueProvider attribute it will be populated with the result of the GetValue call. Which does resolve our issue, but it would be even better if users of our API would be able to pass the ifModifiedSince date by using the header or supply it via a parameter in the query string. There are several ways to make this happen.
One approach would be to use the ValueProvider attribute again, chaining along every value provider we want to use.
public IEnumerable<AppointmentModel> Get( [ValueProvider(typeof(IfModifiedValuesProviderFactory), typeof(QueryStringValueProviderFactory ))] DateTimeOffset? ifModifiedSince = null) { // omitted } |
Adding the QueryStringValueProviderFactory to the list of value providers will help us, but every time we want to add another source of our ifModifiedSince parameter we will have to add it here.
A better approach is to remove the attribute on the parameter entirely and add our value provider to the configuration of our WebAPI.
public static void Register(HttpConfiguration config) { config.Services.Add(typeof(ValueProviderFactory), new IfModifiedValuesProviderFactory()); } |
If we now run the application and use a query string to supply the value for our action, we will see that the date is passed along to our controller. Unfortunately if a client application uses the header, our custom value provider is not invoked at all. What’s missing?
Well it turns out that when you declare actions on your controller, by default only data that’s present in the route data dictionary or the query string will be passed to the controller. It’s like putting [FromUri] on your parameters. If we want have our own value provider come into play we have to use the [ModelBinder] attribute as well.
public IEnumerable<AppointmentModel> Get([ModelBinder]DateTimeOffset? ifModifiedSince = null) { // omitted } |
Now we’re telling WebAPI to use the model binding infrastructure. The default model binder will use all the registered value providers to create a match. Since we’ve registered our IfModifiedValuesProviderFactory in the WebAPI configuration, it will be automatically picked up. If a user of our API uses a query string to pass along the ifModifiedSince value, that will keep working as well. If we add a CookieValueProvider in the future, we will only have to implement the value provider and add it to the configuration of our application. We will not have to inspect every method to see where we should add them explicitly. Best of both worlds. There’s a nice poster of the lifecycle of an HttpRequestMessage on MSDN which includes an illustration on how model binding works.