diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionDescriptorsCollection.cs b/src/Microsoft.AspNet.Mvc.Core/ActionDescriptorsCollection.cs new file mode 100644 index 0000000000..25e472c0e6 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ActionDescriptorsCollection.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// A cached collection of . + /// + public class ActionDescriptorsCollection + { + /// + /// Initializes a new instance of the . + /// + /// The result of action discovery + /// The unique version of discovered actions. + public ActionDescriptorsCollection([NotNull] IReadOnlyList items, int version) + { + Items = items; + Version = version; + } + + /// + /// Returns the cached . + /// + public IReadOnlyList Items { get; private set; } + + /// + /// Returns the unique version of the currently cached items. + /// + public int Version { get; private set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultActionDescriptorsCollectionProvider.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultActionDescriptorsCollectionProvider.cs new file mode 100644 index 0000000000..dc8b68ffdd --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultActionDescriptorsCollectionProvider.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Default implementation for ActionDescriptors. + /// This implementation caches the results at first call, and is not responsible for updates. + /// + public class DefaultActionDescriptorsCollectionProvider : IActionDescriptorsCollectionProvider + { + private readonly IServiceProvider _serviceProvider; + private ActionDescriptorsCollection _collection; + + /// + /// Initializes a new instance of the class. + /// + /// The application IServiceProvider. + public DefaultActionDescriptorsCollectionProvider(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + /// + /// Returns a cached collection of . + /// + public ActionDescriptorsCollection ActionDescriptors + { + get + { + if (_collection == null) + { + _collection = GetCollection(); + } + + return _collection; + } + } + + private ActionDescriptorsCollection GetCollection() + { + var actionDescriptorProvider = _serviceProvider.GetService>(); + var actionDescriptorProviderContext = new ActionDescriptorProviderContext(); + + actionDescriptorProvider.Invoke(actionDescriptorProviderContext); + + return new ActionDescriptorsCollection(actionDescriptorProviderContext.Results, 0); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultActionSelector.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultActionSelector.cs index 5e34ef8c54..90682df27e 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultActionSelector.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultActionSelector.cs @@ -9,19 +9,18 @@ using System.Threading.Tasks; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Routing; -using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc { public class DefaultActionSelector : IActionSelector { - private readonly INestedProviderManager _actionDescriptorProvider; + private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider; private readonly IActionBindingContextProvider _bindingProvider; - public DefaultActionSelector(INestedProviderManager actionDescriptorProvider, + public DefaultActionSelector(IActionDescriptorsCollectionProvider actionDescriptorsCollectionProvider, IActionBindingContextProvider bindingProvider) { - _actionDescriptorProvider = actionDescriptorProvider; + _actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider; _bindingProvider = bindingProvider; } @@ -164,7 +163,7 @@ namespace Microsoft.AspNet.Mvc // Read further for details. public virtual IEnumerable GetCandidateActions(VirtualPathContext context) { - // This method attemptss to find a unique 'best' candidate set of actions from the provided route + // This method attempts to find a unique 'best' candidate set of actions from the provided route // values and ambient route values. // // The purpose of this process is to avoid allowing certain routes to be too greedy. When a route uses @@ -332,12 +331,26 @@ namespace Microsoft.AspNet.Mvc return exemplar; } - private List GetActions() + private IReadOnlyList GetActions() { - var actionDescriptorProviderContext = new ActionDescriptorProviderContext(); - _actionDescriptorProvider.Invoke(actionDescriptorProviderContext); + var descriptors = _actionDescriptorsCollectionProvider.ActionDescriptors; - return actionDescriptorProviderContext.Results; + if (descriptors == null) + { + throw new InvalidOperationException( + Resources.FormatPropertyOfTypeCannotBeNull(_actionDescriptorsCollectionProvider.GetType(), + "ActionDescriptors")); + } + + var items = descriptors.Items; + + if (items == null) + { + throw new InvalidOperationException( + Resources.FormatPropertyOfTypeCannotBeNull(descriptors.GetType(), "Items")); + } + + return items; } private class ActionDescriptorCandidate diff --git a/src/Microsoft.AspNet.Mvc.Core/IActionDescriptorsCollectionProvider.cs b/src/Microsoft.AspNet.Mvc.Core/IActionDescriptorsCollectionProvider.cs new file mode 100644 index 0000000000..7e3917c756 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/IActionDescriptorsCollectionProvider.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Provides the currently cached collection of . + /// + /// + /// The default implementation, does not update the cache, it is up to the user + /// to create or use an implementation that can update the available actions in + /// the application. The implementor is also responsible for updating the + /// in a thread safe way. + /// + /// Default consumers of this service, are aware of the version and will recache + /// data as appropriate, but rely on the version being unique. + /// + public interface IActionDescriptorsCollectionProvider + { + /// + /// Returns the current cached + /// + ActionDescriptorsCollection ActionDescriptors { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj index e2a77b035b..c412c9ad65 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj +++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj @@ -26,6 +26,7 @@ + @@ -67,6 +68,7 @@ + @@ -122,6 +124,7 @@ + diff --git a/src/Microsoft.AspNet.Mvc/Microsoft.AspNet.Mvc.kproj b/src/Microsoft.AspNet.Mvc/Microsoft.AspNet.Mvc.kproj index b9bc5ac0ea..aed005034f 100644 --- a/src/Microsoft.AspNet.Mvc/Microsoft.AspNet.Mvc.kproj +++ b/src/Microsoft.AspNet.Mvc/Microsoft.AspNet.Mvc.kproj @@ -25,4 +25,4 @@ - + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 27784799cb..6147867aa4 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -48,6 +48,7 @@ namespace Microsoft.AspNet.Mvc ReflectedRouteConstraintsActionDescriptorProvider>(); yield return describe.Transient, ReflectedActionInvokerProvider>(); + yield return describe.Singleton(); yield return describe.Transient(); yield return describe.Transient(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs index 41de226cae..29c62a2316 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; +using System.ComponentModel.Design; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection.NestedProviders; using Moq; using Xunit; @@ -190,14 +192,24 @@ namespace Microsoft.AspNet.Mvc.Core.Test return await InvokeActionSelector(context, _actionDiscoveryConventions); } - private async Task InvokeActionSelector(RouteContext context, DefaultActionDiscoveryConventions actionDiscoveryConventions) + private async Task InvokeActionSelector(RouteContext context, + DefaultActionDiscoveryConventions actionDiscoveryConventions) { var actionDescriptorProvider = GetActionDescriptorProvider(actionDiscoveryConventions); var descriptorProvider = new NestedProviderManager(new[] { actionDescriptorProvider }); + + var serviceContainer = new ServiceContainer(); + serviceContainer.AddService(typeof(INestedProviderManager), + descriptorProvider); + + var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer); + var bindingProvider = new Mock(); - var defaultActionSelector = new DefaultActionSelector(descriptorProvider, bindingProvider.Object); + var defaultActionSelector = new DefaultActionSelector(actionCollectionDescriptorProvider, + bindingProvider.Object); + return await defaultActionSelector.SelectAsync(context); } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionSelectionConventionTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionSelectionConventionTests.cs index c276d16125..9345bc1be2 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionSelectionConventionTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionSelectionConventionTests.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; +using System.ComponentModel.Design; using System.Reflection; using System.Threading.Tasks; -using Microsoft.AspNet.Http; using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Http; +using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection.NestedProviders; using Moq; using Xunit; @@ -158,14 +160,24 @@ namespace Microsoft.AspNet.Mvc.Core.Test return await InvokeActionSelector(context, _actionDiscoveryConventions); } - private async Task InvokeActionSelector(RouteContext context, DefaultActionDiscoveryConventions actionDiscoveryConventions) + private async Task InvokeActionSelector(RouteContext context, + DefaultActionDiscoveryConventions actionDiscoveryConventions) { var actionDescriptorProvider = GetActionDescriptorProvider(actionDiscoveryConventions); var descriptorProvider = new NestedProviderManager(new[] { actionDescriptorProvider }); + + var serviceContainer = new ServiceContainer(); + serviceContainer.AddService(typeof(INestedProviderManager), + descriptorProvider); + + var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer); + var bindingProvider = new Mock(); - var defaultActionSelector = new DefaultActionSelector(descriptorProvider, bindingProvider.Object); + var defaultActionSelector = new DefaultActionSelector(actionCollectionDescriptorProvider, + bindingProvider.Object); + return await defaultActionSelector.SelectAsync(context); } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTest.cs index 2e662d0162..dd38df11ca 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTest.cs @@ -258,12 +258,12 @@ namespace Microsoft.AspNet.Mvc.Core.Test .Where(a => a.RouteConstraints.Any(c => c.RouteKey == "action" && c.Comparer.Equals(c.RouteValue, action))); } - private static DefaultActionSelector CreateSelector(IEnumerable actions) + private static DefaultActionSelector CreateSelector(IReadOnlyList actions) { - var actionProvider = new Mock>(MockBehavior.Strict); + var actionProvider = new Mock(MockBehavior.Strict); + actionProvider - .Setup(p => p.Invoke(It.IsAny())) - .Callback(c => c.Results.AddRange(actions)); + .Setup(p => p.ActionDescriptors).Returns(new ActionDescriptorsCollection(actions, 0)); var bindingProvider = new Mock(MockBehavior.Strict); bindingProvider diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/BasicTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/BasicTests.cs index a21d17826e..821127897f 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/BasicTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/BasicTests.cs @@ -68,6 +68,32 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal(expectedContent, responseContent); } + [Fact] + public async Task ActionDescriptors_CreatedOncePerRequest() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.Handler; + + var expectedContent = "1"; + + // Call the server 3 times, and make sure the return value remains the same. + var results = new string[3]; + + // Act + for (int i = 0; i < 3; i++) + { + var result = await client.GetAsync("http://localhost/Monitor/CountActionDescriptorInvocations"); + Assert.Equal(200, result.StatusCode); + results[i] = await result.ReadBodyAsStringAsync(); + } + + // Assert + Assert.Equal(expectedContent, results[0]); + Assert.Equal(expectedContent, results[1]); + Assert.Equal(expectedContent, results[2]); + } + // Calculate the path relative to the current application base path. private static string CalculateApplicationBasePath(IApplicationEnvironment appEnvironment) { diff --git a/test/WebSites/BasicWebSite/ActionDescriptorCreationCounter.cs b/test/WebSites/BasicWebSite/ActionDescriptorCreationCounter.cs new file mode 100644 index 0000000000..1be89a7aeb --- /dev/null +++ b/test/WebSites/BasicWebSite/ActionDescriptorCreationCounter.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading; +using Microsoft.AspNet.Mvc; + +namespace BasicWebSite +{ + public class ActionDescriptorCreationCounter : IActionDescriptorProvider + { + private long _callCount; + + public long CallCount + { + get + { + var callCount = Interlocked.Read(ref _callCount); + + return callCount; + } + } + + public int Order + { + get + { + return ReflectedActionDescriptorProvider.DefaultOrder - 100; + } + } + + public void Invoke(ActionDescriptorProviderContext context, Action callNext) + { + callNext(); + + if (context.Results.Count == 0) + { + throw new InvalidOperationException("No actions found!"); + } + + Interlocked.Increment(ref _callCount); + } + } +} \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/BasicWebSite.kproj b/test/WebSites/BasicWebSite/BasicWebSite.kproj index 3d70c1a8d3..70716c2d7e 100644 --- a/test/WebSites/BasicWebSite/BasicWebSite.kproj +++ b/test/WebSites/BasicWebSite/BasicWebSite.kproj @@ -28,6 +28,7 @@ 38820 + diff --git a/test/WebSites/BasicWebSite/Controllers/HomeController.cs b/test/WebSites/BasicWebSite/Controllers/HomeController.cs index 1b9005ca49..0cac77bb59 100644 --- a/test/WebSites/BasicWebSite/Controllers/HomeController.cs +++ b/test/WebSites/BasicWebSite/Controllers/HomeController.cs @@ -7,6 +7,6 @@ namespace BasicWebSite.Controllers public IActionResult Index() { return View(); - } + } } } \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/Controllers/MonitorController.cs b/test/WebSites/BasicWebSite/Controllers/MonitorController.cs new file mode 100644 index 0000000000..7e6e7c2f02 --- /dev/null +++ b/test/WebSites/BasicWebSite/Controllers/MonitorController.cs @@ -0,0 +1,21 @@ +using System.Globalization; +using Microsoft.AspNet.Mvc; +using Microsoft.Framework.DependencyInjection; + +namespace BasicWebSite +{ + public class MonitorController : Controller + { + private readonly ActionDescriptorCreationCounter _counterService; + + public MonitorController(INestedProvider counterService) + { + _counterService = (ActionDescriptorCreationCounter)counterService; + } + + public IActionResult CountActionDescriptorInvocations() + { + return Content(_counterService.CallCount.ToString(CultureInfo.InvariantCulture)); + } + } +} \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/Startup.cs b/test/WebSites/BasicWebSite/Startup.cs index 667b0dc330..ea3e18b75e 100644 --- a/test/WebSites/BasicWebSite/Startup.cs +++ b/test/WebSites/BasicWebSite/Startup.cs @@ -1,4 +1,5 @@ using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Routing; using Microsoft.Framework.DependencyInjection; @@ -13,12 +14,14 @@ namespace BasicWebSite { // Add MVC services to the services container services.AddMvc(); + + services.AddSingleton, ActionDescriptorCreationCounter>(); }); // Add MVC to the request pipeline app.UseMvc(routes => { - routes.MapRoute("ActionAsMethod", "{controller}/{action}/{id?}", + routes.MapRoute("ActionAsMethod", "{controller}/{action}", defaults: new { controller = "Home", action = "Index" }); }); }