(I had this sitting in draft form for years now, and I just dug it up. Most of it is pretty obsolete, with .net core and all, but I’ll just leave this here.)
In a previous post, I discussed a method for using abstract classes in ASP.NET MVC 4.0. With it, I was able to clean up and hide the details of implementation from much of my code. I assumed that it would be relatively simple to do the equivalent in Web API, but I found that the task to be quite different.
The equivalent functionality requires use of asynchronous programming, code generation, and some complex processing for handling things like IEnumerable
. I also found some race conditions that needed 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;
}
}
1 Response
[…] [Update June, 2018: I finally just dumped the WebAPI version of this here.] […]