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,
"{controller}/{action}",
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase) { { "controller", "Home" }, { "action", "Index" } }));
router.Add(new TemplateRoute(
endpoint,
"{controller}",
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase) { { "controller", "Home" } }));
}
}
}

View File

@ -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<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
// 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<string>(_viewLocationFormats.Length);
for (int i = 0; i < _viewLocationFormats.Length; i++)
{

View File

@ -4,6 +4,7 @@ namespace Microsoft.AspNet.Mvc
{
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.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<IControllerFactory, DefaultControllerFactory>();
Add<IControllerDescriptorFactory, DefaultControllerDescriptorFactory>();
Add<IActionSelector, DefaultActionSelector>();
Add<IActionInvokerFactory, ActionInvokerFactory>();
Add<IActionResultHelper, ActionResultHelper>();
Add<IActionResultFactory, ActionResultFactory>();
Add<IActionDescriptorProvider, TypeMethodBasedActionDescriptorProvider>();
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<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 @@

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

View File

@ -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,

View File

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

View File

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

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Mvc
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;
}
public async Task ExecuteResultAsync(RequestContext context)
public async Task ExecuteResultAsync(ActionContext context)
{
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;

View File

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

View File

@ -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<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)
{
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.
// 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;
}
}

View File

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

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.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;

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
{
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 @@

using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
namespace Microsoft.AspNet.Mvc
{
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
{
IActionInvoker GetInvoker(RequestContext requestContext, ActionDescriptor routeContext);
IActionInvoker GetInvoker(ActionContext actionContext);
}
}

View File

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

View File

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

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
{
object CreateController(HttpContext context, string controllerName);
object CreateController(HttpContext context, ActionDescriptor actionDescriptor);
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 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<IActionSelector>();
}
return _actionSelector;
}
}
public async Task<bool> Send(HttpContext context)
{
var routeValues = context.GetFeature<IRouteValues>();
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;
}
}
}

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
{
// 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; }
}
}

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 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<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,
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<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
{
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);
}
}
}