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