parent
177fb2a6b1
commit
c5a5ba1fee
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a way to signal invalidation of the cached collection of <see cref="Abstractions.ActionDescriptor" /> from an
|
||||
/// <see cref="IActionDescriptorCollectionProvider"/>.
|
||||
/// </summary>
|
||||
public interface IActionDescriptorChangeProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a <see cref="IChangeToken"/> used to signal invalidation of cached <see cref="Abstractions.ActionDescriptor"/>
|
||||
/// instances.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IChangeToken"/>.</returns>
|
||||
IChangeToken GetChangeToken();
|
||||
}
|
||||
}
|
||||
|
|
@ -7,10 +7,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
/// Provides the currently cached collection of <see cref="Abstractions.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="ActionDescriptorCollection.Version"/> in a thread safe way.
|
||||
/// The default implementation internally caches the collection and uses
|
||||
/// <see cref="IActionDescriptorChangeProvider"/> to invalidate this cache, incrementing
|
||||
/// <see cref="ActionDescriptorCollection.Version"/> 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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IActionDescriptorCollectionProvider"/>.
|
||||
/// This implementation caches the results at first call, and is not responsible for updates.
|
||||
/// </summary>
|
||||
public class ActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IActionDescriptorProvider[] _actionDescriptorProviders;
|
||||
private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
|
||||
private ActionDescriptorCollection _collection;
|
||||
private int _version = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActionDescriptorCollectionProvider" /> class.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The application IServiceProvider.</param>
|
||||
public ActionDescriptorCollectionProvider(IServiceProvider serviceProvider)
|
||||
/// <param name="actionDescriptorProviders">The sequence of <see cref="IActionDescriptorProvider"/>.</param>
|
||||
/// <param name="actionDescriptorChangeProviders">The sequence of <see cref="IActionDescriptorChangeProvider"/>.</param>
|
||||
public ActionDescriptorCollectionProvider(
|
||||
IEnumerable<IActionDescriptorProvider> actionDescriptorProviders,
|
||||
IEnumerable<IActionDescriptorChangeProvider> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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<IActionDescriptorProvider>()
|
||||
.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<ActionDescriptor>(context.Results), 0);
|
||||
_collection = new ActionDescriptorCollection(
|
||||
new ReadOnlyCollection<ActionDescriptor>(context.Results),
|
||||
Interlocked.Increment(ref _version));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IChangeToken> changeTokens)
|
||||
{
|
||||
if (changeTokens == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(changeTokens));
|
||||
}
|
||||
|
||||
ChangeTokens = changeTokens;
|
||||
}
|
||||
|
||||
public IList<IChangeToken> ChangeTokens { get; }
|
||||
|
||||
public IDisposable RegisterChangeCallback(Action<object> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IActionDescriptorProvider>()
|
||||
{
|
||||
actionDescriptorProvider,
|
||||
};
|
||||
|
||||
serviceContainer.AddSingleton(typeof(IEnumerable<IActionDescriptorProvider>), list);
|
||||
|
||||
var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
|
||||
serviceContainer.BuildServiceProvider());
|
||||
new[] { actionDescriptorProvider },
|
||||
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||
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<IActionDescriptorProvider>(),
|
||||
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||
return new ActionConstraintCache(descriptorProvider, actionConstraintProviders.AsEnumerable() ?? new List<IActionConstraintProvider>());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IActionDescriptorProvider>(),
|
||||
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||
return new ActionConstraintCache(descriptorProvider, providers);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IActionDescriptorChangeProvider>());
|
||||
|
||||
// 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<IActionDescriptorChangeProvider>());
|
||||
|
||||
// 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<ActionDescriptorProviderContext>()), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionDescriptors_UpdateWhenChangeTokenProviderChanges()
|
||||
{
|
||||
// Arrange
|
||||
var actionDescriptorProvider = new Mock<IActionDescriptorProvider>();
|
||||
var expected1 = new ActionDescriptor();
|
||||
var expected2 = new ActionDescriptor();
|
||||
|
||||
var invocations = 0;
|
||||
actionDescriptorProvider
|
||||
.Setup(p => p.OnProvidersExecuting(It.IsAny<ActionDescriptorProviderContext>()))
|
||||
.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<IActionDescriptorProvider>();
|
||||
var expected1 = new ActionDescriptor();
|
||||
var expected2 = new ActionDescriptor();
|
||||
var expected3 = new ActionDescriptor();
|
||||
|
||||
var invocations = 0;
|
||||
actionDescriptorProvider
|
||||
.Setup(p => p.OnProvidersExecuting(It.IsAny<ActionDescriptorProviderContext>()))
|
||||
.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<IActionDescriptorProvider>();
|
||||
actionDescriptorProvider
|
||||
.Setup(p => p.OnProvidersExecuting(It.IsAny<ActionDescriptorProviderContext>()))
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IActionDescriptorProvider>(),
|
||||
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||
return new ControllerActionInvokerCache(descriptorProvider, filterProviders.AsEnumerable() ?? new List<IFilterProvider>());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IActionDescriptorProvider>(),
|
||||
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||
return new ControllerActionInvokerCache(
|
||||
descriptorProvider,
|
||||
filterProviders.AsEnumerable() ?? new List<IFilterProvider>());
|
||||
|
|
|
|||
|
|
@ -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<ActionDescriptorProviderContext>()))
|
||||
.Verifiable();
|
||||
|
||||
var descriptorCollectionProvider = new ActionDescriptorCollectionProvider(
|
||||
new[] { actionProvider.Object },
|
||||
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||
|
||||
var context = new Mock<HttpContext>();
|
||||
context.Setup(o => o.RequestServices
|
||||
.GetService(typeof(IEnumerable<IActionDescriptorProvider>)))
|
||||
.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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<List<ApiExplorerData>>(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<List<ApiExplorerData>>(body);
|
||||
|
||||
// Assert - 4
|
||||
description = Assert.Single(result);
|
||||
Assert.Empty(description.ParameterDescriptions);
|
||||
Assert.Equal("ApiExplorerReload/NewIndex", description.RelativePath);
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetSortedMediaTypes(ApiExplorerResponseType apiResponseType)
|
||||
{
|
||||
return apiResponseType.ResponseFormats
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ApiExplorerData>();
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ApiExplorerDataFilter>();
|
||||
services.AddSingleton<IActionDescriptorChangeProvider>(ActionDescriptorChangeProvider.Instance);
|
||||
services.AddSingleton(ActionDescriptorChangeProvider.Instance);
|
||||
}
|
||||
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseCultureReplacer();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
|
||||
|
||||
namespace ApplicationModelWebSite
|
||||
{
|
||||
public class ControllerDescriptionAttribute : Attribute, IControllerModelConvention
|
||||
|
|
|
|||
Loading…
Reference in New Issue