diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IActionDescriptorChangeProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IActionDescriptorChangeProvider.cs
new file mode 100644
index 0000000000..d736512f1c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IActionDescriptorChangeProvider.cs
@@ -0,0 +1,21 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.Mvc.Infrastructure
+{
+ ///
+ /// Provides a way to signal invalidation of the cached collection of from an
+ /// .
+ ///
+ public interface IActionDescriptorChangeProvider
+ {
+ ///
+ /// Gets a used to signal invalidation of cached
+ /// instances.
+ ///
+ /// The .
+ IChangeToken GetChangeToken();
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IActionDescriptorCollectionProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IActionDescriptorCollectionProvider.cs
index 2e4cd492e6..3e64a66677 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IActionDescriptorCollectionProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IActionDescriptorCollectionProvider.cs
@@ -7,10 +7,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
/// 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.
+ /// The default implementation internally caches the collection and uses
+ /// to invalidate this cache, incrementing
+ /// the collection is reconstructed.
///
/// Default consumers of this service, are aware of the version and will recache
/// data as appropriate, but rely on the version being unique.
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionDescriptorCollectionProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionDescriptorCollectionProvider.cs
index 448b0caaae..1090a11a97 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionDescriptorCollectionProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionDescriptorCollectionProvider.cs
@@ -1,31 +1,55 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
+using System.Threading;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.Internal
{
///
/// Default implementation of .
- /// This implementation caches the results at first call, and is not responsible for updates.
///
public class ActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
{
- private readonly IServiceProvider _serviceProvider;
+ private readonly IActionDescriptorProvider[] _actionDescriptorProviders;
+ private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
private ActionDescriptorCollection _collection;
+ private int _version = -1;
///
/// Initializes a new instance of the class.
///
- /// The application IServiceProvider.
- public ActionDescriptorCollectionProvider(IServiceProvider serviceProvider)
+ /// The sequence of .
+ /// The sequence of .
+ public ActionDescriptorCollectionProvider(
+ IEnumerable actionDescriptorProviders,
+ IEnumerable actionDescriptorChangeProviders)
{
- _serviceProvider = serviceProvider;
+ _actionDescriptorProviders = actionDescriptorProviders
+ .OrderBy(p => p.Order)
+ .ToArray();
+
+ _actionDescriptorChangeProviders = actionDescriptorChangeProviders.ToArray();
+
+ ChangeToken.OnChange(
+ GetCompositeChangeToken,
+ UpdateCollection);
+ }
+
+ private IChangeToken GetCompositeChangeToken()
+ {
+ var changeTokens = new IChangeToken[_actionDescriptorChangeProviders.Length];
+ for (var i = 0; i < _actionDescriptorChangeProviders.Length; i++)
+ {
+ changeTokens[i] = _actionDescriptorChangeProviders[i].GetChangeToken();
+ }
+
+ return new CompositeChangeToken(changeTokens);
}
///
@@ -37,34 +61,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
if (_collection == null)
{
- _collection = GetCollection();
+ UpdateCollection();
}
return _collection;
}
}
- private ActionDescriptorCollection GetCollection()
+ private void UpdateCollection()
{
- var providers =
- _serviceProvider.GetServices()
- .OrderBy(p => p.Order)
- .ToArray();
-
var context = new ActionDescriptorProviderContext();
- foreach (var provider in providers)
+ for (var i = 0; i < _actionDescriptorProviders.Length; i++)
{
- provider.OnProvidersExecuting(context);
+ _actionDescriptorProviders[i].OnProvidersExecuting(context);
}
- for (var i = providers.Length - 1; i >= 0; i--)
+ for (var i = _actionDescriptorProviders.Length - 1; i >= 0; i--)
{
- providers[i].OnProvidersExecuted(context);
+ _actionDescriptorProviders[i].OnProvidersExecuted(context);
}
- return new ActionDescriptorCollection(
- new ReadOnlyCollection(context.Results), 0);
+ _collection = new ActionDescriptorCollection(
+ new ReadOnlyCollection(context.Results),
+ Interlocked.Increment(ref _version));
}
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/CompositeChangeToken.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/CompositeChangeToken.cs
new file mode 100644
index 0000000000..6c24d95720
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/CompositeChangeToken.cs
@@ -0,0 +1,85 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.Mvc.Internal
+{
+ internal class CompositeChangeToken : IChangeToken
+ {
+ public CompositeChangeToken(IList changeTokens)
+ {
+ if (changeTokens == null)
+ {
+ throw new ArgumentNullException(nameof(changeTokens));
+ }
+
+ ChangeTokens = changeTokens;
+ }
+
+ public IList ChangeTokens { get; }
+
+ public IDisposable RegisterChangeCallback(Action callback, object state)
+ {
+ var disposables = new IDisposable[ChangeTokens.Count];
+ for (var i = 0; i < ChangeTokens.Count; i++)
+ {
+ var disposable = ChangeTokens[i].RegisterChangeCallback(callback, state);
+ disposables[i] = disposable;
+ }
+ return new CompositeDisposable(disposables);
+ }
+
+ public bool HasChanged
+ {
+ get
+ {
+ for (var i = 0; i < ChangeTokens.Count; i++)
+ {
+ if (ChangeTokens[i].HasChanged)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ public bool ActiveChangeCallbacks
+ {
+ get
+ {
+ for (var i = 0; i < ChangeTokens.Count; i++)
+ {
+ if (ChangeTokens[i].ActiveChangeCallbacks)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ private class CompositeDisposable : IDisposable
+ {
+ private readonly IDisposable[] _disposables;
+
+ public CompositeDisposable(IDisposable[] disposables)
+ {
+ _disposables = disposables;
+ }
+
+ public void Dispose()
+ {
+ for (var i = 0; i < _disposables.Length; i++)
+ {
+ _disposables[i].Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/DefaultActionSelectorTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/DefaultActionSelectorTests.cs
index 71ffc0f7f6..19908e9f1a 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/DefaultActionSelectorTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/DefaultActionSelectorTests.cs
@@ -657,17 +657,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
private ControllerActionDescriptor InvokeActionSelector(RouteContext context)
{
var actionDescriptorProvider = GetActionDescriptorProvider();
-
- var serviceContainer = new ServiceCollection();
- var list = new List()
- {
- actionDescriptorProvider,
- };
-
- serviceContainer.AddSingleton(typeof(IEnumerable), list);
-
var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
- serviceContainer.BuildServiceProvider());
+ new[] { actionDescriptorProvider },
+ Enumerable.Empty());
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionDescriptorCollectionProvider);
var actionConstraintProviders = new[]
@@ -827,8 +819,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
private static ActionConstraintCache GetActionConstraintCache(IActionConstraintProvider[] actionConstraintProviders = null)
{
- var services = new ServiceCollection().BuildServiceProvider();
- var descriptorProvider = new ActionDescriptorCollectionProvider(services);
+ var descriptorProvider = new ActionDescriptorCollectionProvider(
+ Enumerable.Empty(),
+ Enumerable.Empty());
return new ActionConstraintCache(descriptorProvider, actionConstraintProviders.AsEnumerable() ?? new List());
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionConstraintCacheTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionConstraintCacheTest.cs
index d76f0272bb..0187f81666 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionConstraintCacheTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionConstraintCacheTest.cs
@@ -2,9 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Linq;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
@@ -156,8 +159,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private static ActionConstraintCache CreateCache(params IActionConstraintProvider[] providers)
{
- var services = CreateServices();
- var descriptorProvider = new ActionDescriptorCollectionProvider(services);
+ var descriptorProvider = new ActionDescriptorCollectionProvider(
+ Enumerable.Empty(),
+ Enumerable.Empty());
return new ActionConstraintCache(descriptorProvider, providers);
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionDescriptorCollectionProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionDescriptorCollectionProviderTest.cs
new file mode 100644
index 0000000000..0c599b47c6
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionDescriptorCollectionProviderTest.cs
@@ -0,0 +1,206 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Linq;
+using System.Threading;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.Primitives;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.Internal
+{
+ public class ActionDescriptorCollectionProviderTest
+ {
+ [Fact]
+ public void ActionDescriptors_ReadsDescriptorsFromActionDescriptorProviders()
+ {
+ // Arrange
+ var expected1 = new ActionDescriptor();
+ var actionDescriptorProvider1 = GetActionDescriptorProvider(expected1);
+
+ var expected2 = new ActionDescriptor();
+ var expected3 = new ActionDescriptor();
+ var actionDescriptorProvider2 = GetActionDescriptorProvider(expected2, expected3);
+
+ var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
+ new[] { actionDescriptorProvider1, actionDescriptorProvider2 },
+ Enumerable.Empty());
+
+ // Act
+ var collection = actionDescriptorCollectionProvider.ActionDescriptors;
+
+ // Assert
+ Assert.Equal(0, collection.Version);
+ Assert.Collection(
+ collection.Items,
+ descriptor => Assert.Same(expected1, descriptor),
+ descriptor => Assert.Same(expected2, descriptor),
+ descriptor => Assert.Same(expected3, descriptor));
+ }
+
+ [Fact]
+ public void ActionDescriptors_CachesValuesByDefault()
+ {
+ // Arrange
+ var actionDescriptorProvider = GetActionDescriptorProvider(new ActionDescriptor());
+
+ var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
+ new[] { actionDescriptorProvider },
+ Enumerable.Empty());
+
+ // Act - 1
+ var collection1 = actionDescriptorCollectionProvider.ActionDescriptors;
+
+ // Assert - 1
+ Assert.Equal(0, collection1.Version);
+
+ // Act - 2
+ var collection2 = actionDescriptorCollectionProvider.ActionDescriptors;
+
+ // Assert - 2
+ Assert.Same(collection1, collection2);
+ Mock.Get(actionDescriptorProvider)
+ .Verify(v => v.OnProvidersExecuting(It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public void ActionDescriptors_UpdateWhenChangeTokenProviderChanges()
+ {
+ // Arrange
+ var actionDescriptorProvider = new Mock();
+ var expected1 = new ActionDescriptor();
+ var expected2 = new ActionDescriptor();
+
+ var invocations = 0;
+ actionDescriptorProvider
+ .Setup(p => p.OnProvidersExecuting(It.IsAny()))
+ .Callback((ActionDescriptorProviderContext context) =>
+ {
+ if (invocations == 0)
+ {
+ context.Results.Add(expected1);
+ }
+ else
+ {
+ context.Results.Add(expected2);
+ }
+
+ invocations++;
+ });
+ var changeProvider = new TestChangeProvider();
+ var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
+ new[] { actionDescriptorProvider.Object },
+ new[] { changeProvider });
+
+ // Act - 1
+ var collection1 = actionDescriptorCollectionProvider.ActionDescriptors;
+
+ // Assert - 1
+ Assert.Equal(0, collection1.Version);
+ Assert.Collection(collection1.Items,
+ item => Assert.Same(expected1, item));
+
+ // Act - 2
+ changeProvider.TokenSource.Cancel();
+ var collection2 = actionDescriptorCollectionProvider.ActionDescriptors;
+
+ // Assert - 2
+ Assert.NotSame(collection1, collection2);
+ Assert.Equal(1, collection2.Version);
+ Assert.Collection(collection2.Items,
+ item => Assert.Same(expected2, item));
+ }
+
+ [Fact]
+ public void ActionDescriptors_SubscribesToNewChangeNotificationsAfterInvalidating()
+ {
+ // Arrange
+ var actionDescriptorProvider = new Mock();
+ var expected1 = new ActionDescriptor();
+ var expected2 = new ActionDescriptor();
+ var expected3 = new ActionDescriptor();
+
+ var invocations = 0;
+ actionDescriptorProvider
+ .Setup(p => p.OnProvidersExecuting(It.IsAny()))
+ .Callback((ActionDescriptorProviderContext context) =>
+ {
+ if (invocations == 0)
+ {
+ context.Results.Add(expected1);
+ }
+ else if (invocations == 1)
+ {
+ context.Results.Add(expected2);
+ }
+ else
+ {
+ context.Results.Add(expected3);
+ }
+
+ invocations++;
+ });
+ var changeProvider = new TestChangeProvider();
+ var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
+ new[] { actionDescriptorProvider.Object },
+ new[] { changeProvider });
+
+ // Act - 1
+ var collection1 = actionDescriptorCollectionProvider.ActionDescriptors;
+
+ // Assert - 1
+ Assert.Equal(0, collection1.Version);
+ Assert.Collection(collection1.Items,
+ item => Assert.Same(expected1, item));
+
+ // Act - 2
+ changeProvider.TokenSource.Cancel();
+ var collection2 = actionDescriptorCollectionProvider.ActionDescriptors;
+
+ // Assert - 2
+ Assert.NotSame(collection1, collection2);
+ Assert.Equal(1, collection2.Version);
+ Assert.Collection(collection2.Items,
+ item => Assert.Same(expected2, item));
+
+ // Act - 3
+ changeProvider.TokenSource.Cancel();
+ var collection3 = actionDescriptorCollectionProvider.ActionDescriptors;
+
+ // Assert - 3
+ Assert.NotSame(collection2, collection3);
+ Assert.Equal(2, collection3.Version);
+ Assert.Collection(collection3.Items,
+ item => Assert.Same(expected3, item));
+ }
+
+ private static IActionDescriptorProvider GetActionDescriptorProvider(params ActionDescriptor[] values)
+ {
+ var actionDescriptorProvider = new Mock();
+ actionDescriptorProvider
+ .Setup(p => p.OnProvidersExecuting(It.IsAny()))
+ .Callback((ActionDescriptorProviderContext context) =>
+ {
+ foreach (var value in values)
+ {
+ context.Results.Add(value);
+ }
+ });
+
+ return actionDescriptorProvider.Object;
+ }
+
+ private class TestChangeProvider : IActionDescriptorChangeProvider
+ {
+ public CancellationTokenSource TokenSource { get; private set; }
+
+ public IChangeToken GetChangeToken()
+ {
+ TokenSource = new CancellationTokenSource();
+ return new CancellationChangeToken(TokenSource.Token);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs
index 52c050cbde..7f3de8ceaa 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs
@@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Routing;
@@ -3300,8 +3301,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private static ControllerActionInvokerCache CreateFilterCache(IFilterProvider[] filterProviders = null)
{
- var services = new ServiceCollection().BuildServiceProvider();
- var descriptorProvider = new ActionDescriptorCollectionProvider(services);
+ var descriptorProvider = new ActionDescriptorCollectionProvider(
+ Enumerable.Empty(),
+ Enumerable.Empty());
return new ControllerActionInvokerCache(descriptorProvider, filterProviders.AsEnumerable() ?? new List());
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs
index 68b63d1153..57f68a122a 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs
@@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
@@ -400,8 +401,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private static ControllerActionInvokerCache CreateFilterCache(IFilterProvider[] filterProviders = null)
{
- var services = new ServiceCollection().BuildServiceProvider();
- var descriptorProvider = new ActionDescriptorCollectionProvider(services);
+ var descriptorProvider = new ActionDescriptorCollectionProvider(
+ Enumerable.Empty(),
+ Enumerable.Empty());
return new ControllerActionInvokerCache(
descriptorProvider,
filterProviders.AsEnumerable() ?? new List());
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/KnownRouteValueConstraintTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/KnownRouteValueConstraintTests.cs
index 4e7aed2089..e6ee1638f6 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/KnownRouteValueConstraintTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/KnownRouteValueConstraintTests.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
@@ -172,14 +173,14 @@ namespace Microsoft.AspNetCore.Mvc.Routing
.Setup(p => p.OnProvidersExecuted(It.IsAny()))
.Verifiable();
+ var descriptorCollectionProvider = new ActionDescriptorCollectionProvider(
+ new[] { actionProvider.Object },
+ Enumerable.Empty());
+
var context = new Mock();
context.Setup(o => o.RequestServices
- .GetService(typeof(IEnumerable)))
- .Returns(new[] { actionProvider.Object });
-
- context.Setup(o => o.RequestServices
- .GetService(typeof(IActionDescriptorCollectionProvider)))
- .Returns(new ActionDescriptorCollectionProvider(context.Object.RequestServices));
+ .GetService(typeof(IActionDescriptorCollectionProvider)))
+ .Returns(descriptorCollectionProvider);
return context.Object;
}
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs
index 9836ad3a77..82915a8a26 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs
@@ -10,6 +10,8 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Testing.xunit;
using Newtonsoft.Json;
using Xunit;
+using Microsoft.AspNetCore.Http;
+using System.Net;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
@@ -976,6 +978,40 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(typeof(string).FullName, feedback.Type);
}
+ [Fact]
+ public async Task ApiExplorer_Updates_WhenActionDescriptorCollectionIsUpdated()
+ {
+ // Act - 1
+ var body = await Client.GetStringAsync("ApiExplorerReload/Index");
+ var result = JsonConvert.DeserializeObject>(body);
+
+ // Assert - 1
+ var description = Assert.Single(result);
+ Assert.Empty(description.ParameterDescriptions);
+ Assert.Equal("ApiExplorerReload/Index", description.RelativePath);
+
+ // Act - 2
+ var response = await Client.GetAsync("ApiExplorerReload/Reload");
+
+ // Assert - 2
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ // Act - 3
+ response = await Client.GetAsync("ApiExplorerReload/Index");
+
+ // Assert - 3
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+
+ // Act - 4
+ body = await Client.GetStringAsync("ApiExplorerReload/NewIndex");
+ result = JsonConvert.DeserializeObject>(body);
+
+ // Assert - 4
+ description = Assert.Single(result);
+ Assert.Empty(description.ParameterDescriptions);
+ Assert.Equal("ApiExplorerReload/NewIndex", description.RelativePath);
+ }
+
private IEnumerable GetSortedMediaTypes(ApiExplorerResponseType apiResponseType)
{
return apiResponseType.ResponseFormats
diff --git a/test/WebSites/ApiExplorerWebSite/ActionDescriptorChangeProvider.cs b/test/WebSites/ApiExplorerWebSite/ActionDescriptorChangeProvider.cs
new file mode 100644
index 0000000000..abc1b3b8b3
--- /dev/null
+++ b/test/WebSites/ApiExplorerWebSite/ActionDescriptorChangeProvider.cs
@@ -0,0 +1,28 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Threading;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.Primitives;
+
+namespace ApiExplorerWebSite
+{
+ public class ActionDescriptorChangeProvider : IActionDescriptorChangeProvider
+ {
+ private ActionDescriptorChangeProvider()
+ {
+ }
+
+ public static ActionDescriptorChangeProvider Instance { get; } = new ActionDescriptorChangeProvider();
+
+ public CancellationTokenSource TokenSource { get; private set; }
+
+ public bool HasChanged { get; set; }
+
+ public IChangeToken GetChangeToken()
+ {
+ TokenSource = new CancellationTokenSource();
+ return new CancellationChangeToken(TokenSource.Token);
+ }
+ }
+}
diff --git a/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs b/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs
index d91179e35c..a028cf3a0d 100644
--- a/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs
+++ b/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs
@@ -4,8 +4,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
namespace ApiExplorerWebSite
@@ -26,6 +28,12 @@ namespace ApiExplorerWebSite
public void OnResourceExecuting(ResourceExecutingContext context)
{
+ var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
+ if (controllerActionDescriptor != null && controllerActionDescriptor.MethodInfo.IsDefined(typeof(PassThruAttribute)))
+ {
+ return;
+ }
+
var descriptions = new List();
foreach (var group in _descriptionProvider.ApiDescriptionGroups.Items)
{
@@ -43,7 +51,6 @@ namespace ApiExplorerWebSite
public void OnResourceExecuted(ResourceExecutedContext context)
{
- throw new NotImplementedException();
}
private ApiExplorerData CreateSerializableData(ApiDescription description)
diff --git a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerReloadableController.cs b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerReloadableController.cs
new file mode 100644
index 0000000000..31103ba0f2
--- /dev/null
+++ b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerReloadableController.cs
@@ -0,0 +1,45 @@
+// Copyright (c) .NET Foundation. 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.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ApplicationModels;
+
+namespace ApiExplorerWebSite
+{
+ [Route("ApiExplorerReload")]
+ public class ApiExplorerReloadableController : Controller
+ {
+ [ApiExplorerRouteChangeConvention]
+ [Route("Index")]
+ public string Index() => "Hello world";
+
+ [Route("Reload")]
+ [PassThru]
+ public IActionResult Reload()
+ {
+ ActionDescriptorChangeProvider.Instance.HasChanged = true;
+ ActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
+ return Ok();
+ }
+
+ public class ApiExplorerRouteChangeConventionAttribute : Attribute, IActionModelConvention
+ {
+ public void Apply(ActionModel action)
+ {
+ if (ActionDescriptorChangeProvider.Instance.HasChanged)
+ {
+ action.ActionName = "NewIndex";
+ action.Selectors.Clear();
+ action.Selectors.Add(new SelectorModel
+ {
+ AttributeRouteModel = new AttributeRouteModel
+ {
+ Template = "NewIndex"
+ }
+ });
+ }
+ }
+ }
+ }
+}
diff --git a/test/WebSites/ApiExplorerWebSite/PassThruAttribute.cs b/test/WebSites/ApiExplorerWebSite/PassThruAttribute.cs
new file mode 100644
index 0000000000..fa55ef3f83
--- /dev/null
+++ b/test/WebSites/ApiExplorerWebSite/PassThruAttribute.cs
@@ -0,0 +1,12 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace ApiExplorerWebSite
+{
+ [AttributeUsage(AttributeTargets.Method)]
+ public class PassThruAttribute : Attribute
+ {
+ }
+}
diff --git a/test/WebSites/ApiExplorerWebSite/Startup.cs b/test/WebSites/ApiExplorerWebSite/Startup.cs
index b8535fc74d..f994f315a8 100644
--- a/test/WebSites/ApiExplorerWebSite/Startup.cs
+++ b/test/WebSites/ApiExplorerWebSite/Startup.cs
@@ -6,6 +6,7 @@ using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Formatters;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -33,9 +34,10 @@ namespace ApiExplorerWebSite
});
services.AddSingleton();
+ services.AddSingleton(ActionDescriptorChangeProvider.Instance);
+ services.AddSingleton(ActionDescriptorChangeProvider.Instance);
}
-
public void Configure(IApplicationBuilder app)
{
app.UseCultureReplacer();
diff --git a/test/WebSites/ApplicationModelWebSite/Conventions/ControllerDescriptionAttribute.cs b/test/WebSites/ApplicationModelWebSite/Conventions/ControllerDescriptionAttribute.cs
index f1442b5a07..20109b08ad 100644
--- a/test/WebSites/ApplicationModelWebSite/Conventions/ControllerDescriptionAttribute.cs
+++ b/test/WebSites/ApplicationModelWebSite/Conventions/ControllerDescriptionAttribute.cs
@@ -4,7 +4,6 @@
using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
-
namespace ApplicationModelWebSite
{
public class ControllerDescriptionAttribute : Attribute, IControllerModelConvention