Cache action descriptor providers and provide a race safe data structure to get the version.
The default implementation has a safe race, and does not allow for action description addition at runtime. It can be replaced with an implementation that can reload. Consumers of the new service that do extra caching are now responsible to look at the version and change the implementation.
This commit is contained in:
parent
31d3180635
commit
6d78f8adb3
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A cached collection of <see cref="ActionDescriptor" />.
|
||||
/// </summary>
|
||||
public class ActionDescriptorsCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActionDescriptorsCollection"/>.
|
||||
/// </summary>
|
||||
/// <param name="items">The result of action discovery</param>
|
||||
/// <param name="version">The unique version of discovered actions.</param>
|
||||
public ActionDescriptorsCollection([NotNull] IReadOnlyList<ActionDescriptor> items, int version)
|
||||
{
|
||||
Items = items;
|
||||
Version = version;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached <see cref="IReadOnlyList{ActionDescriptor}"/>.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ActionDescriptor> Items { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the unique version of the currently cached items.
|
||||
/// </summary>
|
||||
public int Version { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation for ActionDescriptors.
|
||||
/// This implementation caches the results at first call, and is not responsible for updates.
|
||||
/// </summary>
|
||||
public class DefaultActionDescriptorsCollectionProvider : IActionDescriptorsCollectionProvider
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private ActionDescriptorsCollection _collection;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultActionDescriptorsCollectionProvider" /> class.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The application IServiceProvider.</param>
|
||||
public DefaultActionDescriptorsCollectionProvider(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a cached collection of <see cref="ActionDescriptor" />.
|
||||
/// </summary>
|
||||
public ActionDescriptorsCollection ActionDescriptors
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_collection == null)
|
||||
{
|
||||
_collection = GetCollection();
|
||||
}
|
||||
|
||||
return _collection;
|
||||
}
|
||||
}
|
||||
|
||||
private ActionDescriptorsCollection GetCollection()
|
||||
{
|
||||
var actionDescriptorProvider = _serviceProvider.GetService<INestedProviderManager<ActionDescriptorProviderContext>>();
|
||||
var actionDescriptorProviderContext = new ActionDescriptorProviderContext();
|
||||
|
||||
actionDescriptorProvider.Invoke(actionDescriptorProviderContext);
|
||||
|
||||
return new ActionDescriptorsCollection(actionDescriptorProviderContext.Results, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ActionDescriptorProviderContext> _actionDescriptorProvider;
|
||||
private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider;
|
||||
private readonly IActionBindingContextProvider _bindingProvider;
|
||||
|
||||
public DefaultActionSelector(INestedProviderManager<ActionDescriptorProviderContext> 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<ActionDescriptor> 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<ActionDescriptor> GetActions()
|
||||
private IReadOnlyList<ActionDescriptor> 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides the currently cached collection of <see cref="ActionDescriptor"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
/// <see cref="ActionDescriptorsCollection.Version"/> 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.
|
||||
/// </remarks>
|
||||
public interface IActionDescriptorsCollectionProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the current cached <see cref="ActionDescriptorsCollection"/>
|
||||
/// </summary>
|
||||
ActionDescriptorsCollection ActionDescriptors { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@
|
|||
<Compile Include="ActionConvention.cs" />
|
||||
<Compile Include="ActionDescriptor.cs" />
|
||||
<Compile Include="ActionDescriptorProviderContext.cs" />
|
||||
<Compile Include="ActionDescriptorsCollection.cs" />
|
||||
<Compile Include="ActionInvokerFactory.cs" />
|
||||
<Compile Include="ActionInvokerProviderContext.cs" />
|
||||
<Compile Include="ActionMethodSelectorAttribute.cs" />
|
||||
|
|
@ -67,6 +68,7 @@
|
|||
<Compile Include="BodyParameterInfo.cs" />
|
||||
<Compile Include="Controller.cs" />
|
||||
<Compile Include="ControllerDescriptor.cs" />
|
||||
<Compile Include="DefaultActionDescriptorsCollectionProvider.cs" />
|
||||
<Compile Include="DefaultActionDiscoveryConventions.cs" />
|
||||
<Compile Include="DefaultActionSelector.cs" />
|
||||
<Compile Include="DefaultControllerAssemblyProvider.cs" />
|
||||
|
|
@ -122,6 +124,7 @@
|
|||
<Compile Include="HttpPutAttribute.cs" />
|
||||
<Compile Include="IActionConstraint.cs" />
|
||||
<Compile Include="IActionDescriptorProvider.cs" />
|
||||
<Compile Include="IActionDescriptorsCollectionProvider.cs" />
|
||||
<Compile Include="IActionDiscoveryConventions.cs" />
|
||||
<Compile Include="IActionInvoker.cs" />
|
||||
<Compile Include="IActionInvokerFactory.cs" />
|
||||
|
|
|
|||
|
|
@ -25,4 +25,4 @@
|
|||
<Compile Include="MvcServices.cs" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
@ -48,6 +48,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
ReflectedRouteConstraintsActionDescriptorProvider>();
|
||||
yield return describe.Transient<INestedProvider<ActionInvokerProviderContext>,
|
||||
ReflectedActionInvokerProvider>();
|
||||
yield return describe.Singleton<IActionDescriptorsCollectionProvider, DefaultActionDescriptorsCollectionProvider>();
|
||||
|
||||
yield return describe.Transient<IModelMetadataProvider, DataAnnotationsModelMetadataProvider>();
|
||||
yield return describe.Transient<IActionBindingContextProvider, DefaultActionBindingContextProvider>();
|
||||
|
|
|
|||
|
|
@ -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<ActionDescriptor> InvokeActionSelector(RouteContext context, DefaultActionDiscoveryConventions actionDiscoveryConventions)
|
||||
private async Task<ActionDescriptor> InvokeActionSelector(RouteContext context,
|
||||
DefaultActionDiscoveryConventions actionDiscoveryConventions)
|
||||
{
|
||||
var actionDescriptorProvider = GetActionDescriptorProvider(actionDiscoveryConventions);
|
||||
var descriptorProvider =
|
||||
new NestedProviderManager<ActionDescriptorProviderContext>(new[] { actionDescriptorProvider });
|
||||
|
||||
var serviceContainer = new ServiceContainer();
|
||||
serviceContainer.AddService(typeof(INestedProviderManager<ActionDescriptorProviderContext>),
|
||||
descriptorProvider);
|
||||
|
||||
var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
|
||||
|
||||
var bindingProvider = new Mock<IActionBindingContextProvider>();
|
||||
|
||||
var defaultActionSelector = new DefaultActionSelector(descriptorProvider, bindingProvider.Object);
|
||||
var defaultActionSelector = new DefaultActionSelector(actionCollectionDescriptorProvider,
|
||||
bindingProvider.Object);
|
||||
|
||||
return await defaultActionSelector.SelectAsync(context);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ActionDescriptor> InvokeActionSelector(RouteContext context, DefaultActionDiscoveryConventions actionDiscoveryConventions)
|
||||
private async Task<ActionDescriptor> InvokeActionSelector(RouteContext context,
|
||||
DefaultActionDiscoveryConventions actionDiscoveryConventions)
|
||||
{
|
||||
var actionDescriptorProvider = GetActionDescriptorProvider(actionDiscoveryConventions);
|
||||
var descriptorProvider =
|
||||
new NestedProviderManager<ActionDescriptorProviderContext>(new[] { actionDescriptorProvider });
|
||||
|
||||
var serviceContainer = new ServiceContainer();
|
||||
serviceContainer.AddService(typeof(INestedProviderManager<ActionDescriptorProviderContext>),
|
||||
descriptorProvider);
|
||||
|
||||
var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
|
||||
|
||||
var bindingProvider = new Mock<IActionBindingContextProvider>();
|
||||
|
||||
var defaultActionSelector = new DefaultActionSelector(descriptorProvider, bindingProvider.Object);
|
||||
var defaultActionSelector = new DefaultActionSelector(actionCollectionDescriptorProvider,
|
||||
bindingProvider.Object);
|
||||
|
||||
return await defaultActionSelector.SelectAsync(context);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ActionDescriptor> actions)
|
||||
private static DefaultActionSelector CreateSelector(IReadOnlyList<ActionDescriptor> actions)
|
||||
{
|
||||
var actionProvider = new Mock<INestedProviderManager<ActionDescriptorProviderContext>>(MockBehavior.Strict);
|
||||
var actionProvider = new Mock<IActionDescriptorsCollectionProvider>(MockBehavior.Strict);
|
||||
|
||||
actionProvider
|
||||
.Setup(p => p.Invoke(It.IsAny<ActionDescriptorProviderContext>()))
|
||||
.Callback<ActionDescriptorProviderContext>(c => c.Results.AddRange(actions));
|
||||
.Setup(p => p.ActionDescriptors).Returns(new ActionDescriptorsCollection(actions, 0));
|
||||
|
||||
var bindingProvider = new Mock<IActionBindingContextProvider>(MockBehavior.Strict);
|
||||
bindingProvider
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@
|
|||
<DevelopmentServerPort>38820</DevelopmentServerPort>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="ActionDescriptorCreationCounter.cs" />
|
||||
<Compile Include="Controllers\HomeController.cs" />
|
||||
<Compile Include="Startup.cs" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,6 @@ namespace BasicWebSite.Controllers
|
|||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ActionDescriptorProviderContext> counterService)
|
||||
{
|
||||
_counterService = (ActionDescriptorCreationCounter)counterService;
|
||||
}
|
||||
|
||||
public IActionResult CountActionDescriptorInvocations()
|
||||
{
|
||||
return Content(_counterService.CallCount.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<INestedProvider<ActionDescriptorProviderContext>, 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" });
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue