Consume ModelBinding from Mvc

* Modify ReflectedActionInvoker to model bind parameters
* Introduce IBodyReader for reading request bodies
* Introduce types for per-action-context specific binders, value providers
  and body readers
This commit is contained in:
Pranav K 2014-02-24 19:21:37 -08:00
parent 9dc79df9cf
commit e87f8c372c
20 changed files with 376 additions and 56 deletions

View File

@ -10,6 +10,22 @@ namespace MvcSample
return View("MyView", User());
}
/// <summary>
/// Action that exercises query\form based model binding.
/// </summary>
public IActionResult SaveUser(User user)
{
return View("MyView", user);
}
/// <summary>
/// Action that exercises input formatter
/// </summary>
public IActionResult Post([FromBody]User user)
{
return View("MyView", user);
}
public IActionResult Something()
{
return new ContentResult

View File

@ -40,14 +40,16 @@ namespace MvcSample
var endpoint = ActivatorUtilities.CreateInstance<RouteEndpoint>(mvcServices.Services);
router.Add(new TemplateRoute(
endpoint,
"{controller}/{action}",
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase) { { "controller", "Home" }, { "action", "Index" } }));
router.Add(new TemplateRoute(
endpoint,
"{controller}",
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase) { { "controller", "Home" } }));
router.Add(new TemplateRoute(
endpoint,
"{controller}/{action}",
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase) { { "controller", "Home" }, { "action", "Index" } }));
}
}
}

View File

@ -7,13 +7,16 @@ namespace Microsoft.AspNet.Mvc
private readonly IActionResultFactory _actionResultFactory;
private readonly IServiceProvider _serviceProvider;
private readonly IControllerFactory _controllerFactory;
private readonly IActionBindingContextProvider _bindingProvider;
public ActionInvokerProvider(IActionResultFactory actionResultFactory,
IControllerFactory controllerFactory,
IActionBindingContextProvider bindingProvider,
IServiceProvider serviceProvider)
{
_actionResultFactory = actionResultFactory;
_controllerFactory = controllerFactory;
_bindingProvider = bindingProvider;
_serviceProvider = serviceProvider;
}
@ -24,21 +27,20 @@ namespace Microsoft.AspNet.Mvc
public void Invoke(ActionInvokerProviderContext context, Action callNext)
{
var ad = context.ActionContext.ActionDescriptor as ReflectedActionDescriptor;
var actionDescriptor = context.ActionContext.ActionDescriptor as ReflectedActionDescriptor;
if (ad != null)
if (actionDescriptor != null)
{
context.Result = new ReflectedActionInvoker(
context.ActionContext,
ad,
_actionResultFactory,
_controllerFactory,
_serviceProvider);
context.ActionContext,
actionDescriptor,
_actionResultFactory,
_controllerFactory,
_bindingProvider,
_serviceProvider);
}
callNext();
}
}
}

View File

@ -0,0 +1,15 @@
using System;
namespace Microsoft.AspNet.Mvc
{
public class BodyParameterInfo
{
public BodyParameterInfo(Type parameterType)
{
ParameterType = parameterType;
}
public Type ParameterType { get; private set; }
}
}

View File

@ -3,21 +3,19 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.DependencyInjection;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
public class DefaultActionSelector : IActionSelector
{
private readonly INestedProviderManager<ActionDescriptorProviderContext> _actionDescriptorProvider;
private readonly IEnumerable<IValueProviderFactory> _valueProviderFactory;
private readonly IActionBindingContextProvider _bindingProvider;
public DefaultActionSelector(
INestedProviderManager<ActionDescriptorProviderContext> actionDescriptorProvider,
IEnumerable<IValueProviderFactory> valueProviderFactories)
public DefaultActionSelector(INestedProviderManager<ActionDescriptorProviderContext> actionDescriptorProvider,
IActionBindingContextProvider bindingProvider)
{
_actionDescriptorProvider = actionDescriptorProvider;
_valueProviderFactory = valueProviderFactories;
_bindingProvider = bindingProvider;
}
public async Task<ActionDescriptor> SelectAsync(RequestContext context)
@ -61,9 +59,6 @@ namespace Microsoft.AspNet.Mvc
protected virtual async Task<ActionDescriptor> SelectBestCandidate(RequestContext context, List<ActionDescriptor> candidates)
{
var valueProviders = await Task.WhenAll(_valueProviderFactory.Select(vpf => vpf.GetValueProviderAsync(context)));
valueProviders = valueProviders.Where(vp => vp != null).ToArray();
var applicableCandiates = new List<ActionDescriptorCandidate>();
foreach (var action in candidates)
{
@ -72,18 +67,20 @@ namespace Microsoft.AspNet.Mvc
{
Action = action,
};
var actionContext = new ActionContext(context.HttpContext, context.RouteValues, action);
var actionBindingContext = await _bindingProvider.GetActionBindingContextAsync(actionContext);
foreach (var parameter in action.Parameters.Where(p => !p.Binding.IsFromBody))
foreach (var parameter in action.Parameters.Where(p => p.ParameterBindingInfo != null))
{
if (valueProviders.Any(vp => vp.ContainsPrefix(parameter.Binding.Prefix)))
if (actionBindingContext.ValueProvider.ContainsPrefix(parameter.ParameterBindingInfo.Prefix))
{
candidate.FoundParameters++;
if (parameter.Binding.IsOptional)
if (parameter.IsOptional)
{
candidate.FoundOptionalParameters++;
}
}
else if (!parameter.Binding.IsOptional)
else if (!parameter.IsOptional)
{
isApplicable = false;
break;

View File

@ -1,5 +1,4 @@

using System.Reflection;
using System.Reflection;
namespace Microsoft.AspNet.Mvc
{
@ -7,24 +6,29 @@ namespace Microsoft.AspNet.Mvc
{
public ParameterDescriptor GetDescriptor(ParameterInfo parameter)
{
var bindingInfo = new ParameterBindingInfo()
{
IsOptional = parameter.IsOptional,
IsFromBody = IsFromBody(parameter),
Prefix = parameter.Name,
};
bool isFromBody = IsFromBody(parameter);
return new ParameterDescriptor()
return new ParameterDescriptor
{
Name = parameter.Name,
Binding = bindingInfo,
IsOptional = parameter.IsOptional,
ParameterBindingInfo = isFromBody ? null : GetParameterBindingInfo(parameter),
BodyParameterInfo = isFromBody ? GetBodyParameterInfo(parameter) : null
};
}
public virtual bool IsFromBody(ParameterInfo parameter)
{
// Assume for now everything is read from value providers
return false;
return parameter.GetCustomAttribute<FromBodyAttribute>() != null;
}
private ParameterBindingInfo GetParameterBindingInfo(ParameterInfo parameter)
{
return new ParameterBindingInfo(parameter.Name, parameter.ParameterType);
}
private BodyParameterInfo GetBodyParameterInfo(ParameterInfo parameter)
{
return new BodyParameterInfo(parameter.ParameterType);
}
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc.Internal
{
public static class ActionBindingContextExtensions
{
public static InputFormatterContext CreateInputFormatterContext(this ActionBindingContext actionBindingContext,
ModelStateDictionary modelState,
ParameterDescriptor parameter)
{
var metadataProvider = actionBindingContext.MetadataProvider;
var parameterType = parameter.BodyParameterInfo.ParameterType;
var modelMetadata = metadataProvider.GetMetadataForType(modelAccessor: null, modelType: parameterType);
return new InputFormatterContext(modelMetadata, modelState);
}
public static ModelBindingContext CreateModelBindingContext(this ActionBindingContext actionBindingContext,
ModelStateDictionary modelState,
ParameterDescriptor parameter)
{
var metadataProvider = actionBindingContext.MetadataProvider;
var parameterType = parameter.ParameterBindingInfo.ParameterType;
var modelMetadata = metadataProvider.GetMetadataForType(modelAccessor: null, modelType: parameterType);
return new ModelBindingContext
{
ModelName = parameter.Name,
ModelState = modelState,
ModelMetadata = modelMetadata,
ModelBinder = actionBindingContext.ModelBinder,
ValueProvider = actionBindingContext.ValueProvider,
MetadataProvider = metadataProvider,
HttpContext = actionBindingContext.ActionContext.HttpContext,
FallbackToEmptyPrefix = true
};
}
}
}

View File

@ -0,0 +1,30 @@
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
public class ActionBindingContext
{
public ActionBindingContext(ActionContext context,
IModelMetadataProvider metadataProvider,
IModelBinder modelBinder,
IValueProvider valueProvider,
IInputFormatter inputFormatter)
{
ActionContext = context;
MetadataProvider = metadataProvider;
ModelBinder = modelBinder;
ValueProvider = valueProvider;
InputFormatter = inputFormatter;
}
public ActionContext ActionContext { get; private set; }
public IModelMetadataProvider MetadataProvider { get; private set; }
public IModelBinder ModelBinder { get; private set; }
public IValueProvider ValueProvider { get; private set; }
public IInputFormatter InputFormatter { get; private set; }
}
}

View File

@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
public class DefaultActionBindingContextProvider : IActionBindingContextProvider
{
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly IEnumerable<IModelBinder> _modelBinders;
private readonly IEnumerable<IValueProviderFactory> _valueProviderFactories;
private readonly IEnumerable<IInputFormatter> _bodyReaders;
public DefaultActionBindingContextProvider(IModelMetadataProvider modelMetadataProvider,
IEnumerable<IModelBinder> modelBinders,
IEnumerable<IValueProviderFactory> valueProviderFactories,
IEnumerable<IInputFormatter> bodyReaders)
{
_modelMetadataProvider = modelMetadataProvider;
_modelBinders = modelBinders.OrderBy(binder => binder.GetType() == typeof(ComplexModelDtoModelBinder) ? 1 : 0);
_valueProviderFactories = valueProviderFactories;
_bodyReaders = bodyReaders;
}
public async Task<ActionBindingContext> GetActionBindingContextAsync(ActionContext actionContext)
{
var requestContext = new RequestContext(actionContext.HttpContext, actionContext.RouteValues);
var valueProviders = await Task.WhenAll(_valueProviderFactories.Select(factory => factory.GetValueProviderAsync(requestContext)));
valueProviders = valueProviders.Where(vp => vp != null)
.ToArray();
return new ActionBindingContext(
actionContext,
_modelMetadataProvider,
new CompositeModelBinder(_modelBinders),
new CompositeValueProvider(valueProviders),
new CompositeInputFormatter(_bodyReaders)
);
}
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace Microsoft.AspNet.Mvc
{
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class FromBodyAttribute : Attribute
{
}
}

View File

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
public interface IActionBindingContextProvider
{
Task<ActionBindingContext> GetActionBindingContextAsync(ActionContext actionContext);
}
}

View File

@ -1,13 +1,17 @@

using System;
namespace Microsoft.AspNet.Mvc
{
// This is a placeholder and is missing things that we'll need for real model binding
public class ParameterBindingInfo
{
public bool IsOptional { get; set; }
public ParameterBindingInfo(string prefix, Type parameterType)
{
Prefix = prefix;
ParameterType = parameterType;
}
public bool IsFromBody { get; set; }
public string Prefix { get; private set; }
public string Prefix { get; set; }
public Type ParameterType { get; private set; }
}
}

View File

@ -1,4 +1,4 @@

using System;
namespace Microsoft.AspNet.Mvc
{
@ -6,6 +6,10 @@ namespace Microsoft.AspNet.Mvc
{
public string Name { get; set; }
public ParameterBindingInfo Binding { get; set; }
public bool IsOptional { get; set; }
public ParameterBindingInfo ParameterBindingInfo { get; set; }
public BodyParameterInfo BodyParameterInfo { get; set; }
}
}

View File

@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Abstractions;
using Microsoft.AspNet.Mvc.Internal;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
@ -13,21 +16,24 @@ namespace Microsoft.AspNet.Mvc
private readonly IActionResultFactory _actionResultFactory;
private readonly IServiceProvider _serviceProvider;
private readonly IControllerFactory _controllerFactory;
private readonly IActionBindingContextProvider _bindingProvider;
public ReflectedActionInvoker(ActionContext actionContext,
ReflectedActionDescriptor descriptor,
IActionResultFactory actionResultFactory,
IControllerFactory controllerFactory,
IActionBindingContextProvider bindingContextProvider,
IServiceProvider serviceProvider)
{
_actionContext = actionContext;
_descriptor = descriptor;
_actionResultFactory = actionResultFactory;
_controllerFactory = controllerFactory;
_bindingProvider = bindingContextProvider;
_serviceProvider = serviceProvider;
}
public Task InvokeActionAsync()
public async Task InvokeActionAsync()
{
IActionResult actionResult = null;
@ -39,7 +45,8 @@ namespace Microsoft.AspNet.Mvc
}
else
{
Initialize(controller);
var modelState = new ModelStateDictionary();
InitializeController(controller, modelState);
var method = _descriptor.MethodInfo;
@ -49,17 +56,19 @@ namespace Microsoft.AspNet.Mvc
}
else
{
object actionReturnValue = method.Invoke(controller, null);
var parameterValues = await GetParameterValues(modelState);
object actionReturnValue = method.Invoke(controller, GetArgumentValues(parameterValues));
actionResult = _actionResultFactory.CreateActionResult(method.ReturnType, actionReturnValue, _actionContext);
}
}
// TODO: This will probably move out once we got filters
return actionResult.ExecuteResultAsync(_actionContext);
await actionResult.ExecuteResultAsync(_actionContext);
}
private void Initialize(object controller)
private void InitializeController(object controller, ModelStateDictionary modelState)
{
var controllerType = controller.GetType();
@ -72,6 +81,10 @@ namespace Microsoft.AspNet.Mvc
prop.SetValue(controller, _actionContext.HttpContext);
}
}
else if (prop.Name == "ModelState" && prop.PropertyType == typeof(ModelStateDictionary))
{
prop.SetValue(controller, modelState);
}
}
var method = controllerType.GetRuntimeMethods().FirstOrDefault(m => m.Name.Equals("Initialize", StringComparison.OrdinalIgnoreCase));
@ -86,5 +99,57 @@ namespace Microsoft.AspNet.Mvc
method.Invoke(controller, args);
}
private async Task<IDictionary<string, object>> GetParameterValues(ModelStateDictionary modelState)
{
var actionBindingContext = await _bindingProvider.GetActionBindingContextAsync(_actionContext);
var parameters = _descriptor.Parameters;
var parameterValues = new Dictionary<string, object>(parameters.Count, StringComparer.Ordinal);
for (int i = 0; i < parameters.Count; i++)
{
var parameter = parameters[i];
if (parameter.BodyParameterInfo != null)
{
var inputFormatterContext = actionBindingContext.CreateInputFormatterContext(
modelState,
parameter);
await actionBindingContext.InputFormatter.ReadAsync(inputFormatterContext);
parameterValues[parameter.Name] = inputFormatterContext.Model;
}
else
{
var modelBindingContext = actionBindingContext.CreateModelBindingContext(
modelState,
parameter);
actionBindingContext.ModelBinder.BindModel(modelBindingContext);
parameterValues[parameter.Name] = modelBindingContext.Model;
}
}
return parameterValues;
}
private object[] GetArgumentValues(IDictionary<string, object> parameterValues)
{
var parameters = _descriptor.MethodInfo.GetParameters();
var arguments = new object[parameters.Length];
for (int i = 0; i < arguments.Length; i++)
{
object value;
if (parameterValues.TryGetValue(parameters[i].Name, out value))
{
arguments[i] = value;
}
else
{
arguments[i] = parameters[i].DefaultValue;
}
}
return arguments;
}
}
}

View File

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class CompositeInputFormatter : IInputFormatter
{
private IInputFormatter[] _bodyReaders;
public CompositeInputFormatter(IEnumerable<IInputFormatter> bodyReaders)
{
_bodyReaders = bodyReaders.ToArray();
}
public async Task<bool> ReadAsync(InputFormatterContext context)
{
for(int i = 0; i < _bodyReaders.Length; i++)
{
if (await _bodyReaders[i].ReadAsync(context))
{
return true;
}
}
return false;
}
}
}

View File

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public interface IInputFormatter
{
Task<bool> ReadAsync(InputFormatterContext context);
}
}

View File

@ -0,0 +1,19 @@
using System;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class InputFormatterContext
{
public InputFormatterContext(ModelMetadata metadata, ModelStateDictionary modelState)
{
Metadata = metadata;
ModelState = modelState;
}
public ModelMetadata Metadata { get; private set; }
public ModelStateDictionary ModelState { get; private set; }
public object Model { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class JsonInputFormatter : IInputFormatter
{
public Task<bool> ReadAsync(InputFormatterContext bindingContext)
{
return Task.FromResult(false);
}
}
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
@ -14,8 +15,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
}
public CompositeValueProvider(IList<IValueProvider> list)
: base(list)
public CompositeValueProvider(IEnumerable<IValueProvider> valueProviders)
: base(valueProviders.ToList())
{
}

View File

@ -4,7 +4,6 @@ using Microsoft.AspNet.DependencyInjection.NestedProviders;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Razor;
using Microsoft.AspNet.Mvc.Razor.Compilation;
namespace Microsoft.AspNet.Mvc
{
@ -28,8 +27,6 @@ namespace Microsoft.AspNet.Mvc
Add<IActionResultHelper, ActionResultHelper>();
Add<IActionResultFactory, ActionResultFactory>();
Add<IParameterDescriptorFactory, DefaultParameterDescriptorFactory>();
Add<IValueProviderFactory, RouteValueValueProviderFactory>();
Add<IValueProviderFactory, QueryStringValueProviderFactory>();
Add<IControllerAssemblyProvider, AppDomainControllerAssemblyProvider>();
Add<IActionDiscoveryConventions, DefaultActionDiscoveryConventions>();
AddInstance<IFileSystem>(new PhysicalFileSystem(appRoot));
@ -48,11 +45,25 @@ namespace Microsoft.AspNet.Mvc
Add<IVirtualPathViewFactory, VirtualPathViewFactory>();
Add<IViewEngine, RazorViewEngine>();
Add<IModelMetadataProvider, DataAnnotationsModelMetadataProvider>();
Add<IActionBindingContextProvider, DefaultActionBindingContextProvider>();
// This is temporary until DI has some magic for it
Add<INestedProviderManager<ActionDescriptorProviderContext>, NestedProviderManager<ActionDescriptorProviderContext>>();
Add<INestedProviderManager<ActionInvokerProviderContext>, NestedProviderManager<ActionInvokerProviderContext>>();
Add<INestedProvider<ActionDescriptorProviderContext>, ReflectedActionDescriptorProvider>();
Add<INestedProvider<ActionInvokerProviderContext>, ActionInvokerProvider>();
Add<IValueProviderFactory, RouteValueValueProviderFactory>();
Add<IValueProviderFactory, QueryStringValueProviderFactory>();
Add<IModelBinder, TypeConverterModelBinder>();
Add<IModelBinder, TypeMatchModelBinder>();
Add<IModelBinder, GenericModelBinder>();
Add<IModelBinder, MutableObjectModelBinder>();
Add<IModelBinder, ComplexModelDtoModelBinder>();
Add<IInputFormatter, JsonInputFormatter>();
}
private void Add<T, TU>() where TU : T