From 941a12daea46b865621c2db3306c24ebfd8ce136 Mon Sep 17 00:00:00 2001 From: Yishai Galatzer Date: Sun, 16 Feb 2014 17:42:48 -0800 Subject: [PATCH] Action selection based on action descriptors --- samples/MvcSample/SimpleRest.cs | 12 ++ samples/MvcSample/Startup.cs | 4 + .../ViewEngine/RazorViewEngine.cs | 12 +- .../View/IViewEngine.cs | 3 +- .../MvcServices.cs | 12 +- src/Microsoft.AspNet.Mvc/ActionContext.cs | 21 ++++ src/Microsoft.AspNet.Mvc/ActionConvention.cs | 9 ++ src/Microsoft.AspNet.Mvc/ActionDescriptor.cs | 19 +++ .../ActionInvokerFactory.cs | 15 +-- .../ActionInvokerProvider.cs | 7 +- .../ActionResultFactory.cs | 12 +- .../ActionResults/ContentResult.cs | 9 +- .../ActionResults/EmptyResult.cs | 2 +- .../ActionResults/HttpStatusCodeResult.cs | 2 +- .../ActionResults/JsonResult.cs | 2 +- .../ActionResults/NoContentResult.cs | 7 +- .../ActionResults/ViewResult.cs | 6 +- .../AppDomainControllerAssemblyProvider.cs | 1 + .../ControllerDescriptor.cs | 27 ++--- .../DefaultActionDiscoveryConventions.cs | 94 +++++++++++++++ .../DefaultActionSelector.cs | 40 +++++++ .../DefaultControllerDescriptorFactory.cs | 12 ++ .../DefaultControllerDescriptorProvider.cs | 91 --------------- .../DefaultControllerFactory.cs | 57 ++++----- .../HttpMethodConstraint.cs | 60 ++++++++++ src/Microsoft.AspNet.Mvc/IActionConstraint.cs | 7 ++ .../IActionDescriptorProvider.cs | 4 +- .../IActionDiscoveryConventions.cs | 12 ++ .../IActionInvokerFactory.cs | 7 +- .../IActionInvokerProvider.cs | 5 +- src/Microsoft.AspNet.Mvc/IActionResult.cs | 2 +- .../IActionResultFactory.cs | 2 +- src/Microsoft.AspNet.Mvc/IActionSelector.cs | 9 ++ .../IControllerDescriptorFactory.cs | 9 ++ .../IControllerDescriptorProvider.cs | 9 -- .../IControllerFactory.cs | 2 +- src/Microsoft.AspNet.Mvc/RouteContext.cs | 6 - .../RouteDataActionConstraint.cs | 110 ++++++++++++++++++ src/Microsoft.AspNet.Mvc/RouteKeyHandling.cs | 26 +++++ .../Routing/RouteEndpoint.cs | 36 +++++- .../TypeMethodBasedActionDescriptor.cs | 34 +++++- ...TypeMethodBasedActionDescriptorProvider.cs | 103 ++++++++++++++-- .../TypeMethodBasedActionInvoker.cs | 24 ++-- 43 files changed, 687 insertions(+), 256 deletions(-) create mode 100644 samples/MvcSample/SimpleRest.cs create mode 100644 src/Microsoft.AspNet.Mvc/ActionContext.cs create mode 100644 src/Microsoft.AspNet.Mvc/ActionConvention.cs create mode 100644 src/Microsoft.AspNet.Mvc/ActionDescriptor.cs create mode 100644 src/Microsoft.AspNet.Mvc/DefaultActionDiscoveryConventions.cs create mode 100644 src/Microsoft.AspNet.Mvc/DefaultActionSelector.cs create mode 100644 src/Microsoft.AspNet.Mvc/DefaultControllerDescriptorFactory.cs delete mode 100644 src/Microsoft.AspNet.Mvc/DefaultControllerDescriptorProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc/HttpMethodConstraint.cs create mode 100644 src/Microsoft.AspNet.Mvc/IActionConstraint.cs create mode 100644 src/Microsoft.AspNet.Mvc/IActionDiscoveryConventions.cs create mode 100644 src/Microsoft.AspNet.Mvc/IActionSelector.cs create mode 100644 src/Microsoft.AspNet.Mvc/IControllerDescriptorFactory.cs delete mode 100644 src/Microsoft.AspNet.Mvc/IControllerDescriptorProvider.cs delete mode 100644 src/Microsoft.AspNet.Mvc/RouteContext.cs create mode 100644 src/Microsoft.AspNet.Mvc/RouteDataActionConstraint.cs create mode 100644 src/Microsoft.AspNet.Mvc/RouteKeyHandling.cs diff --git a/samples/MvcSample/SimpleRest.cs b/samples/MvcSample/SimpleRest.cs new file mode 100644 index 0000000000..c60502144b --- /dev/null +++ b/samples/MvcSample/SimpleRest.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNet.Mvc; + +namespace MvcSample +{ + public class SimpleRest : Controller + { + public string Get() + { + return "Get method"; + } + } +} diff --git a/samples/MvcSample/Startup.cs b/samples/MvcSample/Startup.cs index 20309f6658..43c071ff1c 100644 --- a/samples/MvcSample/Startup.cs +++ b/samples/MvcSample/Startup.cs @@ -38,6 +38,10 @@ namespace MvcSample endpoint, "{controller}/{action}", new Dictionary(StringComparer.OrdinalIgnoreCase) { { "controller", "Home" }, { "action", "Index" } })); + router.Add(new TemplateRoute( + endpoint, + "{controller}", + new Dictionary(StringComparer.OrdinalIgnoreCase) { { "controller", "Home" } })); } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/RazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/RazorViewEngine.cs index e83e9bf0a5..6af2bff458 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/RazorViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/RazorViewEngine.cs @@ -7,7 +7,7 @@ namespace Microsoft.AspNet.Mvc.Razor { public class RazorViewEngine : IViewEngine { - private static readonly string[] _viewLocationFormats = new[] + private static readonly string[] _viewLocationFormats = { "/Views/{1}/{0}.cshtml", "/Views/Shared/{0}.cshtml", @@ -28,11 +28,13 @@ namespace Microsoft.AspNet.Mvc.Razor get { return _viewLocationFormats; } } - public async Task FindView(RequestContext requestContext, string viewName) + public async Task FindView(object context, string viewName) { + var actionContext = (ActionContext)context; + // TODO: We plan to change this on the next CR, so we don't have a strong depenedency directly on the specific // type of the action descriptor - var actionDescriptor = _actionDescriptorProvider.CreateDescriptor(requestContext) as TypeMethodBasedActionDescriptor; + ActionDescriptor actionDescriptor = actionContext.ActionDescriptor; if (actionDescriptor == null) { @@ -41,7 +43,7 @@ namespace Microsoft.AspNet.Mvc.Razor if (String.IsNullOrEmpty(viewName)) { - viewName = actionDescriptor.ActionName; + viewName = actionDescriptor.Name; } if (String.IsNullOrEmpty(viewName)) @@ -59,7 +61,7 @@ namespace Microsoft.AspNet.Mvc.Razor } else { - string controllerName = actionDescriptor.ControllerName; + string controllerName = actionDescriptor.Path; var searchedLocations = new List(_viewLocationFormats.Length); for (int i = 0; i < _viewLocationFormats.Length; i++) { diff --git a/src/Microsoft.AspNet.Mvc.Rendering/View/IViewEngine.cs b/src/Microsoft.AspNet.Mvc.Rendering/View/IViewEngine.cs index a28e4fb71d..69746f1be5 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/View/IViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/View/IViewEngine.cs @@ -4,6 +4,7 @@ namespace Microsoft.AspNet.Mvc { public interface IViewEngine { - Task FindView(RequestContext requestContext, string viewName); + // TODO: Relayer to allow this to be ActionContext. We probably need the common MVC assembly + Task FindView(object actionContext, string viewName); } } diff --git a/src/Microsoft.AspNet.Mvc.Startup/MvcServices.cs b/src/Microsoft.AspNet.Mvc.Startup/MvcServices.cs index 32f2b6358e..b97f29e086 100644 --- a/src/Microsoft.AspNet.Mvc.Startup/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc.Startup/MvcServices.cs @@ -1,8 +1,6 @@ using Microsoft.AspNet.DependencyInjection; using Microsoft.AspNet.FileSystems; using Microsoft.AspNet.Mvc.Razor; -using Microsoft.AspNet.Mvc.Routing; -using Microsoft.AspNet.Routing; namespace Microsoft.AspNet.Mvc.Startup { @@ -15,18 +13,16 @@ namespace Microsoft.AspNet.Mvc.Startup Services = new ServiceProvider(); Add(); + Add(); + Add(); Add(); Add(); Add(); Add(); Add(); + Add(); + Add(); - // need singleton support here. - // need a design for immutable caches at startup - var provider = new DefaultControllerDescriptorProvider(new AppDomainControllerAssemblyProvider()); - provider.FinalizeSetup(); - - AddInstance(provider); AddInstance(new PhysicalFileSystem(appRoot)); AddInstance(new MvcRazorHost(typeof(RazorView).FullName)); diff --git a/src/Microsoft.AspNet.Mvc/ActionContext.cs b/src/Microsoft.AspNet.Mvc/ActionContext.cs new file mode 100644 index 0000000000..1f9979c941 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/ActionContext.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.Mvc +{ + public class ActionContext + { + public ActionContext(HttpContext httpContext, IDictionary routeValues, ActionDescriptor actionDescriptor) + { + HttpContext = httpContext; + RouteValues = routeValues; + ActionDescriptor = actionDescriptor; + } + + public HttpContext HttpContext { get; private set; } + + public IDictionary RouteValues { get; private set; } + + public ActionDescriptor ActionDescriptor { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc/ActionConvention.cs b/src/Microsoft.AspNet.Mvc/ActionConvention.cs new file mode 100644 index 0000000000..859f973e5d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/ActionConvention.cs @@ -0,0 +1,9 @@ +namespace Microsoft.AspNet.Mvc +{ + public class ActionInfo + { + public string ActionName { get; set; } + public string[] HttpMethods { get; set; } + public bool RequireActionNameMatch { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc/ActionDescriptor.cs b/src/Microsoft.AspNet.Mvc/ActionDescriptor.cs new file mode 100644 index 0000000000..53733e5cfe --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/ActionDescriptor.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.AspNet.Mvc +{ + [DebuggerDisplay("{Path}:{Name}")] + public class ActionDescriptor + { + public virtual string Path { get; set; } + + public virtual string Name { get; set; } + + public List RouteConstraints { get; set; } + + public List MethodConstraints { get; set; } + + public IEnumerable DynamicConstraints { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc/ActionInvokerFactory.cs b/src/Microsoft.AspNet.Mvc/ActionInvokerFactory.cs index 82cf43525c..4bd7f8e5c1 100644 --- a/src/Microsoft.AspNet.Mvc/ActionInvokerFactory.cs +++ b/src/Microsoft.AspNet.Mvc/ActionInvokerFactory.cs @@ -1,7 +1,4 @@ - -using Microsoft.AspNet.Mvc.Routing; - -namespace Microsoft.AspNet.Mvc +namespace Microsoft.AspNet.Mvc { public class ActionInvokerFactory : IActionInvokerFactory { @@ -18,15 +15,9 @@ namespace Microsoft.AspNet.Mvc _actionInvokerProvider = actionInvokerProvider; } - public IActionInvoker CreateInvoker(RequestContext requestContext) + public IActionInvoker CreateInvoker(ActionContext actionContext) { - ActionDescriptor routeContext = _routeContextProvider.CreateDescriptor(requestContext); - if (routeContext == null) - { - return null; - } - - return _actionInvokerProvider.GetInvoker(requestContext, routeContext); + return _actionInvokerProvider.GetInvoker(actionContext); } } } diff --git a/src/Microsoft.AspNet.Mvc/ActionInvokerProvider.cs b/src/Microsoft.AspNet.Mvc/ActionInvokerProvider.cs index 4607a6466a..e5bc20e074 100644 --- a/src/Microsoft.AspNet.Mvc/ActionInvokerProvider.cs +++ b/src/Microsoft.AspNet.Mvc/ActionInvokerProvider.cs @@ -1,5 +1,4 @@ using System; -using Microsoft.AspNet.DependencyInjection; namespace Microsoft.AspNet.Mvc { @@ -18,14 +17,14 @@ namespace Microsoft.AspNet.Mvc _serviceProvider = serviceProvider; } - public IActionInvoker GetInvoker(RequestContext requestContext, ActionDescriptor actionDescriptor) + public IActionInvoker GetInvoker(ActionContext actionContext) { - var ad = actionDescriptor as TypeMethodBasedActionDescriptor; + var ad = actionContext.ActionDescriptor as TypeMethodBasedActionDescriptor; if (ad != null) { return new TypeMethodBasedActionInvoker( - requestContext, + actionContext, ad, _actionResultFactory, _controllerFactory, diff --git a/src/Microsoft.AspNet.Mvc/ActionResultFactory.cs b/src/Microsoft.AspNet.Mvc/ActionResultFactory.cs index 2f42af5bd9..b298ce65d1 100644 --- a/src/Microsoft.AspNet.Mvc/ActionResultFactory.cs +++ b/src/Microsoft.AspNet.Mvc/ActionResultFactory.cs @@ -11,10 +11,10 @@ namespace Microsoft.AspNet.Mvc _result = result; } - public IActionResult CreateActionResult(Type declaredReturnType, object actionReturnValue, RequestContext requestContext) + public IActionResult CreateActionResult(Type declaredReturnType, object actionReturnValue, ActionContext actionContext) { // optimize common path - IActionResult actionResult = actionReturnValue as IActionResult; + var actionResult = actionReturnValue as IActionResult; if (actionResult != null) { @@ -38,17 +38,19 @@ namespace Microsoft.AspNet.Mvc throw new InvalidOperationException("HttpActionDescriptor_NoConverterForGenericParamterTypeExists"); } - if (declaredReturnType.IsAssignableFrom(typeof(void))) + if (declaredReturnType.IsAssignableFrom(typeof(void)) || actionReturnValue == null) { return new NoContentResult(); } - if (actionReturnValue is string) + var actionReturnString = actionReturnValue as string; + + if (actionReturnString != null) { return new ContentResult { ContentType = "text/plain", - Content = (string)actionReturnValue, + Content = actionReturnString, }; } diff --git a/src/Microsoft.AspNet.Mvc/ActionResults/ContentResult.cs b/src/Microsoft.AspNet.Mvc/ActionResults/ContentResult.cs index 6b6c987752..dda593864f 100644 --- a/src/Microsoft.AspNet.Mvc/ActionResults/ContentResult.cs +++ b/src/Microsoft.AspNet.Mvc/ActionResults/ContentResult.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc public string ContentType { get; set; } - public async Task ExecuteResultAsync(RequestContext context) + public async Task ExecuteResultAsync(ActionContext context) { if (context == null) { @@ -26,12 +26,7 @@ namespace Microsoft.AspNet.Mvc { response.ContentType = ContentType; } - - //if (ContentEncoding != null) - //{ - // response.ContentEncoding = ContentEncoding; - //} - + if (Content != null) { await response.WriteAsync(Content); diff --git a/src/Microsoft.AspNet.Mvc/ActionResults/EmptyResult.cs b/src/Microsoft.AspNet.Mvc/ActionResults/EmptyResult.cs index 94b5e4d91c..71d493c122 100644 --- a/src/Microsoft.AspNet.Mvc/ActionResults/EmptyResult.cs +++ b/src/Microsoft.AspNet.Mvc/ActionResults/EmptyResult.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Mvc get { return _singleton; } } - public async Task ExecuteResultAsync(RequestContext context) + public async Task ExecuteResultAsync(ActionContext context) { } } diff --git a/src/Microsoft.AspNet.Mvc/ActionResults/HttpStatusCodeResult.cs b/src/Microsoft.AspNet.Mvc/ActionResults/HttpStatusCodeResult.cs index bc60749739..5e2903a3f3 100644 --- a/src/Microsoft.AspNet.Mvc/ActionResults/HttpStatusCodeResult.cs +++ b/src/Microsoft.AspNet.Mvc/ActionResults/HttpStatusCodeResult.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Mvc _statusCode = statusCode; } - public async Task ExecuteResultAsync(RequestContext context) + public async Task ExecuteResultAsync(ActionContext context) { context.HttpContext.Response.StatusCode = _statusCode; } diff --git a/src/Microsoft.AspNet.Mvc/ActionResults/JsonResult.cs b/src/Microsoft.AspNet.Mvc/ActionResults/JsonResult.cs index 35fb191b2f..2b850b0f8e 100644 --- a/src/Microsoft.AspNet.Mvc/ActionResults/JsonResult.cs +++ b/src/Microsoft.AspNet.Mvc/ActionResults/JsonResult.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNet.Mvc } } - public async Task ExecuteResultAsync(RequestContext context) + public async Task ExecuteResultAsync(ActionContext context) { HttpResponse response = context.HttpContext.Response; diff --git a/src/Microsoft.AspNet.Mvc/ActionResults/NoContentResult.cs b/src/Microsoft.AspNet.Mvc/ActionResults/NoContentResult.cs index 39318e0f68..e97ee8a1d5 100644 --- a/src/Microsoft.AspNet.Mvc/ActionResults/NoContentResult.cs +++ b/src/Microsoft.AspNet.Mvc/ActionResults/NoContentResult.cs @@ -1,14 +1,13 @@ using System; -using System.Text; -using System.Threading.Tasks; using System.Net; +using System.Threading.Tasks; using Microsoft.AspNet.Abstractions; namespace Microsoft.AspNet.Mvc { public class NoContentResult : IActionResult { - public async Task ExecuteResultAsync(RequestContext context) + public async Task ExecuteResultAsync(ActionContext context) { if (context == null) { @@ -24,8 +23,6 @@ namespace Microsoft.AspNet.Mvc #endif await Task.FromResult(false); - - return; } } } diff --git a/src/Microsoft.AspNet.Mvc/ActionResults/ViewResult.cs b/src/Microsoft.AspNet.Mvc/ActionResults/ViewResult.cs index 58a6a1f962..c454eec5c7 100644 --- a/src/Microsoft.AspNet.Mvc/ActionResults/ViewResult.cs +++ b/src/Microsoft.AspNet.Mvc/ActionResults/ViewResult.cs @@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc public ViewData ViewData { get; set; } - public async Task ExecuteResultAsync(RequestContext context) + public async Task ExecuteResultAsync(ActionContext context) { if (context == null) { @@ -44,9 +44,9 @@ namespace Microsoft.AspNet.Mvc } } - private async Task FindView(RequestContext requestContext, string viewName) + private async Task FindView(ActionContext actionContext, string viewName) { - ViewEngineResult result = await _viewEngine.FindView(requestContext, viewName); + ViewEngineResult result = await _viewEngine.FindView(actionContext, viewName); if (!result.Success) { string locationsText = String.Join(Environment.NewLine, result.SearchedLocations); diff --git a/src/Microsoft.AspNet.Mvc/AppDomainControllerAssemblyProvider.cs b/src/Microsoft.AspNet.Mvc/AppDomainControllerAssemblyProvider.cs index 7875318d0f..a676e0f743 100644 --- a/src/Microsoft.AspNet.Mvc/AppDomainControllerAssemblyProvider.cs +++ b/src/Microsoft.AspNet.Mvc/AppDomainControllerAssemblyProvider.cs @@ -23,6 +23,7 @@ namespace Microsoft.AspNet.Mvc // consider mechanisms to filter assemblies upfront, so scanning cost is minimized and startup improved. // 1 - Does assembly reference the WebFx assembly (directly or indirectly). - Down side, object only controller not supported. // 2 - Remove well known assemblies (maintenance and composability cost) + return true; } } diff --git a/src/Microsoft.AspNet.Mvc/ControllerDescriptor.cs b/src/Microsoft.AspNet.Mvc/ControllerDescriptor.cs index aaaa208ac1..6f87876475 100644 --- a/src/Microsoft.AspNet.Mvc/ControllerDescriptor.cs +++ b/src/Microsoft.AspNet.Mvc/ControllerDescriptor.cs @@ -5,31 +5,22 @@ namespace Microsoft.AspNet.Mvc { public class ControllerDescriptor { - public ControllerDescriptor(Type controllerType, Assembly assembly) + public ControllerDescriptor(TypeInfo controllerTypeInfo) { - if (controllerType == null) + if (controllerTypeInfo == null) { - throw new ArgumentNullException("controllerType"); + throw new ArgumentNullException("controllerTypeInfo"); } - if (assembly == null) - { - throw new ArgumentNullException("assembly"); - } + ControllerTypeInfo = controllerTypeInfo; - ControllerType = controllerType; - Assembly = assembly; - - ControllerName = controllerType.Name; - AssemblyName = assembly.GetName().Name; + Name = controllerTypeInfo.Name.EndsWith("Controller", StringComparison.Ordinal) + ? controllerTypeInfo.Name.Substring(0, controllerTypeInfo.Name.Length - "Controller".Length) + : controllerTypeInfo.Name; } - public string ControllerName { get; private set; } + public string Name { get; private set; } - public string AssemblyName { get; private set; } - - public Type ControllerType { get; private set; } - - public Assembly Assembly { get; private set; } + public TypeInfo ControllerTypeInfo { get; private set; } } } diff --git a/src/Microsoft.AspNet.Mvc/DefaultActionDiscoveryConventions.cs b/src/Microsoft.AspNet.Mvc/DefaultActionDiscoveryConventions.cs new file mode 100644 index 0000000000..2c64953c4a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/DefaultActionDiscoveryConventions.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.AspNet.Mvc +{ + public class DefaultActionDiscoveryConventions : IActionDiscoveryConventions + { + private static readonly string[] _supportedHttpMethodsByConvention = + { + "GET", + "POST", + "PUT", + "DELETE", + "HEAD", + "OPTIONS", + "PATCH", + }; + + public virtual bool IsController(TypeInfo typeInfo) + { + if (typeInfo == null) + { + throw new ArgumentNullException("typeInfo"); + } + + if (!typeInfo.IsClass || + typeInfo.IsAbstract || + typeInfo.ContainsGenericParameters) + { + return false; + } + + if (typeInfo.Name.Equals("Controller", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) || + typeof(Controller).GetTypeInfo().IsAssignableFrom(typeInfo); + + } + + public IEnumerable GetActions(MethodInfo methodInfo) + { + if (methodInfo == null) + { + throw new ArgumentNullException("methodInfo"); + } + + if (!IsValidMethod(methodInfo)) + { + return null; + } + + for (var i = 0; i < _supportedHttpMethodsByConvention.Length; i++) + { + if (methodInfo.Name.StartsWith(_supportedHttpMethodsByConvention[i], StringComparison.OrdinalIgnoreCase)) + { + return new [] { + new ActionInfo() + { + HttpMethods = new[] { _supportedHttpMethodsByConvention[i] }, + ActionName = methodInfo.Name, + RequireActionNameMatch = false, + } + }; + } + } + + // TODO: Consider mapping Index here to both Get and also to Index + + return new[] + { + new ActionInfo() + { + ActionName = methodInfo.Name, + RequireActionNameMatch = true, + } + }; + } + + public virtual bool IsValidMethod(MethodInfo method) + { + return + method.IsPublic && + !method.IsAbstract && + !method.IsConstructor && + !method.IsGenericMethod && + !method.IsSpecialName; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc/DefaultActionSelector.cs b/src/Microsoft.AspNet.Mvc/DefaultActionSelector.cs new file mode 100644 index 0000000000..53c5aa2609 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/DefaultActionSelector.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.Mvc +{ + public class DefaultActionSelector : IActionSelector + { + private readonly IEnumerable _actionDescriptorProviders; + + public DefaultActionSelector(IEnumerable actionDescriptorProviders) + { + _actionDescriptorProviders = actionDescriptorProviders; + } + + public ActionDescriptor Select(RequestContext context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + var allDescriptors = _actionDescriptorProviders.SelectMany(d => d.GetDescriptors()); + + return allDescriptors.SingleOrDefault(d => Match(d, context)); + } + + public bool Match(ActionDescriptor descriptor, RequestContext context) + { + if (descriptor == null) + { + throw new ArgumentNullException("descriptor"); + } + + return (descriptor.RouteConstraints == null || descriptor.RouteConstraints.All(c => c.Accept(context))) && + (descriptor.MethodConstraints == null || descriptor.MethodConstraints.All(c => c.Accept(context))) && + (descriptor.DynamicConstraints == null || descriptor.DynamicConstraints.All(c => c.Accept(context))); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc/DefaultControllerDescriptorFactory.cs b/src/Microsoft.AspNet.Mvc/DefaultControllerDescriptorFactory.cs new file mode 100644 index 0000000000..8ac9995150 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/DefaultControllerDescriptorFactory.cs @@ -0,0 +1,12 @@ +using System.Reflection; + +namespace Microsoft.AspNet.Mvc +{ + public class DefaultControllerDescriptorFactory : IControllerDescriptorFactory + { + public ControllerDescriptor CreateControllerDescriptor(TypeInfo typeInfo) + { + return new ControllerDescriptor(typeInfo); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc/DefaultControllerDescriptorProvider.cs b/src/Microsoft.AspNet.Mvc/DefaultControllerDescriptorProvider.cs deleted file mode 100644 index 9132e1c2a7..0000000000 --- a/src/Microsoft.AspNet.Mvc/DefaultControllerDescriptorProvider.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Microsoft.AspNet.Mvc -{ - public class DefaultControllerDescriptorProvider : IControllerDescriptorProvider - { - private readonly IControllerAssemblyProvider _controllerAssemblyProvider; - - public IReadOnlyDictionary> Controllers { get; protected set; } - - public void FinalizeSetup() - { - Controllers = ScanAppDomain(); - } - - public DefaultControllerDescriptorProvider(IControllerAssemblyProvider controllerAssemblyProvider) - { - if (controllerAssemblyProvider == null) - { - throw new ArgumentNullException("controllerAssemblyProvider"); - } - - _controllerAssemblyProvider = controllerAssemblyProvider; - } - - public IEnumerable GetControllers(string controllerName) - { - if (!controllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)) - { - controllerName += "Controller"; - } - - if (Controllers == null) - { - throw new InvalidOperationException("Finalizing the setup must happen prior to accessing controllers"); - } - - IEnumerable descriptors = null; - - if (Controllers.TryGetValue(controllerName, out descriptors)) - { - return descriptors; - } - - return Enumerable.Empty(); - } - - public Dictionary> ScanAppDomain() - { - var dictionary = new Dictionary>(StringComparer.Ordinal); - - foreach (var assembly in _controllerAssemblyProvider.Assemblies) - { - foreach (var type in assembly.DefinedTypes.Where(IsController).Select(info => info.AsType())) - { - var descriptor = new ControllerDescriptor(type, assembly); - - IEnumerable controllerDescriptors; - if (!dictionary.TryGetValue(type.Name, out controllerDescriptors)) - { - controllerDescriptors = new List(); - dictionary.Add(descriptor.ControllerName, controllerDescriptors); - } - - ((List)controllerDescriptors).Add(descriptor); - } - } - - return dictionary; - } - - public virtual bool IsController(TypeInfo typeInfo) - { - if (typeInfo == null) - { - throw new ArgumentNullException("typeInfo"); - } - - bool validController = typeInfo.IsClass && - !typeInfo.IsAbstract && - !typeInfo.ContainsGenericParameters; - - validController = validController && typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase); - - return validController; - } - } -} diff --git a/src/Microsoft.AspNet.Mvc/DefaultControllerFactory.cs b/src/Microsoft.AspNet.Mvc/DefaultControllerFactory.cs index bd983f6ae9..e18c8fa1b5 100644 --- a/src/Microsoft.AspNet.Mvc/DefaultControllerFactory.cs +++ b/src/Microsoft.AspNet.Mvc/DefaultControllerFactory.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Reflection; using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.DependencyInjection; @@ -9,49 +8,37 @@ namespace Microsoft.AspNet.Mvc public class DefaultControllerFactory : IControllerFactory { private readonly IServiceProvider _serviceProvider; - private readonly IControllerDescriptorProvider _controllerDescriptorProvider; - public DefaultControllerFactory(IServiceProvider serviceProvider, IControllerDescriptorProvider controllerDescriptorProvider) + public DefaultControllerFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; - _controllerDescriptorProvider = controllerDescriptorProvider; } - public object CreateController(HttpContext context, string controllerName) - { - var controllers = _controllerDescriptorProvider.GetControllers(controllerName); + public object CreateController(HttpContext context, ActionDescriptor actionDescriptor) + { + var typedAd = actionDescriptor as TypeMethodBasedActionDescriptor; - if (controllers != null) + if (typedAd == null) { - try + return null; + } + + try + { + var controller = ActivatorUtilities.CreateInstance(_serviceProvider, typedAd.ControllerDescriptor.ControllerTypeInfo.AsType()); + + // TODO: How do we feed the controller with context (need DI improvements) + var contextProperty = controller.GetType().GetRuntimeProperty("Context"); + + if (contextProperty != null) { - var descriptor = controllers.SingleOrDefault(); - - if (descriptor != null) - { - try - { - var controller = ActivatorUtilities.CreateInstance(_serviceProvider, descriptor.ControllerType); - - // TODO: How do we feed the controller with context (need DI improvements) - var contextProperty = descriptor.ControllerType.GetRuntimeProperty("Context"); - - if (contextProperty != null) - { - contextProperty.SetMethod.Invoke(controller, new object[] { context }); - } - - return controller; - } - catch (ReflectionTypeLoadException) - { - } - } - } - catch (InvalidOperationException) - { - throw new InvalidOperationException("Ambiguity: Duplicate controllers match the controller name"); + contextProperty.SetMethod.Invoke(controller, new object[] { context }); } + + return controller; + } + catch (ReflectionTypeLoadException) + { } return null; diff --git a/src/Microsoft.AspNet.Mvc/HttpMethodConstraint.cs b/src/Microsoft.AspNet.Mvc/HttpMethodConstraint.cs new file mode 100644 index 0000000000..486b8f7e84 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/HttpMethodConstraint.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Microsoft.AspNet.Mvc +{ + public class HttpMethodConstraint : IActionConstraint + { + private readonly IReadOnlyList _methods; + + // Empty collection means any method will be accepted. + public HttpMethodConstraint(IEnumerable httpMethods) + { + if (httpMethods == null) + { + throw new ArgumentNullException("httpMethods"); + } + + var methods = new List(); + + foreach (var method in httpMethods) + { + if (string.IsNullOrEmpty(method)) + { + throw new ArgumentException("httpMethod cannot be null or empty"); + } + + methods.Add(method); + } + + _methods = new ReadOnlyCollection(methods); + } + + public IEnumerable HttpMethods + { + get + { + return _methods; + } + } + + public bool Accept(RequestContext context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + if (_methods.Count == 0) + { + return true; + } + + var request = context.HttpContext.Request; + + return (HttpMethods.Any(m => m.Equals(request.Method, StringComparison.Ordinal))); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc/IActionConstraint.cs b/src/Microsoft.AspNet.Mvc/IActionConstraint.cs new file mode 100644 index 0000000000..8f992b0406 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/IActionConstraint.cs @@ -0,0 +1,7 @@ +namespace Microsoft.AspNet.Mvc +{ + public interface IActionConstraint + { + bool Accept(RequestContext context); + } +} diff --git a/src/Microsoft.AspNet.Mvc/IActionDescriptorProvider.cs b/src/Microsoft.AspNet.Mvc/IActionDescriptorProvider.cs index 0eab785a57..674eba433d 100644 --- a/src/Microsoft.AspNet.Mvc/IActionDescriptorProvider.cs +++ b/src/Microsoft.AspNet.Mvc/IActionDescriptorProvider.cs @@ -1,9 +1,9 @@ -using Microsoft.AspNet.Mvc.Routing; +using System.Collections.Generic; namespace Microsoft.AspNet.Mvc { public interface IActionDescriptorProvider { - ActionDescriptor CreateDescriptor(RequestContext requestContext); + IEnumerable GetDescriptors(); } } diff --git a/src/Microsoft.AspNet.Mvc/IActionDiscoveryConventions.cs b/src/Microsoft.AspNet.Mvc/IActionDiscoveryConventions.cs new file mode 100644 index 0000000000..9a5c0bf6b3 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/IActionDiscoveryConventions.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.AspNet.Mvc +{ + public interface IActionDiscoveryConventions + { + bool IsController(TypeInfo typeInfo); + + IEnumerable GetActions(MethodInfo methodInfo); + } +} diff --git a/src/Microsoft.AspNet.Mvc/IActionInvokerFactory.cs b/src/Microsoft.AspNet.Mvc/IActionInvokerFactory.cs index 53a9d6385e..5d6a711c20 100644 --- a/src/Microsoft.AspNet.Mvc/IActionInvokerFactory.cs +++ b/src/Microsoft.AspNet.Mvc/IActionInvokerFactory.cs @@ -1,10 +1,7 @@ - -using Microsoft.AspNet.Mvc.Routing; - -namespace Microsoft.AspNet.Mvc +namespace Microsoft.AspNet.Mvc { public interface IActionInvokerFactory { - IActionInvoker CreateInvoker(RequestContext requestContext); + IActionInvoker CreateInvoker(ActionContext actionContext); } } diff --git a/src/Microsoft.AspNet.Mvc/IActionInvokerProvider.cs b/src/Microsoft.AspNet.Mvc/IActionInvokerProvider.cs index 29ce3fb36d..47aed560f0 100644 --- a/src/Microsoft.AspNet.Mvc/IActionInvokerProvider.cs +++ b/src/Microsoft.AspNet.Mvc/IActionInvokerProvider.cs @@ -1,8 +1,7 @@ - -namespace Microsoft.AspNet.Mvc +namespace Microsoft.AspNet.Mvc { public interface IActionInvokerProvider { - IActionInvoker GetInvoker(RequestContext requestContext, ActionDescriptor routeContext); + IActionInvoker GetInvoker(ActionContext actionContext); } } diff --git a/src/Microsoft.AspNet.Mvc/IActionResult.cs b/src/Microsoft.AspNet.Mvc/IActionResult.cs index 4df054d7f3..36071faea0 100644 --- a/src/Microsoft.AspNet.Mvc/IActionResult.cs +++ b/src/Microsoft.AspNet.Mvc/IActionResult.cs @@ -4,6 +4,6 @@ namespace Microsoft.AspNet.Mvc { public interface IActionResult { - Task ExecuteResultAsync(RequestContext context); + Task ExecuteResultAsync(ActionContext context); } } diff --git a/src/Microsoft.AspNet.Mvc/IActionResultFactory.cs b/src/Microsoft.AspNet.Mvc/IActionResultFactory.cs index 86f8c14ba1..5472d14cde 100644 --- a/src/Microsoft.AspNet.Mvc/IActionResultFactory.cs +++ b/src/Microsoft.AspNet.Mvc/IActionResultFactory.cs @@ -4,6 +4,6 @@ namespace Microsoft.AspNet.Mvc { public interface IActionResultFactory { - IActionResult CreateActionResult(Type declaredReturnType, object actionReturnValue, RequestContext requestContext); + IActionResult CreateActionResult(Type declaredReturnType, object actionReturnValue, ActionContext actionContext); } } diff --git a/src/Microsoft.AspNet.Mvc/IActionSelector.cs b/src/Microsoft.AspNet.Mvc/IActionSelector.cs new file mode 100644 index 0000000000..7ffaf9b77a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/IActionSelector.cs @@ -0,0 +1,9 @@ +namespace Microsoft.AspNet.Mvc +{ + public interface IActionSelector + { + ActionDescriptor Select(RequestContext context); + + bool Match(ActionDescriptor descriptor, RequestContext context); + } +} diff --git a/src/Microsoft.AspNet.Mvc/IControllerDescriptorFactory.cs b/src/Microsoft.AspNet.Mvc/IControllerDescriptorFactory.cs new file mode 100644 index 0000000000..13b1922a37 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/IControllerDescriptorFactory.cs @@ -0,0 +1,9 @@ +using System.Reflection; + +namespace Microsoft.AspNet.Mvc +{ + public interface IControllerDescriptorFactory + { + ControllerDescriptor CreateControllerDescriptor(TypeInfo type); + } +} diff --git a/src/Microsoft.AspNet.Mvc/IControllerDescriptorProvider.cs b/src/Microsoft.AspNet.Mvc/IControllerDescriptorProvider.cs deleted file mode 100644 index 3139e14c56..0000000000 --- a/src/Microsoft.AspNet.Mvc/IControllerDescriptorProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace Microsoft.AspNet.Mvc -{ - public interface IControllerDescriptorProvider - { - IEnumerable GetControllers(string controllerName); - } -} diff --git a/src/Microsoft.AspNet.Mvc/IControllerFactory.cs b/src/Microsoft.AspNet.Mvc/IControllerFactory.cs index 9d212af315..adae9e35be 100644 --- a/src/Microsoft.AspNet.Mvc/IControllerFactory.cs +++ b/src/Microsoft.AspNet.Mvc/IControllerFactory.cs @@ -4,7 +4,7 @@ namespace Microsoft.AspNet.Mvc { public interface IControllerFactory { - object CreateController(HttpContext context, string controllerName); + object CreateController(HttpContext context, ActionDescriptor actionDescriptor); void ReleaseController(object controller); } diff --git a/src/Microsoft.AspNet.Mvc/RouteContext.cs b/src/Microsoft.AspNet.Mvc/RouteContext.cs deleted file mode 100644 index 40924be169..0000000000 --- a/src/Microsoft.AspNet.Mvc/RouteContext.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Microsoft.AspNet.Mvc -{ - public class ActionDescriptor - { - } -} diff --git a/src/Microsoft.AspNet.Mvc/RouteDataActionConstraint.cs b/src/Microsoft.AspNet.Mvc/RouteDataActionConstraint.cs new file mode 100644 index 0000000000..1a72bc9fe4 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/RouteDataActionConstraint.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections; +using System.ComponentModel; +using System.Diagnostics; + +namespace Microsoft.AspNet.Mvc +{ + public class RouteDataActionConstraint : IActionConstraint + { + private IEqualityComparer _comparer; + + private RouteDataActionConstraint(string routeKey) + { + if (routeKey == null) + { + throw new ArgumentNullException("routeKey"); + } + + RouteKey = routeKey; + Comparer = StringComparer.OrdinalIgnoreCase; // Is this the right comparer for route values? + } + + public RouteDataActionConstraint(string routeKey, string routeValue) + : this(routeKey) + { + if (string.IsNullOrEmpty(routeValue)) + { + throw new ArgumentNullException("routeValue"); + } + + RouteValue = routeValue; + KeyHandling = RouteKeyHandling.RequireKey; + } + + public RouteDataActionConstraint(string routeKey, RouteKeyHandling keyHandling) + : this(routeKey) + { + switch (keyHandling) + { + case RouteKeyHandling.AcceptAlways: + case RouteKeyHandling.CatchAll: + case RouteKeyHandling.DenyKey: + case RouteKeyHandling.RequireKey: + KeyHandling = keyHandling; + break; + default: +#if NET45 + throw new InvalidEnumArgumentException("keyHandling", (int)keyHandling, typeof (RouteKeyHandling)); +#else + throw new ArgumentOutOfRangeException("keyHandling"); +#endif + } + } + + public string RouteKey { get; private set; } + public string RouteValue { get; private set; } + public RouteKeyHandling KeyHandling { get; private set; } + + public IEqualityComparer Comparer + { + get { return _comparer; } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + _comparer = value; + } + } + + public bool Accept(RequestContext context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + var routeValues = context.RouteValues; + + if (routeValues == null) + { + throw new ArgumentException("Need route values", "context"); + } + + switch (KeyHandling) + { + case RouteKeyHandling.AcceptAlways: + return true; + case RouteKeyHandling.DenyKey: + return !routeValues.ContainsKey(RouteKey); + case RouteKeyHandling.CatchAll: + return routeValues.ContainsKey(RouteKey); + } + + Debug.Assert(KeyHandling == RouteKeyHandling.RequireKey, "Unexpected routeValue"); + + object value; + if (routeValues.TryGetValue(RouteKey, out value)) + { + return Comparer.Equals(value, RouteValue); + } + else + { + return false; + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc/RouteKeyHandling.cs b/src/Microsoft.AspNet.Mvc/RouteKeyHandling.cs new file mode 100644 index 0000000000..ad7bcbb486 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/RouteKeyHandling.cs @@ -0,0 +1,26 @@ +namespace Microsoft.AspNet.Mvc +{ + // This needs more thought, the intent is that we would be able to cache over this constraint without running the accept method. + public enum RouteKeyHandling + { + /// + /// Requires that the key will be in the route values, and that the content matches. + /// + RequireKey, + + /// + /// Requires that the key will not be in the route values. + /// + DenyKey, + + /// + /// Requires that the key will be in the route values, but ignore the content. + /// + CatchAll, + + /// + /// Always accept. + /// + AcceptAlways, + } +} diff --git a/src/Microsoft.AspNet.Mvc/Routing/RouteEndpoint.cs b/src/Microsoft.AspNet.Mvc/Routing/RouteEndpoint.cs index 118a640c8a..70cf70cb6d 100644 --- a/src/Microsoft.AspNet.Mvc/Routing/RouteEndpoint.cs +++ b/src/Microsoft.AspNet.Mvc/Routing/RouteEndpoint.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNet.Mvc.Routing { private readonly IServiceProvider _services; private IActionInvokerFactory _actionInvokerFactory; + private IActionSelector _actionSelector; // Using service provider here to prevent ordering issues with configuration... // IE: creating routes before configuring services, vice-versa. @@ -32,21 +33,46 @@ namespace Microsoft.AspNet.Mvc.Routing } } + private IActionSelector ActionSelector + { + get + { + if (_actionSelector == null) + { + _actionSelector = _services.GetService(); + } + + return _actionSelector; + } + } + public async Task Send(HttpContext context) { var routeValues = context.GetFeature(); var requestContext = new RequestContext(context, routeValues.Values); - var invoker = ActionInvokerFactory.CreateInvoker(requestContext); - if (invoker == null) + var actionDescriptor = ActionSelector.Select(requestContext); + + if (actionDescriptor == null) { return false; } - else + + var invoker = ActionInvokerFactory.CreateInvoker(new ActionContext(context, routeValues.Values, actionDescriptor)); + + if (invoker == null) { - await invoker.InvokeActionAsync(); - return true; + var ex = new InvalidOperationException("Could not instantiate invoker for the actionDescriptor"); + + // Add tracing/logging (what do we think of this pattern of tacking on extra data on the exception?) + ex.Data.Add("AD", actionDescriptor); + + throw ex; } + + await invoker.InvokeActionAsync(); + + return true; } } } diff --git a/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionDescriptor.cs b/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionDescriptor.cs index b6dcff9e19..f14174f949 100644 --- a/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionDescriptor.cs +++ b/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionDescriptor.cs @@ -1,12 +1,34 @@ -namespace Microsoft.AspNet.Mvc +using System; +using System.Diagnostics; +using System.Reflection; + +namespace Microsoft.AspNet.Mvc { + [DebuggerDisplay("CA {Path}:{Name}(RC-{RouteConstraints.Count})")] public class TypeMethodBasedActionDescriptor : ActionDescriptor { - // TODO: - // In the next PR the content of the descriptor is changing, and the string below will - // be represented as route constraints, so for now leaving as is. - public string ControllerName { get; set; } + public override string Path + { + get + { + return ControllerDescriptor.Name; + } + set + { + throw new InvalidOperationException("Cannot override path"); + } + } - public string ActionName { get; set; } + public string ControllerName + { + get + { + return ControllerDescriptor.Name; + } + } + + public MethodInfo MethodInfo { get; set; } + + public ControllerDescriptor ControllerDescriptor { get; set; } } } diff --git a/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionDescriptorProvider.cs b/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionDescriptorProvider.cs index e15a030b9b..92718842fa 100644 --- a/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionDescriptorProvider.cs +++ b/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionDescriptorProvider.cs @@ -1,17 +1,104 @@ -namespace Microsoft.AspNet.Mvc +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Microsoft.AspNet.Mvc { public class TypeMethodBasedActionDescriptorProvider : IActionDescriptorProvider { - public ActionDescriptor CreateDescriptor(RequestContext requestContext) - { - var controllerName = (string)requestContext.RouteValues["controller"]; - var actionName = (string)requestContext.RouteValues["action"]; + private readonly IControllerAssemblyProvider _controllerAssemblyProvider; + private readonly IActionDiscoveryConventions _conventions; + private readonly IControllerDescriptorFactory _controllerDescriptorFactory; - return new TypeMethodBasedActionDescriptor() + public TypeMethodBasedActionDescriptorProvider(IControllerAssemblyProvider controllerAssemblyProvider, + IActionDiscoveryConventions conventions, + IControllerDescriptorFactory controllerDescriptorFactory) + { + _controllerAssemblyProvider = controllerAssemblyProvider; + _conventions = conventions; + _controllerDescriptorFactory = controllerDescriptorFactory; + } + + public IEnumerable GetDescriptors() + { + var assemblies = _controllerAssemblyProvider.Assemblies; + var types = assemblies.SelectMany(a => a.DefinedTypes); + var controllers = types.Where(_conventions.IsController); + var controllerDescriptors = controllers.Select(t => _controllerDescriptorFactory.CreateControllerDescriptor(t)).ToArray(); + + foreach (var cd in controllerDescriptors) { - ControllerName = controllerName, - ActionName = actionName + foreach (var methodInfo in cd.ControllerTypeInfo.DeclaredMethods) + { + var actionInfos = _conventions.GetActions(methodInfo); + + if (actionInfos == null) + { + continue; + } + + foreach (var actionInfo in actionInfos) + { + yield return BuildDescriptor(cd, methodInfo, actionInfo); + } + } + } + } + + private static TypeMethodBasedActionDescriptor BuildDescriptor(ControllerDescriptor controllerDescriptor, MethodInfo methodInfo, ActionInfo actionInfo) + { + var ad = new TypeMethodBasedActionDescriptor + { + RouteConstraints = new List + { + new RouteDataActionConstraint("controller", controllerDescriptor.Name) + }, + + Name = actionInfo.ActionName, + ControllerDescriptor = controllerDescriptor, + MethodInfo = methodInfo, }; + + var httpMethods = actionInfo.HttpMethods; + if (httpMethods != null && httpMethods.Length > 0) + { + ad.MethodConstraints = new List + { + new HttpMethodConstraint(httpMethods) + }; + } + + if (actionInfo.RequireActionNameMatch) + { + ad.RouteConstraints.Add(new RouteDataActionConstraint("action", actionInfo.ActionName)); + } + else + { + ad.RouteConstraints.Add(new RouteDataActionConstraint("action", RouteKeyHandling.DenyKey)); + } + + return ad; + } + + private static void ApplyRest(TypeMethodBasedActionDescriptor descriptor, IEnumerable httpMethods) + { + + descriptor.RouteConstraints.Add(new RouteDataActionConstraint("action", RouteKeyHandling.DenyKey)); + } + + private static void ApplyRpc(TypeMethodBasedActionDescriptor descriptor, ActionInfo convention) + { + + var methods = convention.HttpMethods; + + // rest action require specific methods, but RPC actions do not. + if (methods != null) + { + descriptor.MethodConstraints = new List + { + new HttpMethodConstraint(methods) + }; + } } } } diff --git a/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionInvoker.cs b/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionInvoker.cs index 73f16633c5..1844274027 100644 --- a/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionInvoker.cs +++ b/src/Microsoft.AspNet.Mvc/TypeMethodBasedActionInvoker.cs @@ -8,19 +8,19 @@ namespace Microsoft.AspNet.Mvc { public class TypeMethodBasedActionInvoker : IActionInvoker { - private readonly RequestContext _requestContext; + private readonly ActionContext _actionContext; private readonly TypeMethodBasedActionDescriptor _descriptor; private readonly IActionResultFactory _actionResultFactory; private readonly IServiceProvider _serviceProvider; private readonly IControllerFactory _controllerFactory; - public TypeMethodBasedActionInvoker(RequestContext requestContext, - TypeMethodBasedActionDescriptor descriptor, - IActionResultFactory actionResultFactory, - IControllerFactory controllerFactory, - IServiceProvider serviceProvider) + public TypeMethodBasedActionInvoker(ActionContext actionContext, + TypeMethodBasedActionDescriptor descriptor, + IActionResultFactory actionResultFactory, + IControllerFactory controllerFactory, + IServiceProvider serviceProvider) { - _requestContext = requestContext; + _actionContext = actionContext; _descriptor = descriptor; _actionResultFactory = actionResultFactory; _controllerFactory = controllerFactory; @@ -31,7 +31,7 @@ namespace Microsoft.AspNet.Mvc { IActionResult actionResult = null; - object controller = _controllerFactory.CreateController(_requestContext.HttpContext, _descriptor.ControllerName); + object controller = _controllerFactory.CreateController(_actionContext.HttpContext, _descriptor); if (controller == null) { @@ -41,7 +41,7 @@ namespace Microsoft.AspNet.Mvc { Initialize(controller); - var method = controller.GetType().GetRuntimeMethods().FirstOrDefault(m => m.Name.Equals(_descriptor.ActionName, StringComparison.OrdinalIgnoreCase)); + var method = _descriptor.MethodInfo; if (method == null) { @@ -51,12 +51,12 @@ namespace Microsoft.AspNet.Mvc { object actionReturnValue = method.Invoke(controller, null); - actionResult = _actionResultFactory.CreateActionResult(method.ReturnType, actionReturnValue, _requestContext); + actionResult = _actionResultFactory.CreateActionResult(method.ReturnType, actionReturnValue, _actionContext); } } // TODO: This will probably move out once we got filters - return actionResult.ExecuteResultAsync(_requestContext); + return actionResult.ExecuteResultAsync(_actionContext); } private void Initialize(object controller) @@ -69,7 +69,7 @@ namespace Microsoft.AspNet.Mvc { if (prop.PropertyType == typeof(HttpContext)) { - prop.SetValue(controller, _requestContext.HttpContext); + prop.SetValue(controller, _actionContext.HttpContext); } } }