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:
Yishai Galatzer 2014-06-05 18:30:36 -07:00
parent 31d3180635
commit 6d78f8adb3
16 changed files with 266 additions and 21 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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" />

View File

@ -25,4 +25,4 @@
<Compile Include="MvcServices.cs" />
</ItemGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>
</Project>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,7 @@
<DevelopmentServerPort>38820</DevelopmentServerPort>
</PropertyGroup>
<ItemGroup>
<Compile Include="ActionDescriptorCreationCounter.cs" />
<Compile Include="Controllers\HomeController.cs" />
<Compile Include="Startup.cs" />
</ItemGroup>

View File

@ -7,6 +7,6 @@ namespace BasicWebSite.Controllers
public IActionResult Index()
{
return View();
}
}
}
}

View File

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

View File

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