Search code examples
.netasp.net-web-apiparametersasp.net-web-api2

Implement the default parameter binding behavior in .NET Web API


I am implementing a custom parameter binder that inherits HttpParameterBinding and will apply custom behavior to certain parameters. In some cases I do not want to apply the custom behavior in which case I want to follow whatever Web API does by default. This decision would be made in ExecuteBindingAsync. How do I implement this default behavior in ExecuteBindingAsync?

I believe this is typically done by simply not applying the parameter binding when registering the binding during startup (in other words the handler to the ParameterBindingRules collection would return null, allowing Web API to bind the default binding to the parameter). However in my case I need to decide whether to apply the binding at runtime, so I need to do this in ExecuteBindingAsync.

I am looking to do something like the following in my custom HttpParameterBinding class:

public override async Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
{
    if (IsCustomBindingNeeded()) {
        // apply custom binding logic... call SetValue()... I'm good with this part
    }
    else {
        // ***************************************************************
        // This is where I want to use the default implementation.
        // Maybe something like this (using a made up class name):

        await return new WhateverDefaultParameterBinding().ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken);

        // ...or at least be able to call GetValue() and get the correct value and then I can call SetValue()
        // ***************************************************************
    }
}

I've tried calling GetValue() but it always returns null. I assume there is some additional step that needs to be performed so that the base class (HttpParameterBinding) can create the value.

My preference is to directly call whatever method within the .NET framework contains that default logic. I would prefer not to have to duplicate that logic.


Solution

  • I found a solution although I'm not sure if it is ideal.

    I discovered the Mono code for DefaultActionValueBinder. The GetParameterBinding method seems to contain the logic I'm looking for. However this method is protected so I can't call it directly. (I could probably call the public GetBinding method but I'm worried that would be overkill.) So I would have to duplicate the logic from GetParameterBinding in my own class, plus some of the methods from the internal TypeHelper class which it references, which is why I don't think this solution is ideal. I'm also not sure how well vetted the Mono implementation is so I'm concerned it could be buggy or not support all scenarios.

    For future reference, this is the current Mono implementation of DefaultActionValueBinder.GetParameterBinding()...

        protected virtual HttpParameterBinding GetParameterBinding(HttpParameterDescriptor parameter)
        {
            // Attribute has the highest precedence
            // Presence of a model binder attribute overrides.
            ParameterBindingAttribute attr = parameter.ParameterBinderAttribute;
            if (attr != null)
            {
                return attr.GetBinding(parameter);
            }
    
            // No attribute, so lookup in global map.
            ParameterBindingRulesCollection pb = parameter.Configuration.ParameterBindingRules;
            if (pb != null)
            {
                HttpParameterBinding binding = pb.LookupBinding(parameter);
                if (binding != null)
                {
                    return binding;
                }
            }
    
            // Not explicitly specified in global map or attribute.
            // Use a default policy to determine it. These are catch-all policies. 
            Type type = parameter.ParameterType;
            if (TypeHelper.IsSimpleUnderlyingType(type) || TypeHelper.HasStringConverter(type))
            {
                // For simple types, the default is to look in URI. Exactly as if the parameter had a [FromUri] attribute.
                return parameter.BindWithAttribute(new FromUriAttribute());
            }
    
            // Fallback. Must be a complex type. Default is to look in body. Exactly as if this type had a [FromBody] attribute.
            attr = new FromBodyAttribute();
            return attr.GetBinding(parameter);
        }
    

    ...and the referenced methods from TypeHelper.

        internal static bool IsSimpleType(Type type)
        {
            return type.IsPrimitive ||
                   type.Equals(typeof(string)) ||
                   type.Equals(typeof(DateTime)) ||
                   type.Equals(typeof(Decimal)) ||
                   type.Equals(typeof(Guid)) ||
                   type.Equals(typeof(DateTimeOffset)) ||
                   type.Equals(typeof(TimeSpan));
        }
    
        internal static bool IsSimpleUnderlyingType(Type type)
        {
            Type underlyingType = Nullable.GetUnderlyingType(type);
            if (underlyingType != null)
            {
                type = underlyingType;
            }
    
            return TypeHelper.IsSimpleType(type);
        }
    
        internal static bool HasStringConverter(Type type)
        {
            return TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string));
        }