From 9d056167e8899c8a7694549fea77c2b68e92db10 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 17 Feb 2014 17:57:42 -0800 Subject: [PATCH] Overload Resolution Skeleton --- samples/MvcSample/OverloadController.cs | 30 +++++++ .../IValueProvider.cs | 8 ++ .../MvcServices.cs | 4 + src/Microsoft.AspNet.Mvc/ActionDescriptor.cs | 4 +- .../DefaultActionSelector.cs | 79 ++++++++++++++++++- .../DefaultParameterDescriptorFactory.cs | 30 +++++++ .../IParameterDescriptorFactory.cs | 10 +++ .../ModelBinding/IValueProviderFactory.cs | 9 +++ .../QueryStringValueProviderFactory.cs | 29 +++++++ .../RouteValueValueProviderFactory.cs | 12 +++ .../ModelBinding/ValueProvider.cs | 21 +++++ .../ParameterBindingInfo.cs | 13 +++ .../ParameterDescriptor.cs | 11 +++ ...TypeMethodBasedActionDescriptorProvider.cs | 9 ++- 14 files changed, 264 insertions(+), 5 deletions(-) create mode 100644 samples/MvcSample/OverloadController.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/IValueProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc/DefaultParameterDescriptorFactory.cs create mode 100644 src/Microsoft.AspNet.Mvc/IParameterDescriptorFactory.cs create mode 100644 src/Microsoft.AspNet.Mvc/ModelBinding/IValueProviderFactory.cs create mode 100644 src/Microsoft.AspNet.Mvc/ModelBinding/QueryStringValueProviderFactory.cs create mode 100644 src/Microsoft.AspNet.Mvc/ModelBinding/RouteValueValueProviderFactory.cs create mode 100644 src/Microsoft.AspNet.Mvc/ModelBinding/ValueProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc/ParameterBindingInfo.cs create mode 100644 src/Microsoft.AspNet.Mvc/ParameterDescriptor.cs diff --git a/samples/MvcSample/OverloadController.cs b/samples/MvcSample/OverloadController.cs new file mode 100644 index 0000000000..79beee5025 --- /dev/null +++ b/samples/MvcSample/OverloadController.cs @@ -0,0 +1,30 @@ + +using Microsoft.AspNet.Mvc; + +namespace MvcSample +{ + public class OverloadController + { + private readonly IActionResultHelper _result; + + public OverloadController(IActionResultHelper result) + { + _result = result; + } + + public IActionResult Get() + { + return _result.Content("Get()"); + } + + public IActionResult Get(int id) + { + return _result.Content("Get(id)"); + } + + public IActionResult Get(int id, string name) + { + return _result.Content("Get(id, name)"); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/IValueProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/IValueProvider.cs new file mode 100644 index 0000000000..a0a7e8ef76 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/IValueProvider.cs @@ -0,0 +1,8 @@ + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public interface IValueProvider + { + bool ContainsPrefix(string key); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Startup/MvcServices.cs b/src/Microsoft.AspNet.Mvc.Startup/MvcServices.cs index b97f29e086..e07fd8eab3 100644 --- a/src/Microsoft.AspNet.Mvc.Startup/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc.Startup/MvcServices.cs @@ -1,5 +1,6 @@ using Microsoft.AspNet.DependencyInjection; using Microsoft.AspNet.FileSystems; +using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Razor; namespace Microsoft.AspNet.Mvc.Startup @@ -19,6 +20,9 @@ namespace Microsoft.AspNet.Mvc.Startup Add(); Add(); Add(); + Add(); + Add(); + Add(); Add(); Add(); Add(); diff --git a/src/Microsoft.AspNet.Mvc/ActionDescriptor.cs b/src/Microsoft.AspNet.Mvc/ActionDescriptor.cs index 53733e5cfe..c6a1b80cc1 100644 --- a/src/Microsoft.AspNet.Mvc/ActionDescriptor.cs +++ b/src/Microsoft.AspNet.Mvc/ActionDescriptor.cs @@ -14,6 +14,8 @@ namespace Microsoft.AspNet.Mvc public List MethodConstraints { get; set; } - public IEnumerable DynamicConstraints { get; set; } + public List DynamicConstraints { get; set; } + + public List Parameters { get; set; } } } diff --git a/src/Microsoft.AspNet.Mvc/DefaultActionSelector.cs b/src/Microsoft.AspNet.Mvc/DefaultActionSelector.cs index 53c5aa2609..f5f0ac7c04 100644 --- a/src/Microsoft.AspNet.Mvc/DefaultActionSelector.cs +++ b/src/Microsoft.AspNet.Mvc/DefaultActionSelector.cs @@ -1,16 +1,19 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { public class DefaultActionSelector : IActionSelector { private readonly IEnumerable _actionDescriptorProviders; + private readonly IEnumerable _valueProviderFactory; - public DefaultActionSelector(IEnumerable actionDescriptorProviders) + public DefaultActionSelector(IEnumerable actionDescriptorProviders, IEnumerable valueProviderFactories) { _actionDescriptorProviders = actionDescriptorProviders; + _valueProviderFactory = valueProviderFactories; } public ActionDescriptor Select(RequestContext context) @@ -22,7 +25,19 @@ namespace Microsoft.AspNet.Mvc var allDescriptors = _actionDescriptorProviders.SelectMany(d => d.GetDescriptors()); - return allDescriptors.SingleOrDefault(d => Match(d, context)); + var matching = allDescriptors.Where(ad => Match(ad, context)).ToList(); + if (matching.Count == 0) + { + return null; + } + else if (matching.Count == 1) + { + return matching[0]; + } + else + { + return SelectBestCandidate(context, matching); + } } public bool Match(ActionDescriptor descriptor, RequestContext context) @@ -36,5 +51,65 @@ namespace Microsoft.AspNet.Mvc (descriptor.MethodConstraints == null || descriptor.MethodConstraints.All(c => c.Accept(context))) && (descriptor.DynamicConstraints == null || descriptor.DynamicConstraints.All(c => c.Accept(context))); } + + protected virtual ActionDescriptor SelectBestCandidate(RequestContext context, List candidates) + { + var valueProviders = _valueProviderFactory.Select(vpf => vpf.CreateValueProvider(context)).ToArray(); + + var applicableCandiates = new List(); + foreach (var action in candidates) + { + var isApplicable = true; + var candidate = new ActionDescriptorCandidate() + { + Action = action, + }; + + foreach (var parameter in action.Parameters.Where(p => !p.Binding.IsFromBody)) + { + if (valueProviders.Any(vp => vp.ContainsPrefix(parameter.Binding.Prefix))) + { + candidate.FoundParameters++; + if (parameter.Binding.IsOptional) + { + candidate.FoundOptionalParameters++; + } + } + else if (!parameter.Binding.IsOptional) + { + isApplicable = false; + break; + } + } + + if (isApplicable) + { + applicableCandiates.Add(candidate); + } + } + + var mostParametersSatisfied = applicableCandiates.GroupBy(c => c.FoundParameters).OrderByDescending(g => g.Key).First(); + if (mostParametersSatisfied == null) + { + return null; + } + + var fewestOptionalParameters = mostParametersSatisfied.GroupBy(c => c.FoundOptionalParameters).OrderBy(g => g.Key).First().ToArray(); + if (fewestOptionalParameters.Length > 1) + { + throw new InvalidOperationException("The actions are ambiguious."); + } + + return fewestOptionalParameters[0].Action; + } + + private class ActionDescriptorCandidate + { + public ActionDescriptor Action { get; set; } + + public int FoundParameters { get; set; } + + public int FoundOptionalParameters { get; set; } + } } } diff --git a/src/Microsoft.AspNet.Mvc/DefaultParameterDescriptorFactory.cs b/src/Microsoft.AspNet.Mvc/DefaultParameterDescriptorFactory.cs new file mode 100644 index 0000000000..4bb1ec6b4a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/DefaultParameterDescriptorFactory.cs @@ -0,0 +1,30 @@ + +using System.Reflection; + +namespace Microsoft.AspNet.Mvc +{ + public class DefaultParameterDescriptorFactory : IParameterDescriptorFactory + { + public ParameterDescriptor GetDescriptor(ParameterInfo parameter) + { + var bindingInfo = new ParameterBindingInfo() + { + IsOptional = parameter.IsOptional, + IsFromBody = IsFromBody(parameter), + Prefix = parameter.Name, + }; + + return new ParameterDescriptor() + { + Name = parameter.Name, + Binding = bindingInfo, + }; + } + + public virtual bool IsFromBody(ParameterInfo parameter) + { + // Assume for now everything is read from value providers + return false; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc/IParameterDescriptorFactory.cs b/src/Microsoft.AspNet.Mvc/IParameterDescriptorFactory.cs new file mode 100644 index 0000000000..b6c1a39d19 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/IParameterDescriptorFactory.cs @@ -0,0 +1,10 @@ + +using System.Reflection; + +namespace Microsoft.AspNet.Mvc +{ + public interface IParameterDescriptorFactory + { + ParameterDescriptor GetDescriptor(ParameterInfo parameter); + } +} diff --git a/src/Microsoft.AspNet.Mvc/ModelBinding/IValueProviderFactory.cs b/src/Microsoft.AspNet.Mvc/ModelBinding/IValueProviderFactory.cs new file mode 100644 index 0000000000..b9e4451843 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/ModelBinding/IValueProviderFactory.cs @@ -0,0 +1,9 @@ + + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public interface IValueProviderFactory + { + IValueProvider CreateValueProvider(RequestContext context); + } +} diff --git a/src/Microsoft.AspNet.Mvc/ModelBinding/QueryStringValueProviderFactory.cs b/src/Microsoft.AspNet.Mvc/ModelBinding/QueryStringValueProviderFactory.cs new file mode 100644 index 0000000000..a9d88365d1 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/ModelBinding/QueryStringValueProviderFactory.cs @@ -0,0 +1,29 @@ + +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + // This is a temporary placeholder + public class QueryStringValueProviderFactory : IValueProviderFactory + { + public IValueProvider CreateValueProvider(RequestContext context) + { + return new QueryStringValueProvider(context.HttpContext.Request.Query); + } + + private class QueryStringValueProvider : IValueProvider + { + private readonly IReadableStringCollection _values; + + public QueryStringValueProvider(IReadableStringCollection values) + { + _values = values; + } + + public bool ContainsPrefix(string key) + { + return _values.Get(key) != null; + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc/ModelBinding/RouteValueValueProviderFactory.cs b/src/Microsoft.AspNet.Mvc/ModelBinding/RouteValueValueProviderFactory.cs new file mode 100644 index 0000000000..7b56b42553 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/ModelBinding/RouteValueValueProviderFactory.cs @@ -0,0 +1,12 @@ + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + // This is a temporary placeholder + public class RouteValueValueProviderFactory : IValueProviderFactory + { + public IValueProvider CreateValueProvider(RequestContext context) + { + return new ValueProvider(context.RouteValues); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc/ModelBinding/ValueProvider.cs b/src/Microsoft.AspNet.Mvc/ModelBinding/ValueProvider.cs new file mode 100644 index 0000000000..a7f7662e19 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/ModelBinding/ValueProvider.cs @@ -0,0 +1,21 @@ + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + // This is a temporary placeholder + public class ValueProvider : IValueProvider + { + private readonly IDictionary _values; + + public ValueProvider(IDictionary values) + { + _values = values; + } + + public bool ContainsPrefix(string key) + { + return _values.ContainsKey(key); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc/ParameterBindingInfo.cs b/src/Microsoft.AspNet.Mvc/ParameterBindingInfo.cs new file mode 100644 index 0000000000..eae59a2dba --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/ParameterBindingInfo.cs @@ -0,0 +1,13 @@ + +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 bool IsFromBody { get; set; } + + public string Prefix { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc/ParameterDescriptor.cs b/src/Microsoft.AspNet.Mvc/ParameterDescriptor.cs new file mode 100644 index 0000000000..f4d6a65778 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/ParameterDescriptor.cs @@ -0,0 +1,11 @@ + + +namespace Microsoft.AspNet.Mvc +{ + public class ParameterDescriptor + { + public string Name { get; set; } + + public ParameterBindingInfo Binding { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionDescriptorProvider.cs b/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionDescriptorProvider.cs index 92718842fa..8674d4368c 100644 --- a/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionDescriptorProvider.cs +++ b/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionDescriptorProvider.cs @@ -9,14 +9,17 @@ namespace Microsoft.AspNet.Mvc private readonly IControllerAssemblyProvider _controllerAssemblyProvider; private readonly IActionDiscoveryConventions _conventions; private readonly IControllerDescriptorFactory _controllerDescriptorFactory; + private readonly IParameterDescriptorFactory _parameterDescriptorFactory; public TypeMethodBasedActionDescriptorProvider(IControllerAssemblyProvider controllerAssemblyProvider, IActionDiscoveryConventions conventions, - IControllerDescriptorFactory controllerDescriptorFactory) + IControllerDescriptorFactory controllerDescriptorFactory, + IParameterDescriptorFactory parameterDescriptorFactory) { _controllerAssemblyProvider = controllerAssemblyProvider; _conventions = conventions; _controllerDescriptorFactory = controllerDescriptorFactory; + _parameterDescriptorFactory = parameterDescriptorFactory; } public IEnumerable GetDescriptors() @@ -45,7 +48,7 @@ namespace Microsoft.AspNet.Mvc } } - private static TypeMethodBasedActionDescriptor BuildDescriptor(ControllerDescriptor controllerDescriptor, MethodInfo methodInfo, ActionInfo actionInfo) + private TypeMethodBasedActionDescriptor BuildDescriptor(ControllerDescriptor controllerDescriptor, MethodInfo methodInfo, ActionInfo actionInfo) { var ad = new TypeMethodBasedActionDescriptor { @@ -77,6 +80,8 @@ namespace Microsoft.AspNet.Mvc ad.RouteConstraints.Add(new RouteDataActionConstraint("action", RouteKeyHandling.DenyKey)); } + ad.Parameters = methodInfo.GetParameters().Select(p => _parameterDescriptorFactory.GetDescriptor(p)).ToList(); + return ad; }