Under the hood of ASP.NET Core WebHooks - Model Binding

For the past last posts I was looking at different mechanisms hiding under the hood of ASP.NET Core WebHooks. They were all part of processing which happens before a mapped action is executed. When the action is about to be executed, there is one more thing which needs to be done - binding the parameters. This will be the subject of fifth post in this series:

ASP.NET Core WebHooks are capable of binding several different parameters thanks to WebHookBindingInfoProvider application model. Let's take a look at it.

Built-in binding capabilities

The WebHookBindingInfoProvider application model has single responsibility, it sets the binding information for parameters. It iterates through all action parameters, skip those which already have binding information (for example in result of using model binding attributes) and checks if parameter meet conditions for one of the four supported:

  • If the parameter name is receiver, receiverName or webHookReceiver (case ignored) and its type is String, it will be set to bind the receiver name from path.
  • If the parameter name is id or receiverId and its type is String, it will be set to bind the id from path.
  • If the parameter name is action, actions, actionName, actionNames, event, events, eventName or eventNames and its type is String or IEnumerable<String> implementation, it will be set to bind the event from path.
  • If the parameter name is data it will be set to bind from body as it would be decorated with FromFormAttribute (if the body type in metadata has been set to WebHookBodyType.Form) or FromBodyAttribute (if the body type in metadata has been set to WebHookBodyType.Json or WebHookBodyType.Xml).

If the parameter haven't meet conditions for any of the supported ones, the WebHookBindingInfoProvider will check if given receiver has defined additional action parameters by implementing IWebHookBindingMetadata as part of its metadata. If yes, and the parameter name matches one of them, it will set binding information based on associated WebHookParameter instance. This way receiver can ensure automatic binding for parameters coming from request headers, query parameters or route values.

If all of the above fails, one final attempt to set binding information will be made. If the parameter type is compatible with IFormCollection, JToken or XElement it will be treated as data regardless of its name.

Customizing binding

There are two things regarding binding which one might want to customize: changing the way in which request body is handled and supporting more fancy additional parameters than IWebHookBindingMetadata allows.

The only way to customize the built in behaviour is to use the same extension mechanism which defines it - application model. Thankfully all the application models used by ASP.NET Core WebHooks have exposed order, so one can be placed after them.

internal class WebSubBindingInfoProvider : IApplicationModelProvider
{

    public static int Order => WebHookBindingInfoProvider.Order + 10;

    int IApplicationModelProvider.Order => Order;

    public void OnProvidersExecuted(ApplicationModelProviderContext context)
    { }

    public void OnProvidersExecuting(ApplicationModelProviderContext context)
    { }
}

In case of the WebSub receiver I'm working on, the body type is not known upfront. I wanted to remove the body type validation. To achieve that I needed to remove WebHookVerifyBodyTypeFilter added by WebHookActionModelFilterProvider for actions decorated with WebSubWebHookAttribute.

internal class WebSubBindingInfoProvider : IApplicationModelProvider
{
    public void OnProvidersExecuted(ApplicationModelProviderContext context)
    {
        ...

        for (int controllerIndex = 0; controllerIndex < context.Result.Controllers.Count;
             controllerIndex++)
        {
            ControllerModel controller = context.Result.Controllers[controllerIndex];
            for (int acionIndex = 0; acionIndex < controller.Actions.Count; acionIndex++)
            {
                ActionModel action = controller.Actions[acionIndex];

                WebSubWebHookAttribute attribute =
                    action.Attributes.OfType<WebSubWebHookAttribute>().FirstOrDefault();
                if (attribute == null)
                {
                    continue;
                }

                RemoveWebHookVerifyBodyTypeFilter(action);
            }
        }
    }

    ...

    private static void RemoveWebHookVerifyBodyTypeFilter(ActionModel action)
    {
        IList<IFilterMetadata> filters = action.Filters;

        int webHookVerifyBodyTypeFilterIndex = 0;
        for (; webHookVerifyBodyTypeFilterIndex < filters.Count; webHookVerifyBodyTypeFilterIndex++)
        {
            if (filters[webHookVerifyBodyTypeFilterIndex] is WebHookVerifyBodyTypeFilter)
            {
                break;
            }
        }

        if (webHookVerifyBodyTypeFilterIndex < filters.Count)
        {
            filters.RemoveAt(webHookVerifyBodyTypeFilterIndex);
        }
    }
}

But spinning up application model only to remove an attribute would be a waste of its power. It can be used to provide automatic support for more parameters. I wanted to automatically bind parameters which represent a subscription (WebSubSubscription) related to the request. At the time of model binding custom filters which are part of my receiver have already cached subscription in HttpContext.Items, so it would be best to get it from there. The easiest way to bind parameters from non-standard places is to use a model binder.

internal class WebSubSubscriptionModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        ...

        IDictionary<object, object> items = bindingContext.HttpContext.Items;

        if (items.ContainsKey("WebSubSubscription"))
        {
            bindingContext.Result = ModelBindingResult.Success(items["WebSubSubscription"]);
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
        }

        return Task.CompletedTask;
    }
}

Now it is enough to find parameters with desired type and set its binding source and binder type.

internal class WebSubBindingInfoProvider : IApplicationModelProvider
{
    public void OnProvidersExecuted(ApplicationModelProviderContext context)
    {
        ...

        for (int controllerIndex = 0; controllerIndex < context.Result.Controllers.Count;
             controllerIndex++)
        {
            ControllerModel controller = context.Result.Controllers[controllerIndex];
            for (int acionIndex = 0; acionIndex < controller.Actions.Count; acionIndex++)
            {
                ...
                AddParametersBindingInfos(action);
            }
        }
    }

    ...

    private void AddParametersBindingInfos(ActionModel action)
    {
        for (int parameterIndex = 0; parameterIndex < action.Parameters.Count; parameterIndex++)
        {
            ParameterModel parameter = action.Parameters[parameterIndex];

            if (typeof(WebSubSubscription) == parameter.ParameterType)
            {
                if (parameter.BindingInfo == null)
                {
                    parameter.BindingInfo = new BindingInfo();
                }

                parameter.BindingInfo.BindingSource = BindingSource.ModelBinding;
                parameter.BindingInfo.BinderType = typeof(HttpContextItemsModelBinder);
                parameter.BindingInfo.BinderModelName = "WebSubSubscription";
            }
        }
    }
}

This pattern is very flexible and allows handling anything for which model binder can be implemented.

Parsing the request body at will

There is one more thing worth mentioning. What if you don't want to use model binding for your action or you want to parse the request body in a filter prior to model binding? ASP.NET Core WebHooks got you covered by providing IWebHookRequestReader service. You can obtain its instance from the DI and call ReadAsFormDataAsync or ReadBodyAsync to get the parsed request body. Also the usage of this service makes it safe for body to be read multiple times.

Altogether ASP.NET Core WebHooks should handle any typical data out-of-the-box but can be also bend to handle more unusual scenarios.