Support per process caching of controller discovery

Scan only relevant (non skipped) assemblies
FinalizeSetup point, to make "stuff" immutable
Support controllers under any namespace
support customizing controller discovery
support customizing skipped assemblies
support customizing multiple controllers with the same name under different namespaces and assemblies
support controller ambiguity detection
This commit is contained in:
Yishai Galatzer 2014-02-07 16:20:12 -08:00
parent 4cd2cce360
commit b6a0969c1c
15 changed files with 438 additions and 52 deletions

View File

@ -2,7 +2,7 @@
using Microsoft.AspNet.Mvc;
using MvcSample.Models;
namespace MvcSample
namespace MvcSample.RandomNameSpace
{
public class Home2Controller
{

View File

@ -1,5 +1,8 @@
using Microsoft.AspNet.Mvc;
<<<<<<< HEAD
using MvcSample.Models;
=======
>>>>>>> Support per process caching of controller discovery
namespace MvcSample
{

View File

@ -2,6 +2,7 @@
using System;
using System.IO;
using Microsoft.AspNet.Abstractions;
using Microsoft.AspNet.DependencyInjection;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Mvc.Startup;
@ -24,8 +25,11 @@ namespace MvcSample
// HACK appbase doesn't seem to work. When in VS we're pointing at bin\Debug\Net45, so move up 3 directories
string appRoot = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "..", "..", ".."));
var serviceProvider = MvcServices.Create(appRoot);
var handler = new MvcHandler(serviceProvider);
var mvcServices = new MvcServices(appRoot);
mvcServices.Finalize();
var handler = (MvcHandler)(ActivatorUtilities.CreateInstance(mvcServices.Services, typeof(MvcHandler)));
builder.Run(async context =>
{

View File

@ -8,20 +8,18 @@ namespace Microsoft.AspNet.Mvc
{
public class MvcHandler
{
private readonly IServiceProvider _serviceProvider;
private readonly IActionInvokerFactory _actionInvokerFactory;
public MvcHandler(IServiceProvider serviceProvider)
public MvcHandler(IActionInvokerFactory actionInvokerFactory)
{
_serviceProvider = serviceProvider;
_actionInvokerFactory = actionInvokerFactory;
}
public Task ExecuteAsync(HttpContext context, IRouteData routeData)
{
var requestContext = new RequestContext(context, routeData);
IActionInvokerFactory invokerFactory = _serviceProvider.GetService<IActionInvokerFactory>();
var invoker = invokerFactory.CreateInvoker(requestContext);
var invoker = _actionInvokerFactory.CreateInvoker(requestContext);
return invoker.InvokeActionAsync();
}

View File

@ -1,31 +1,113 @@
using Microsoft.AspNet.DependencyInjection;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.DependencyInjection;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Mvc.Razor;
namespace Microsoft.AspNet.Mvc.Startup
{
public static class MvcServices
public class MvcServices
{
public static ServiceProvider Create(string appRoot)
private object _lock = new object();
private List<Type> _typesToFinalize = new List<Type>();
public ServiceProvider Services { get; private set; }
public MvcServices(string appRoot)
{
var services = new ServiceProvider();
services.Add<IControllerFactory, DefaultControllerFactory>();
services.Add<IActionInvokerFactory, ActionInvokerFactory>();
services.Add<IActionResultHelper, ActionResultHelper>();
services.Add<IActionResultFactory, ActionResultFactory>();
services.Add<IActionDescriptorProvider, ActionDescriptorProvider>();
services.Add<IActionInvokerProvider, ActionInvokerProvider>();
Services = new ServiceProvider();
services.AddInstance<IFileSystem>(new PhysicalFileSystem(appRoot));
services.AddInstance<IMvcRazorHost>(new MvcRazorHost("Microsoft.AspNet.Mvc.Razor.RazorView<dynamic>"));
#if NET45
services.Add<ICompilationService, CscBasedCompilationService>();
#endif
services.Add<IRazorCompilationService, RazorCompilationService>();
services.Add<IVirtualPathViewFactory, VirtualPathViewFactory>();
services.Add<IViewEngine, RazorViewEngine>();
AddAndRegisterForFinalization<ControllerCache, DefaultControllerCache>();
AddAndRegisterForFinalization<IControllerFactory, DefaultControllerFactory>();
AddAndRegisterForFinalization<IActionInvokerFactory, ActionInvokerFactory>();
AddAndRegisterForFinalization<IActionResultHelper, ActionResultHelper>();
AddAndRegisterForFinalization<IActionResultFactory, ActionResultFactory>();
AddAndRegisterForFinalization<IActionDescriptorProvider, ActionDescriptorProvider>();
AddAndRegisterForFinalization<IActionInvokerProvider, ActionInvokerProvider>();
return services;
// need singleton support here.
// AddAndRegisterForFinalization<SkipAssemblies, DefaultSkipAssemblies>();
AddInstanceAndRegisterForFinalization<ControllerCache>(new DefaultControllerCache(new DefaultSkipAssemblies()));
AddInstanceAndRegisterForFinalization<IFileSystem>(new PhysicalFileSystem(appRoot));
AddInstanceAndRegisterForFinalization<IMvcRazorHost>(new MvcRazorHost("Microsoft.AspNet.Mvc.Razor.RazorView<dynamic>"));
#if NET45
AddAndRegisterForFinalization<ICompilationService, CscBasedCompilationService>();
#endif
AddAndRegisterForFinalization<IRazorCompilationService, RazorCompilationService>();
AddAndRegisterForFinalization<IVirtualPathViewFactory, VirtualPathViewFactory>();
AddAndRegisterForFinalization<IViewEngine, RazorViewEngine>();
}
public void AddAndRegisterForFinalization<T, U>() where U : T
{
Services.Add<T, U>();
#if NET45
if (typeof(IFinalizeSetup).IsAssignableFrom(typeof(U)))
#else
if (typeof(IFinalizeSetup).GetTypeInfo().IsAssignableFrom(typeof(U).GetTypeInfo()))
#endif
{
_typesToFinalize.Add(typeof(T));
}
}
public void AddInstanceAndRegisterForFinalization<T>(object instance)
{
Services.AddInstance<T>(instance);
if ((instance as IFinalizeSetup) != null)
{
_typesToFinalize.Add(typeof(T));
}
}
public void Finalize()
{
if (_typesToFinalize == null)
{
return;
}
// We want to lock around here so finalization happens just once.
// This is not a code intended to be used during request, so the lock is just a safety precaution.
lock (_lock)
{
if (_typesToFinalize == null)
{
return;
}
foreach (var markerType in _typesToFinalize)
{
var services = this.Services.GetService(markerType);
var serviceToFinalize = services as IFinalizeSetup;
if (serviceToFinalize != null)
{
serviceToFinalize.FinalizeSetup();
}
else
{
var setOfServices = services as IEnumerable;
if (setOfServices != null)
{
foreach (var service in setOfServices.OfType<IFinalizeSetup>())
{
service.FinalizeSetup();
}
}
}
}
_typesToFinalize = null;
}
}
}
}

View File

@ -1,16 +1,20 @@
using System;
using Microsoft.AspNet.DependencyInjection;
namespace Microsoft.AspNet.Mvc
{
public class ActionInvokerProvider : IActionInvokerProvider
{
private IActionResultFactory _actionResultFactory;
private IServiceProvider _serviceProvider;
private readonly IActionResultFactory _actionResultFactory;
private readonly IServiceProvider _serviceProvider;
private readonly IControllerFactory _controrllerFactory;
public ActionInvokerProvider(IActionResultFactory actionResultFactory,
IControllerFactory controllerFactory,
IServiceProvider serviceProvider)
{
_actionResultFactory = actionResultFactory;
_controrllerFactory = controllerFactory;
_serviceProvider = serviceProvider;
}
@ -24,6 +28,7 @@ namespace Microsoft.AspNet.Mvc
requestContext,
controllerActionDescriptor,
_actionResultFactory,
_controrllerFactory,
_serviceProvider);
}

View File

@ -14,22 +14,24 @@ namespace Microsoft.AspNet.Mvc
private readonly ControllerBasedActionDescriptor _descriptor;
private readonly IActionResultFactory _actionResultFactory;
private readonly IServiceProvider _serviceProvider;
private readonly IControllerFactory _controllerFactory;
public ControllerActionInvoker(RequestContext requestContext,
ControllerBasedActionDescriptor descriptor,
IActionResultFactory actionResultFactory,
IControllerFactory controllerFactory,
IServiceProvider serviceProvider)
{
_requestContext = requestContext;
_descriptor = descriptor;
_actionResultFactory = actionResultFactory;
_controllerFactory = controllerFactory;
_serviceProvider = serviceProvider;
}
public Task InvokeActionAsync()
{
var factory = _serviceProvider.GetService<IControllerFactory>();
object controller = factory.CreateController(_requestContext.HttpContext, _descriptor.ControllerName);
object controller = _controllerFactory.CreateController(_requestContext.HttpContext, _descriptor.ControllerName);
if (controller == null)
{

View File

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

View File

@ -0,0 +1,35 @@
using System;
using System.Reflection;
namespace Microsoft.AspNet.Mvc
{
public class ControllerDescriptor
{
public ControllerDescriptor(Type controllerType, Assembly assembly)
{
if (controllerType == null)
{
throw new ArgumentNullException("controllerType");
}
if (assembly == null)
{
throw new ArgumentNullException("assembly");
}
ControllerType = controllerType;
Assembly = assembly;
ControllerName = controllerType.Name;
AssemblyName = assembly.GetName().Name;
}
public string ControllerName { get; private set; }
public string AssemblyName { get; private set; }
public Type ControllerType { get; private set; }
public Assembly Assembly { get; private set; }
}
}

View File

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Microsoft.AspNet.Mvc
{
public class DefaultControllerCache : ControllerCache, IFinalizeSetup
{
private readonly SkipAssemblies _skipAssemblies;
public IReadOnlyDictionary<string, IEnumerable<ControllerDescriptor>> Controllers { get; protected set; }
public virtual void FinalizeSetup()
{
Controllers = ScanAppDomain();
}
public DefaultControllerCache(SkipAssemblies skipAssemblies)
{
_skipAssemblies = skipAssemblies ?? new SkipNoAssemblies();
}
public override IEnumerable<ControllerDescriptor> GetController(string controllerName)
{
if (Controllers == null)
{
throw new InvalidOperationException("Finalizing the setup must happen prior to accessing controllers");
}
return Controllers[controllerName];
}
public Dictionary<string, IEnumerable<ControllerDescriptor>> ScanAppDomain()
{
var dictionary = new Dictionary<string, IEnumerable<ControllerDescriptor>>(StringComparer.Ordinal);
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(AllowAssembly))
{
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;
}
private bool AllowAssembly(Assembly assembly)
{
return !_skipAssemblies.Skip(assembly, SkipAssemblies.ControllerDiscoveryScope);
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Abstractions;
@ -10,10 +9,12 @@ namespace Microsoft.AspNet.Mvc
public class DefaultControllerFactory : IControllerFactory
{
private readonly IServiceProvider _serviceProvider;
private readonly ControllerCache _controllerCache;
public DefaultControllerFactory(IServiceProvider serviceProvider)
public DefaultControllerFactory(IServiceProvider serviceProvider, ControllerCache cache)
{
_serviceProvider = serviceProvider;
_controllerCache = cache;
}
public object CreateController(HttpContext context, string controllerName)
@ -23,29 +24,26 @@ namespace Microsoft.AspNet.Mvc
controllerName += "Controller";
}
foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
{
try
{
var type = a.GetType(controllerName) ??
a.GetType(a.GetName().Name + "." + controllerName);
#if NET45
type = type ?? a.GetTypes().FirstOrDefault(t => t.Name.Equals(controllerName, StringComparison.OrdinalIgnoreCase));
#endif
var controllers = _controllerCache.GetController(controllerName);
if (type != null)
try
{
var type = controllers.SingleOrDefault().ControllerType;
if (type != null)
{
try
{
return ActivatorUtilities.CreateInstance(_serviceProvider, type);
}
catch (ReflectionTypeLoadException)
{
}
}
catch (ReflectionTypeLoadException)
{
// TODO: Trace here
}
catch (Exception)
{
// TODO: Trace here
}
}
catch (InvalidOperationException)
{
throw new InvalidOperationException("Ambiguity: Duplicate controllers match the controller name");
}
return null;

View File

@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace Microsoft.AspNet.Mvc
{
public class DefaultSkipAssemblies : SkipAssemblies
{
private HashSet<string> _hash = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
public DefaultSkipAssemblies(IEnumerable<string> assemblyNames)
{
InitializeHash(assemblyNames);
}
public DefaultSkipAssemblies()
{
#if NET45
InitializeHash(@"
klr.net45.managed
Microsoft.Net.Runtime.Interfaces
klr.host
System
System.Core
System.Configuration
System.Xml
Microsoft.Net.ApplicationHost
Microsoft.Net.Runtime
Newtonsoft.Json
System.Numerics
System.ComponentModel.DataAnnotations
System.Runtime.Serialization
System.Xml.Linq
System.Data
Microsoft.CodeAnalysis
System.Collections.Immutable
System.Runtime
Microsoft.CodeAnalysis.CSharp
System.IO.Compression
Microsoft.AspNet.FileSystems
Microsoft.AspNet.Abstractions
Microsoft.AspNet.DependencyInjection
Microsoft.AspNet.Razor
Newtonsoft.Json
System.Linq
System.Collections
System.Runtime.Extensions
System.Threading
System.Reflection.Metadata.Ecma335
Microsoft.AspNet.Mvc.ModelBinding
Microsoft.AspNet.Mvc.Rendering
Microsoft.AspNet.Mvc
Microsoft.AspNet.Mvc.Razor.Host
Microsoft.AspNet.Mvc.Razor
Microsoft.AspNet.Mvc.Startup
Owin
Microsoft.Owin
Microsoft.Owin.Diagnostics
Microsoft.Owin.Hosting
Microsoft.Owin.Host.HttpListener
Microsoft.AspNet.AppBuilderSupport
Anonymously Hosted DynamicMethods Assembly
Microsoft.AspNet.PipelineCore
Microsoft.AspNet.FeatureModel
mscorlib
klr.net45.managed
Microsoft.Net.Runtime.Interfaces
klr.host
System
System.Core
System.Configuration
System.Xml
Microsoft.Net.ApplicationHost
Microsoft.Net.Runtime
Newtonsoft.Json
System.Numerics
System.ComponentModel.DataAnnotations
System.Runtime.Serialization
System.Xml.Linq
System.Data
Microsoft.CodeAnalysis
System.Collections.Immutable
System.Runtime
Microsoft.CodeAnalysis.CSharp
System.IO.Compression
Microsoft.AspNet.FileSystems
Microsoft.AspNet.Abstractions
Microsoft.AspNet.DependencyInjection
Microsoft.AspNet.Razor
Newtonsoft.Json
System.Linq
System.Collections
System.Runtime.Extensions
System.Threading
System.Reflection.Metadata.Ecma335
Microsoft.AspNet.Mvc.ModelBinding
Microsoft.AspNet.Mvc.Rendering
Microsoft.AspNet.Mvc
Microsoft.AspNet.Mvc.Razor.Host
Microsoft.AspNet.Mvc.Razor
Microsoft.AspNet.Mvc.Startup
Owin
Microsoft.Owin
Microsoft.Owin.Diag".Split(new char[] { '\r', '\n'}, StringSplitOptions.RemoveEmptyEntries));
#else
#endif
}
private void InitializeHash(IEnumerable<string> assemblyNames)
{
if (assemblyNames == null)
{
throw new ArgumentNullException("assemblyNames");
}
foreach (var assemblyName in assemblyNames)
{
if (!string.IsNullOrWhiteSpace(assemblyName))
{
_hash.Add(assemblyName);
}
}
}
public override bool Skip(Assembly assembly, string scope)
{
if (scope == null ||
!string.Equals(scope, SkipAssemblies.ControllerDiscoveryScope, StringComparison.Ordinal))
{
return false;
}
string name = assembly.GetName().Name;
bool contains = _hash.Contains(name);
return contains;
}
}
}

View File

@ -0,0 +1,7 @@
namespace Microsoft.AspNet.Mvc
{
public interface IFinalizeSetup
{
void FinalizeSetup();
}
}

View File

@ -0,0 +1,11 @@
using System.Reflection;
namespace Microsoft.AspNet.Mvc
{
public abstract class SkipAssemblies
{
public static readonly string ControllerDiscoveryScope = "DCS";
public abstract bool Skip(Assembly assembly, string scope);
}
}

View File

@ -0,0 +1,12 @@
using System.Reflection;
namespace Microsoft.AspNet.Mvc
{
public class SkipNoAssemblies : SkipAssemblies
{
public override bool Skip(Assembly assembly, string scope)
{
return false;
}
}
}