Action selection based on action descriptors

This commit is contained in:
Yishai Galatzer 2014-02-16 17:42:48 -08:00
parent 1d40372cc3
commit 941a12daea
43 changed files with 687 additions and 256 deletions

View File

@ -0,0 +1,12 @@
using Microsoft.AspNet.Mvc;
namespace MvcSample
{
public class SimpleRest : Controller
{
public string Get()
{
return "Get method";
}
}
}

View File

@ -38,6 +38,10 @@ namespace MvcSample
endpoint, endpoint,
"{controller}/{action}", "{controller}/{action}",
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase) { { "controller", "Home" }, { "action", "Index" } })); new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase) { { "controller", "Home" }, { "action", "Index" } }));
router.Add(new TemplateRoute(
endpoint,
"{controller}",
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase) { { "controller", "Home" } }));
} }
} }
} }

View File

@ -7,7 +7,7 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
public class RazorViewEngine : IViewEngine public class RazorViewEngine : IViewEngine
{ {
private static readonly string[] _viewLocationFormats = new[] private static readonly string[] _viewLocationFormats =
{ {
"/Views/{1}/{0}.cshtml", "/Views/{1}/{0}.cshtml",
"/Views/Shared/{0}.cshtml", "/Views/Shared/{0}.cshtml",
@ -28,11 +28,13 @@ namespace Microsoft.AspNet.Mvc.Razor
get { return _viewLocationFormats; } get { return _viewLocationFormats; }
} }
public async Task<ViewEngineResult> FindView(RequestContext requestContext, string viewName) public async Task<ViewEngineResult> 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 // 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 // type of the action descriptor
var actionDescriptor = _actionDescriptorProvider.CreateDescriptor(requestContext) as TypeMethodBasedActionDescriptor; ActionDescriptor actionDescriptor = actionContext.ActionDescriptor;
if (actionDescriptor == null) if (actionDescriptor == null)
{ {
@ -41,7 +43,7 @@ namespace Microsoft.AspNet.Mvc.Razor
if (String.IsNullOrEmpty(viewName)) if (String.IsNullOrEmpty(viewName))
{ {
viewName = actionDescriptor.ActionName; viewName = actionDescriptor.Name;
} }
if (String.IsNullOrEmpty(viewName)) if (String.IsNullOrEmpty(viewName))
@ -59,7 +61,7 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
else else
{ {
string controllerName = actionDescriptor.ControllerName; string controllerName = actionDescriptor.Path;
var searchedLocations = new List<string>(_viewLocationFormats.Length); var searchedLocations = new List<string>(_viewLocationFormats.Length);
for (int i = 0; i < _viewLocationFormats.Length; i++) for (int i = 0; i < _viewLocationFormats.Length; i++)
{ {

View File

@ -4,6 +4,7 @@ namespace Microsoft.AspNet.Mvc
{ {
public interface IViewEngine public interface IViewEngine
{ {
Task<ViewEngineResult> FindView(RequestContext requestContext, string viewName); // TODO: Relayer to allow this to be ActionContext. We probably need the common MVC assembly
Task<ViewEngineResult> FindView(object actionContext, string viewName);
} }
} }

View File

@ -1,8 +1,6 @@
using Microsoft.AspNet.DependencyInjection; using Microsoft.AspNet.DependencyInjection;
using Microsoft.AspNet.FileSystems; using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Mvc.Razor; using Microsoft.AspNet.Mvc.Razor;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Routing;
namespace Microsoft.AspNet.Mvc.Startup namespace Microsoft.AspNet.Mvc.Startup
{ {
@ -15,18 +13,16 @@ namespace Microsoft.AspNet.Mvc.Startup
Services = new ServiceProvider(); Services = new ServiceProvider();
Add<IControllerFactory, DefaultControllerFactory>(); Add<IControllerFactory, DefaultControllerFactory>();
Add<IControllerDescriptorFactory, DefaultControllerDescriptorFactory>();
Add<IActionSelector, DefaultActionSelector>();
Add<IActionInvokerFactory, ActionInvokerFactory>(); Add<IActionInvokerFactory, ActionInvokerFactory>();
Add<IActionResultHelper, ActionResultHelper>(); Add<IActionResultHelper, ActionResultHelper>();
Add<IActionResultFactory, ActionResultFactory>(); Add<IActionResultFactory, ActionResultFactory>();
Add<IActionDescriptorProvider, TypeMethodBasedActionDescriptorProvider>(); Add<IActionDescriptorProvider, TypeMethodBasedActionDescriptorProvider>();
Add<IActionInvokerProvider, ActionInvokerProvider>(); Add<IActionInvokerProvider, ActionInvokerProvider>();
Add<IControllerAssemblyProvider, AppDomainControllerAssemblyProvider>();
Add<IActionDiscoveryConventions, DefaultActionDiscoveryConventions>();
// need singleton support here.
// need a design for immutable caches at startup
var provider = new DefaultControllerDescriptorProvider(new AppDomainControllerAssemblyProvider());
provider.FinalizeSetup();
AddInstance<IControllerDescriptorProvider>(provider);
AddInstance<IFileSystem>(new PhysicalFileSystem(appRoot)); AddInstance<IFileSystem>(new PhysicalFileSystem(appRoot));
AddInstance<IMvcRazorHost>(new MvcRazorHost(typeof(RazorView).FullName)); AddInstance<IMvcRazorHost>(new MvcRazorHost(typeof(RazorView).FullName));

View File

@ -0,0 +1,21 @@
using System.Collections.Generic;
using Microsoft.AspNet.Abstractions;
namespace Microsoft.AspNet.Mvc
{
public class ActionContext
{
public ActionContext(HttpContext httpContext, IDictionary<string, object> routeValues, ActionDescriptor actionDescriptor)
{
HttpContext = httpContext;
RouteValues = routeValues;
ActionDescriptor = actionDescriptor;
}
public HttpContext HttpContext { get; private set; }
public IDictionary<string, object> RouteValues { get; private set; }
public ActionDescriptor ActionDescriptor { get; private set; }
}
}

View File

@ -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; }
}
}

View File

@ -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<RouteDataActionConstraint> RouteConstraints { get; set; }
public List<HttpMethodConstraint> MethodConstraints { get; set; }
public IEnumerable<IActionConstraint> DynamicConstraints { get; set; }
}
}

View File

@ -1,7 +1,4 @@
 namespace Microsoft.AspNet.Mvc
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{ {
public class ActionInvokerFactory : IActionInvokerFactory public class ActionInvokerFactory : IActionInvokerFactory
{ {
@ -18,15 +15,9 @@ namespace Microsoft.AspNet.Mvc
_actionInvokerProvider = actionInvokerProvider; _actionInvokerProvider = actionInvokerProvider;
} }
public IActionInvoker CreateInvoker(RequestContext requestContext) public IActionInvoker CreateInvoker(ActionContext actionContext)
{ {
ActionDescriptor routeContext = _routeContextProvider.CreateDescriptor(requestContext); return _actionInvokerProvider.GetInvoker(actionContext);
if (routeContext == null)
{
return null;
}
return _actionInvokerProvider.GetInvoker(requestContext, routeContext);
} }
} }
} }

View File

@ -1,5 +1,4 @@
using System; using System;
using Microsoft.AspNet.DependencyInjection;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
{ {
@ -18,14 +17,14 @@ namespace Microsoft.AspNet.Mvc
_serviceProvider = serviceProvider; _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) if (ad != null)
{ {
return new TypeMethodBasedActionInvoker( return new TypeMethodBasedActionInvoker(
requestContext, actionContext,
ad, ad,
_actionResultFactory, _actionResultFactory,
_controllerFactory, _controllerFactory,

View File

@ -11,10 +11,10 @@ namespace Microsoft.AspNet.Mvc
_result = result; _result = result;
} }
public IActionResult CreateActionResult(Type declaredReturnType, object actionReturnValue, RequestContext requestContext) public IActionResult CreateActionResult(Type declaredReturnType, object actionReturnValue, ActionContext actionContext)
{ {
// optimize common path // optimize common path
IActionResult actionResult = actionReturnValue as IActionResult; var actionResult = actionReturnValue as IActionResult;
if (actionResult != null) if (actionResult != null)
{ {
@ -38,17 +38,19 @@ namespace Microsoft.AspNet.Mvc
throw new InvalidOperationException("HttpActionDescriptor_NoConverterForGenericParamterTypeExists"); throw new InvalidOperationException("HttpActionDescriptor_NoConverterForGenericParamterTypeExists");
} }
if (declaredReturnType.IsAssignableFrom(typeof(void))) if (declaredReturnType.IsAssignableFrom(typeof(void)) || actionReturnValue == null)
{ {
return new NoContentResult(); return new NoContentResult();
} }
if (actionReturnValue is string) var actionReturnString = actionReturnValue as string;
if (actionReturnString != null)
{ {
return new ContentResult return new ContentResult
{ {
ContentType = "text/plain", ContentType = "text/plain",
Content = (string)actionReturnValue, Content = actionReturnString,
}; };
} }

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc
public string ContentType { get; set; } public string ContentType { get; set; }
public async Task ExecuteResultAsync(RequestContext context) public async Task ExecuteResultAsync(ActionContext context)
{ {
if (context == null) if (context == null)
{ {
@ -26,12 +26,7 @@ namespace Microsoft.AspNet.Mvc
{ {
response.ContentType = ContentType; response.ContentType = ContentType;
} }
//if (ContentEncoding != null)
//{
// response.ContentEncoding = ContentEncoding;
//}
if (Content != null) if (Content != null)
{ {
await response.WriteAsync(Content); await response.WriteAsync(Content);

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Mvc
get { return _singleton; } get { return _singleton; }
} }
public async Task ExecuteResultAsync(RequestContext context) public async Task ExecuteResultAsync(ActionContext context)
{ {
} }
} }

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Mvc
_statusCode = statusCode; _statusCode = statusCode;
} }
public async Task ExecuteResultAsync(RequestContext context) public async Task ExecuteResultAsync(ActionContext context)
{ {
context.HttpContext.Response.StatusCode = _statusCode; context.HttpContext.Response.StatusCode = _statusCode;
} }

View File

@ -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; HttpResponse response = context.HttpContext.Response;

View File

@ -1,14 +1,13 @@
using System; using System;
using System.Text;
using System.Threading.Tasks;
using System.Net; using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.Abstractions;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
{ {
public class NoContentResult : IActionResult public class NoContentResult : IActionResult
{ {
public async Task ExecuteResultAsync(RequestContext context) public async Task ExecuteResultAsync(ActionContext context)
{ {
if (context == null) if (context == null)
{ {
@ -24,8 +23,6 @@ namespace Microsoft.AspNet.Mvc
#endif #endif
await Task.FromResult(false); await Task.FromResult(false);
return;
} }
} }
} }

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc
public ViewData ViewData { get; set; } public ViewData ViewData { get; set; }
public async Task ExecuteResultAsync(RequestContext context) public async Task ExecuteResultAsync(ActionContext context)
{ {
if (context == null) if (context == null)
{ {
@ -44,9 +44,9 @@ namespace Microsoft.AspNet.Mvc
} }
} }
private async Task<IView> FindView(RequestContext requestContext, string viewName) private async Task<IView> FindView(ActionContext actionContext, string viewName)
{ {
ViewEngineResult result = await _viewEngine.FindView(requestContext, viewName); ViewEngineResult result = await _viewEngine.FindView(actionContext, viewName);
if (!result.Success) if (!result.Success)
{ {
string locationsText = String.Join(Environment.NewLine, result.SearchedLocations); string locationsText = String.Join(Environment.NewLine, result.SearchedLocations);

View File

@ -23,6 +23,7 @@ namespace Microsoft.AspNet.Mvc
// consider mechanisms to filter assemblies upfront, so scanning cost is minimized and startup improved. // 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. // 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) // 2 - Remove well known assemblies (maintenance and composability cost)
return true; return true;
} }
} }

View File

@ -5,31 +5,22 @@ namespace Microsoft.AspNet.Mvc
{ {
public class ControllerDescriptor 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) ControllerTypeInfo = controllerTypeInfo;
{
throw new ArgumentNullException("assembly");
}
ControllerType = controllerType; Name = controllerTypeInfo.Name.EndsWith("Controller", StringComparison.Ordinal)
Assembly = assembly; ? controllerTypeInfo.Name.Substring(0, controllerTypeInfo.Name.Length - "Controller".Length)
: controllerTypeInfo.Name;
ControllerName = controllerType.Name;
AssemblyName = assembly.GetName().Name;
} }
public string ControllerName { get; private set; } public string Name { get; private set; }
public string AssemblyName { get; private set; } public TypeInfo ControllerTypeInfo { get; private set; }
public Type ControllerType { get; private set; }
public Assembly Assembly { get; private set; }
} }
} }

View File

@ -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<ActionInfo> 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;
}
}
}

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNet.Mvc
{
public class DefaultActionSelector : IActionSelector
{
private readonly IEnumerable<IActionDescriptorProvider> _actionDescriptorProviders;
public DefaultActionSelector(IEnumerable<IActionDescriptorProvider> 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)));
}
}
}

View File

@ -0,0 +1,12 @@
using System.Reflection;
namespace Microsoft.AspNet.Mvc
{
public class DefaultControllerDescriptorFactory : IControllerDescriptorFactory
{
public ControllerDescriptor CreateControllerDescriptor(TypeInfo typeInfo)
{
return new ControllerDescriptor(typeInfo);
}
}
}

View File

@ -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<string, IEnumerable<ControllerDescriptor>> Controllers { get; protected set; }
public void FinalizeSetup()
{
Controllers = ScanAppDomain();
}
public DefaultControllerDescriptorProvider(IControllerAssemblyProvider controllerAssemblyProvider)
{
if (controllerAssemblyProvider == null)
{
throw new ArgumentNullException("controllerAssemblyProvider");
}
_controllerAssemblyProvider = controllerAssemblyProvider;
}
public IEnumerable<ControllerDescriptor> 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<ControllerDescriptor> descriptors = null;
if (Controllers.TryGetValue(controllerName, out descriptors))
{
return descriptors;
}
return Enumerable.Empty<ControllerDescriptor>();
}
public Dictionary<string, IEnumerable<ControllerDescriptor>> ScanAppDomain()
{
var dictionary = new Dictionary<string, IEnumerable<ControllerDescriptor>>(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<ControllerDescriptor> controllerDescriptors;
if (!dictionary.TryGetValue(type.Name, out controllerDescriptors))
{
controllerDescriptors = new List<ControllerDescriptor>();
dictionary.Add(descriptor.ControllerName, controllerDescriptors);
}
((List<ControllerDescriptor>)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;
}
}
}

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.Abstractions;
using Microsoft.AspNet.DependencyInjection; using Microsoft.AspNet.DependencyInjection;
@ -9,49 +8,37 @@ namespace Microsoft.AspNet.Mvc
public class DefaultControllerFactory : IControllerFactory public class DefaultControllerFactory : IControllerFactory
{ {
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IControllerDescriptorProvider _controllerDescriptorProvider;
public DefaultControllerFactory(IServiceProvider serviceProvider, IControllerDescriptorProvider controllerDescriptorProvider) public DefaultControllerFactory(IServiceProvider serviceProvider)
{ {
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_controllerDescriptorProvider = controllerDescriptorProvider;
} }
public object CreateController(HttpContext context, string controllerName) public object CreateController(HttpContext context, ActionDescriptor actionDescriptor)
{ {
var controllers = _controllerDescriptorProvider.GetControllers(controllerName); 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(); contextProperty.SetMethod.Invoke(controller, new object[] { context });
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");
} }
return controller;
}
catch (ReflectionTypeLoadException)
{
} }
return null; return null;

View File

@ -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<string> _methods;
// Empty collection means any method will be accepted.
public HttpMethodConstraint(IEnumerable<string> httpMethods)
{
if (httpMethods == null)
{
throw new ArgumentNullException("httpMethods");
}
var methods = new List<string>();
foreach (var method in httpMethods)
{
if (string.IsNullOrEmpty(method))
{
throw new ArgumentException("httpMethod cannot be null or empty");
}
methods.Add(method);
}
_methods = new ReadOnlyCollection<string>(methods);
}
public IEnumerable<string> 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)));
}
}
}

View File

@ -0,0 +1,7 @@
namespace Microsoft.AspNet.Mvc
{
public interface IActionConstraint
{
bool Accept(RequestContext context);
}
}

View File

@ -1,9 +1,9 @@
using Microsoft.AspNet.Mvc.Routing; using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
{ {
public interface IActionDescriptorProvider public interface IActionDescriptorProvider
{ {
ActionDescriptor CreateDescriptor(RequestContext requestContext); IEnumerable<ActionDescriptor> GetDescriptors();
} }
} }

View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Reflection;
namespace Microsoft.AspNet.Mvc
{
public interface IActionDiscoveryConventions
{
bool IsController(TypeInfo typeInfo);
IEnumerable<ActionInfo> GetActions(MethodInfo methodInfo);
}
}

View File

@ -1,10 +1,7 @@
 namespace Microsoft.AspNet.Mvc
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{ {
public interface IActionInvokerFactory public interface IActionInvokerFactory
{ {
IActionInvoker CreateInvoker(RequestContext requestContext); IActionInvoker CreateInvoker(ActionContext actionContext);
} }
} }

View File

@ -1,8 +1,7 @@
 namespace Microsoft.AspNet.Mvc
namespace Microsoft.AspNet.Mvc
{ {
public interface IActionInvokerProvider public interface IActionInvokerProvider
{ {
IActionInvoker GetInvoker(RequestContext requestContext, ActionDescriptor routeContext); IActionInvoker GetInvoker(ActionContext actionContext);
} }
} }

View File

@ -4,6 +4,6 @@ namespace Microsoft.AspNet.Mvc
{ {
public interface IActionResult public interface IActionResult
{ {
Task ExecuteResultAsync(RequestContext context); Task ExecuteResultAsync(ActionContext context);
} }
} }

View File

@ -4,6 +4,6 @@ namespace Microsoft.AspNet.Mvc
{ {
public interface IActionResultFactory public interface IActionResultFactory
{ {
IActionResult CreateActionResult(Type declaredReturnType, object actionReturnValue, RequestContext requestContext); IActionResult CreateActionResult(Type declaredReturnType, object actionReturnValue, ActionContext actionContext);
} }
} }

View File

@ -0,0 +1,9 @@
namespace Microsoft.AspNet.Mvc
{
public interface IActionSelector
{
ActionDescriptor Select(RequestContext context);
bool Match(ActionDescriptor descriptor, RequestContext context);
}
}

View File

@ -0,0 +1,9 @@
using System.Reflection;
namespace Microsoft.AspNet.Mvc
{
public interface IControllerDescriptorFactory
{
ControllerDescriptor CreateControllerDescriptor(TypeInfo type);
}
}

View File

@ -1,9 +0,0 @@
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc
{
public interface IControllerDescriptorProvider
{
IEnumerable<ControllerDescriptor> GetControllers(string controllerName);
}
}

View File

@ -4,7 +4,7 @@ namespace Microsoft.AspNet.Mvc
{ {
public interface IControllerFactory public interface IControllerFactory
{ {
object CreateController(HttpContext context, string controllerName); object CreateController(HttpContext context, ActionDescriptor actionDescriptor);
void ReleaseController(object controller); void ReleaseController(object controller);
} }

View File

@ -1,6 +0,0 @@
namespace Microsoft.AspNet.Mvc
{
public class ActionDescriptor
{
}
}

View File

@ -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;
}
}
}
}

View File

@ -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
{
/// <summary>
/// Requires that the key will be in the route values, and that the content matches.
/// </summary>
RequireKey,
/// <summary>
/// Requires that the key will not be in the route values.
/// </summary>
DenyKey,
/// <summary>
/// Requires that the key will be in the route values, but ignore the content.
/// </summary>
CatchAll,
/// <summary>
/// Always accept.
/// </summary>
AcceptAlways,
}
}

View File

@ -11,6 +11,7 @@ namespace Microsoft.AspNet.Mvc.Routing
{ {
private readonly IServiceProvider _services; private readonly IServiceProvider _services;
private IActionInvokerFactory _actionInvokerFactory; private IActionInvokerFactory _actionInvokerFactory;
private IActionSelector _actionSelector;
// Using service provider here to prevent ordering issues with configuration... // Using service provider here to prevent ordering issues with configuration...
// IE: creating routes before configuring services, vice-versa. // 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<IActionSelector>();
}
return _actionSelector;
}
}
public async Task<bool> Send(HttpContext context) public async Task<bool> Send(HttpContext context)
{ {
var routeValues = context.GetFeature<IRouteValues>(); var routeValues = context.GetFeature<IRouteValues>();
var requestContext = new RequestContext(context, routeValues.Values); var requestContext = new RequestContext(context, routeValues.Values);
var invoker = ActionInvokerFactory.CreateInvoker(requestContext); var actionDescriptor = ActionSelector.Select(requestContext);
if (invoker == null)
if (actionDescriptor == null)
{ {
return false; return false;
} }
else
var invoker = ActionInvokerFactory.CreateInvoker(new ActionContext(context, routeValues.Values, actionDescriptor));
if (invoker == null)
{ {
await invoker.InvokeActionAsync(); var ex = new InvalidOperationException("Could not instantiate invoker for the actionDescriptor");
return true;
// 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;
} }
} }
} }

View File

@ -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 public class TypeMethodBasedActionDescriptor : ActionDescriptor
{ {
// TODO: public override string Path
// 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. get
public string ControllerName { get; set; } {
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; }
} }
} }

View File

@ -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 class TypeMethodBasedActionDescriptorProvider : IActionDescriptorProvider
{ {
public ActionDescriptor CreateDescriptor(RequestContext requestContext) private readonly IControllerAssemblyProvider _controllerAssemblyProvider;
{ private readonly IActionDiscoveryConventions _conventions;
var controllerName = (string)requestContext.RouteValues["controller"]; private readonly IControllerDescriptorFactory _controllerDescriptorFactory;
var actionName = (string)requestContext.RouteValues["action"];
return new TypeMethodBasedActionDescriptor() public TypeMethodBasedActionDescriptorProvider(IControllerAssemblyProvider controllerAssemblyProvider,
IActionDiscoveryConventions conventions,
IControllerDescriptorFactory controllerDescriptorFactory)
{
_controllerAssemblyProvider = controllerAssemblyProvider;
_conventions = conventions;
_controllerDescriptorFactory = controllerDescriptorFactory;
}
public IEnumerable<ActionDescriptor> 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, foreach (var methodInfo in cd.ControllerTypeInfo.DeclaredMethods)
ActionName = actionName {
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<RouteDataActionConstraint>
{
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<HttpMethodConstraint>
{
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<string> 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<HttpMethodConstraint>
{
new HttpMethodConstraint(methods)
};
}
} }
} }
} }

View File

@ -8,19 +8,19 @@ namespace Microsoft.AspNet.Mvc
{ {
public class TypeMethodBasedActionInvoker : IActionInvoker public class TypeMethodBasedActionInvoker : IActionInvoker
{ {
private readonly RequestContext _requestContext; private readonly ActionContext _actionContext;
private readonly TypeMethodBasedActionDescriptor _descriptor; private readonly TypeMethodBasedActionDescriptor _descriptor;
private readonly IActionResultFactory _actionResultFactory; private readonly IActionResultFactory _actionResultFactory;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IControllerFactory _controllerFactory; private readonly IControllerFactory _controllerFactory;
public TypeMethodBasedActionInvoker(RequestContext requestContext, public TypeMethodBasedActionInvoker(ActionContext actionContext,
TypeMethodBasedActionDescriptor descriptor, TypeMethodBasedActionDescriptor descriptor,
IActionResultFactory actionResultFactory, IActionResultFactory actionResultFactory,
IControllerFactory controllerFactory, IControllerFactory controllerFactory,
IServiceProvider serviceProvider) IServiceProvider serviceProvider)
{ {
_requestContext = requestContext; _actionContext = actionContext;
_descriptor = descriptor; _descriptor = descriptor;
_actionResultFactory = actionResultFactory; _actionResultFactory = actionResultFactory;
_controllerFactory = controllerFactory; _controllerFactory = controllerFactory;
@ -31,7 +31,7 @@ namespace Microsoft.AspNet.Mvc
{ {
IActionResult actionResult = null; IActionResult actionResult = null;
object controller = _controllerFactory.CreateController(_requestContext.HttpContext, _descriptor.ControllerName); object controller = _controllerFactory.CreateController(_actionContext.HttpContext, _descriptor);
if (controller == null) if (controller == null)
{ {
@ -41,7 +41,7 @@ namespace Microsoft.AspNet.Mvc
{ {
Initialize(controller); Initialize(controller);
var method = controller.GetType().GetRuntimeMethods().FirstOrDefault(m => m.Name.Equals(_descriptor.ActionName, StringComparison.OrdinalIgnoreCase)); var method = _descriptor.MethodInfo;
if (method == null) if (method == null)
{ {
@ -51,12 +51,12 @@ namespace Microsoft.AspNet.Mvc
{ {
object actionReturnValue = method.Invoke(controller, null); 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 // TODO: This will probably move out once we got filters
return actionResult.ExecuteResultAsync(_requestContext); return actionResult.ExecuteResultAsync(_actionContext);
} }
private void Initialize(object controller) private void Initialize(object controller)
@ -69,7 +69,7 @@ namespace Microsoft.AspNet.Mvc
{ {
if (prop.PropertyType == typeof(HttpContext)) if (prop.PropertyType == typeof(HttpContext))
{ {
prop.SetValue(controller, _requestContext.HttpContext); prop.SetValue(controller, _actionContext.HttpContext);
} }
} }
} }