Abstract classes as action parameters in .NET Web API 4.0

(I had this sit­ting in draft form for years now, and I just dug it up. Most of it is pret­ty obso­lete, with .net core and all, but I’ll just leave this here.)

In a pre­vi­ous post, I dis­cussed a method for using abstract class­es in ASP.NET MVC 4.0. With it, I was able to clean up and hide the details of imple­men­ta­tion from much of my code. I assumed that it would be rel­a­tive­ly sim­ple to do the equiv­a­lent in Web API, but  I found that the task to be quite different.

The equiv­a­lent func­tion­al­i­ty requires use of asyn­chro­nous pro­gram­ming, code gen­er­a­tion, and some com­plex pro­cess­ing for han­dling things like IEnumerable. I also found some race con­di­tions that need­ed to be addressed.

public class ViewModelParameterBinding : HttpParameterBinding
    {
        private static readonly ConcurrentDictionary<Type, Func<object, IDictionary<string, object>>> ModelToDictionaryFuncs =
            new ConcurrentDictionary<Type, Func<object, IDictionary<string, object>>>();

        private readonly IModelResolver modelResolver;
        private readonly RequestModelAttribute requestModelAttribute;

        /// <summary>
        /// Initializes a new instance of the <see cref="ViewModelParameterBinding" /> class.
        /// </summary>
        /// <param name="descriptor">The descriptor.</param>
        /// <param name="requestModelAttribute">The request model attribute.</param>
        /// <param name="modelResolver">The model resolver.</param>
        private ViewModelParameterBinding(HttpParameterDescriptor descriptor, RequestModelAttribute requestModelAttribute, IModelResolver modelResolver)
            : base(descriptor)
        {
            this.requestModelAttribute = requestModelAttribute;
            this.modelResolver = modelResolver;
        }

        /// <summary>
        /// Returns a value indicating whether this <see cref="T:System.Web.Http.Controllers.HttpParameterBinding" /> instance will read the body of the HTTP message.
        /// </summary>
        /// <returns>true if this <see cref="T:System.Web.Http.Controllers.HttpParameterBinding" /> will read the body; otherwise, false.</returns>
        public override bool WillReadBody
        {
            get
            {
                if (requestModelAttribute.SentWillReadBody)
                {
                    return false;
                }

                requestModelAttribute.SentWillReadBody = true;
                return true;
            }
        }

        /// <summary>
        /// Method that can be used to add to <see>
        ///     <cref>P:System.Web.Http.GlobalConfiguration.Configuration.ParameterBindingRules</cref>
        /// </see>
        ///     .
        /// </summary>
        /// <param name="modelResolver">The model resolver.</param>
        /// <returns>
        /// An instance of <see cref="ViewModelParameterBinding" /> for the parameter,
        /// or <c>null</c> if the method does not have <see cref="RequestModelAttribute" /> applied.
        /// </returns>
        // Borrowed this pattern from http://www.west-wind.com/weblog/posts/2012/Sep/11/Passing-multiple-simple-POST-Values-to-ASPNET-Web-API
        public static Func<HttpParameterDescriptor, HttpParameterBinding> BindingRule(IModelResolver modelResolver)
        {
            return descriptor =>
            {
                RequestModelAttribute requestModelAttribute =
                    descriptor.ActionDescriptor.GetCustomAttributes<RequestModelAttribute>().FirstOrDefault();
                return requestModelAttribute == null
                           ? null
                           : new ViewModelParameterBinding(descriptor, requestModelAttribute, modelResolver);
            };
        }

        /// <summary>
        /// Asynchronously executes the binding for the given request.
        /// </summary>
        /// <param name="metadataProvider">Metadata provider to use for validation.</param>
        /// <param name="actionContext">The action context for the binding. The action context contains the parameter dictionary that will get populated with the parameter.</param>
        /// <param name="cancellationToken">Cancellation token for cancelling the binding operation.</param>
        /// <returns>
        /// A task object representing the asynchronous operation.
        /// </returns>
        public override async Task ExecuteBindingAsync(
            ModelMetadataProvider metadataProvider,
            HttpActionContext actionContext,
            CancellationToken cancellationToken)
        {
            if (actionContext.Request.Content != null)
            {
                IDictionary<string, object> parameters = await ParseBody(actionContext.Request).ConfigureAwait(false);

                object value;
                if (parameters == null || !parameters.TryGetValue(Descriptor.ParameterName, out value))
                {
                    return;
                }

                var parameterType = Descriptor.ParameterType;
                if (parameterType.IsInstanceOfType(value) || (!parameterType.IsValueType && value == null))
                {
                    SetValue(actionContext, value);
                    return;
                }

                var valueType = value.GetType();
                // IEnumerable
                if (typeof(IEnumerable).IsAssignableFrom(parameterType) &&
                    typeof(IEnumerable).IsAssignableFrom(valueType))
                {
                    Type innerType = parameterType.IsArray
                        ? parameterType.GetElementType()
                        : EnumerableTypes(parameterType)
                            .Select(a => new
                            {
                                vm =
                                    a.GetCustomAttributes(typeof(ViewModelAttribute), true).FirstOrDefault() as
                                        ViewModelAttribute,
                                type = a
                            })
                            .Where(a => a.vm != null && EnumerableTypes(valueType).Contains(a.vm.ViewModelType))
                            .Select(a => a.type)
                            .FirstOrDefault() ?? typeof(object);

                    var genericEnumerableType = typeof(IEnumerable<>).MakeGenericType(innerType);
                    if (genericEnumerableType == parameterType)
                    {
                        var modelEnumerable = ((IEnumerable)value).Cast<IViewModel>()
                            .Select(
                                vm =>
                                {
                                    var m = modelResolver.ResolveModel(innerType, vm);
                                    if (m != null)
                                    {
                                        m.Initialize(vm, modelResolver);
                                    }
                                    return m;
                                });
                        MethodInfo castExtensionMethod =
                            typeof(Enumerable).GetMethod("Cast")
                                .MakeGenericMethod(innerType);
                        var actualEnumerable = castExtensionMethod.Invoke(
                            null,
                            new object[] { modelEnumerable });
                        SetValue(
                            actionContext,
                            actualEnumerable);
                        return;
                    }
                }

                var viewModelAttribute =
                    parameterType.GetCustomAttributes(typeof(ViewModelAttribute), true)
                        .FirstOrDefault() as ViewModelAttribute;
                var viewModel = value as IViewModel;
                if (viewModelAttribute == null || viewModel == null
                    || valueType != viewModelAttribute.ViewModelType)
                {
                    Descriptor.BindAsError("Unable to bind parameter");
                    return;
                }

                IModel model = modelResolver.ResolveModel(parameterType, viewModel);
                if (model == null)
                {
                    Descriptor.BindAsError("Unable to bind parameter");
                }
                else
                {
                    model.Initialize(viewModel, modelResolver);
                    SetValue(actionContext, model);
                }

            }
        }

        private static IEnumerable<Type> EnumerableTypes(Type type)
        {
            return
                type.GetInterfaces()
                    .Concat(new[] { type })
                    .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
                    .Select(t => t.GetGenericArguments()[0]);
        }

        /// <summary>
        /// Emits a function that returns an <see cref="IDictionary{TKey,TValue}"/> of the incoming parameters
        /// </summary>
        /// <param name="requestModelAttribute">The request model attribute.</param>
        /// <returns>The dynamic function delegate.</returns>
        private static Func<object, IDictionary<string, object>> RequestModelToDictionaryFunc(RequestModelAttribute requestModelAttribute)
        {
            var methodArgs = new[] { typeof(object) };
            var method =
                new DynamicMethod(
                    "DynamicDictionaryPopulate_" + requestModelAttribute.RequestModelType.FullName,
                    typeof(IDictionary<string, object>),
                    methodArgs);
            ILGenerator il = method.GetILGenerator();

            LocalBuilder requestModel = il.DeclareLocal(requestModelAttribute.RequestModelType);
            LocalBuilder outputDictionary = il.DeclareLocal(typeof(Dictionary<string, object>));

            il.Emit(OpCodes.Ldarg_0);  // Push the object argument on the stack
            il.Emit(OpCodes.Castclass, requestModelAttribute.RequestModelType); // Cast the object to <requestModelType>
            il.Emit(OpCodes.Stloc_S, requestModel); // Load the request model into a local variable

            ConstructorInfo dictionaryConstructor = typeof(Dictionary<string, object>).GetConstructor(Type.EmptyTypes);
            il.Emit(OpCodes.Newobj, dictionaryConstructor); // Construct a Dictionary<string, object>
            il.Emit(OpCodes.Stloc_S, outputDictionary); // Store it in the outputDictionary local variable

            MethodInfo addMethod = typeof(Dictionary<string, object>).GetMethod("Add");
            foreach (PropertyInfo pi in requestModelAttribute.RequestModelType.GetProperties())
            {
                string name = pi.Name;
                if (requestModelAttribute.ParameterNameConvention == ParameterNameConvention.CamelCase)
                {
                    name = char.ToLowerInvariant(name[0]) + name.Substring(1);
                }

                il.Emit(OpCodes.Ldloc_S, outputDictionary); // Push the dictionary to the stack
                il.Emit(OpCodes.Ldstr, name); // Push the name of the property to the stack
                il.Emit(OpCodes.Ldloc_S, requestModel); // Push the request model variable to the stack
                il.Emit(OpCodes.Call, pi.GetGetMethod()); // Call the get method for the property
                if (pi.PropertyType.IsValueType)
                {
                    il.Emit(OpCodes.Box, pi.PropertyType); // Box value types.
                }
                il.Emit(OpCodes.Call, addMethod); // Add the name and the value of the property to the dictionary
            }

            il.Emit(OpCodes.Ldloc_S, outputDictionary); // Push the dictionary to the stack
            il.Emit(OpCodes.Ret); // Return the dictionary

            return method.CreateDelegate<Func<object, IDictionary<string, object>>>();
        }

        /// <summary>
        /// Parses the body.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns>A dictionary of items found within the requestModel object.</returns>
        private async Task<IDictionary<string, object>> ParseBody(HttpRequestMessage request)
        {
            IDictionary<string, object> parameters;
            if (request.Properties.TryGetValue("RequestParameters", out parameters))
            {
                return parameters;
            }

            object requestModel =
                await
                    request.Content.ReadAsAsync(requestModelAttribute.RequestModelType,
                        GlobalConfiguration.Configuration.Formatters).ConfigureAwait(false);

            Func<object, IDictionary<string, object>> getParametersFunc;

            if (!ModelToDictionaryFuncs.TryGetValue(requestModelAttribute.RequestModelType,
                out getParametersFunc))
            {
                ModelToDictionaryFuncs.TryAdd(requestModelAttribute.RequestModelType,
                    RequestModelToDictionaryFunc(requestModelAttribute));
            getParametersFunc = ModelToDictionaryFuncs[requestModelAttribute.RequestModelType];
        }

        parameters = getParametersFunc(requestModel);

        request.Properties.Add("RequestParameters", parameters);

        return parameters;
    }
}

public static class GenericExtensions
{
    [Pure]
    public static bool TryGetValue<TKey, TValue>(this IDictionary<TKey, object> dictionary, TKey key, out TValue value) where TValue : class
    {
        object v;
        bool result = dictionary.TryGetValue(key, out v);
        value = result ? v as TValue : null;

        return result;
    }

    public static T CreateDelegate<T>(this DynamicMethod dynamicMethod) where T : class
    {
        return dynamicMethod.CreateDelegate(typeof(T)) as T;
    }
}
Share