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"/>.
|
/// Provides the currently cached collection of <see cref="Abstractions.ActionDescriptor"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The default implementation, does not update the cache, it is up to the user
|
/// The default implementation internally caches the collection and uses
|
||||||
/// to create or use an implementation that can update the available actions in
|
/// <see cref="IActionDescriptorChangeProvider"/> to invalidate this cache, incrementing
|
||||||
/// the application. The implementor is also responsible for updating the
|
/// <see cref="ActionDescriptorCollection.Version"/> the collection is reconstructed.
|
||||||
/// <see cref="ActionDescriptorCollection.Version"/> in a thread safe way.
|
|
||||||
///
|
///
|
||||||
/// Default consumers of this service, are aware of the version and will recache
|
/// Default consumers of this service, are aware of the version and will recache
|
||||||
/// data as appropriate, but rely on the version being unique.
|
/// data as appropriate, but rely on the version being unique.
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,55 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default implementation of <see cref="IActionDescriptorCollectionProvider"/>.
|
/// Default implementation of <see cref="IActionDescriptorCollectionProvider"/>.
|
||||||
/// This implementation caches the results at first call, and is not responsible for updates.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
|
public class ActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IActionDescriptorProvider[] _actionDescriptorProviders;
|
||||||
|
private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
|
||||||
private ActionDescriptorCollection _collection;
|
private ActionDescriptorCollection _collection;
|
||||||
|
private int _version = -1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ActionDescriptorCollectionProvider" /> class.
|
/// Initializes a new instance of the <see cref="ActionDescriptorCollectionProvider" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serviceProvider">The application IServiceProvider.</param>
|
/// <param name="actionDescriptorProviders">The sequence of <see cref="IActionDescriptorProvider"/>.</param>
|
||||||
public ActionDescriptorCollectionProvider(IServiceProvider serviceProvider)
|
/// <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>
|
/// <summary>
|
||||||
|
|
@ -37,34 +61,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
if (_collection == null)
|
if (_collection == null)
|
||||||
{
|
{
|
||||||
_collection = GetCollection();
|
UpdateCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _collection;
|
return _collection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ActionDescriptorCollection GetCollection()
|
private void UpdateCollection()
|
||||||
{
|
{
|
||||||
var providers =
|
|
||||||
_serviceProvider.GetServices<IActionDescriptorProvider>()
|
|
||||||
.OrderBy(p => p.Order)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var context = new ActionDescriptorProviderContext();
|
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(
|
_collection = new ActionDescriptorCollection(
|
||||||
new ReadOnlyCollection<ActionDescriptor>(context.Results), 0);
|
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)
|
private ControllerActionDescriptor InvokeActionSelector(RouteContext context)
|
||||||
{
|
{
|
||||||
var actionDescriptorProvider = GetActionDescriptorProvider();
|
var actionDescriptorProvider = GetActionDescriptorProvider();
|
||||||
|
|
||||||
var serviceContainer = new ServiceCollection();
|
|
||||||
var list = new List<IActionDescriptorProvider>()
|
|
||||||
{
|
|
||||||
actionDescriptorProvider,
|
|
||||||
};
|
|
||||||
|
|
||||||
serviceContainer.AddSingleton(typeof(IEnumerable<IActionDescriptorProvider>), list);
|
|
||||||
|
|
||||||
var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
|
var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
|
||||||
serviceContainer.BuildServiceProvider());
|
new[] { actionDescriptorProvider },
|
||||||
|
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||||
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionDescriptorCollectionProvider);
|
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionDescriptorCollectionProvider);
|
||||||
|
|
||||||
var actionConstraintProviders = new[]
|
var actionConstraintProviders = new[]
|
||||||
|
|
@ -827,8 +819,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
|
||||||
private static ActionConstraintCache GetActionConstraintCache(IActionConstraintProvider[] actionConstraintProviders = null)
|
private static ActionConstraintCache GetActionConstraintCache(IActionConstraintProvider[] actionConstraintProviders = null)
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection().BuildServiceProvider();
|
var descriptorProvider = new ActionDescriptorCollectionProvider(
|
||||||
var descriptorProvider = new ActionDescriptorCollectionProvider(services);
|
Enumerable.Empty<IActionDescriptorProvider>(),
|
||||||
|
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||||
return new ActionConstraintCache(descriptorProvider, actionConstraintProviders.AsEnumerable() ?? new List<IActionConstraintProvider>());
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
||||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -156,8 +159,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
private static ActionConstraintCache CreateCache(params IActionConstraintProvider[] providers)
|
private static ActionConstraintCache CreateCache(params IActionConstraintProvider[] providers)
|
||||||
{
|
{
|
||||||
var services = CreateServices();
|
var descriptorProvider = new ActionDescriptorCollectionProvider(
|
||||||
var descriptorProvider = new ActionDescriptorCollectionProvider(services);
|
Enumerable.Empty<IActionDescriptorProvider>(),
|
||||||
|
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||||
return new ActionConstraintCache(descriptorProvider, providers);
|
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.Controllers;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
@ -3300,8 +3301,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
private static ControllerActionInvokerCache CreateFilterCache(IFilterProvider[] filterProviders = null)
|
private static ControllerActionInvokerCache CreateFilterCache(IFilterProvider[] filterProviders = null)
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection().BuildServiceProvider();
|
var descriptorProvider = new ActionDescriptorCollectionProvider(
|
||||||
var descriptorProvider = new ActionDescriptorCollectionProvider(services);
|
Enumerable.Empty<IActionDescriptorProvider>(),
|
||||||
|
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||||
return new ControllerActionInvokerCache(descriptorProvider, filterProviders.AsEnumerable() ?? new List<IFilterProvider>());
|
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.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
@ -400,8 +401,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
private static ControllerActionInvokerCache CreateFilterCache(IFilterProvider[] filterProviders = null)
|
private static ControllerActionInvokerCache CreateFilterCache(IFilterProvider[] filterProviders = null)
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection().BuildServiceProvider();
|
var descriptorProvider = new ActionDescriptorCollectionProvider(
|
||||||
var descriptorProvider = new ActionDescriptorCollectionProvider(services);
|
Enumerable.Empty<IActionDescriptorProvider>(),
|
||||||
|
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||||
return new ControllerActionInvokerCache(
|
return new ControllerActionInvokerCache(
|
||||||
descriptorProvider,
|
descriptorProvider,
|
||||||
filterProviders.AsEnumerable() ?? new List<IFilterProvider>());
|
filterProviders.AsEnumerable() ?? new List<IFilterProvider>());
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||||
|
|
@ -172,14 +173,14 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
.Setup(p => p.OnProvidersExecuted(It.IsAny<ActionDescriptorProviderContext>()))
|
.Setup(p => p.OnProvidersExecuted(It.IsAny<ActionDescriptorProviderContext>()))
|
||||||
.Verifiable();
|
.Verifiable();
|
||||||
|
|
||||||
|
var descriptorCollectionProvider = new ActionDescriptorCollectionProvider(
|
||||||
|
new[] { actionProvider.Object },
|
||||||
|
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||||
|
|
||||||
var context = new Mock<HttpContext>();
|
var context = new Mock<HttpContext>();
|
||||||
context.Setup(o => o.RequestServices
|
context.Setup(o => o.RequestServices
|
||||||
.GetService(typeof(IEnumerable<IActionDescriptorProvider>)))
|
.GetService(typeof(IActionDescriptorCollectionProvider)))
|
||||||
.Returns(new[] { actionProvider.Object });
|
.Returns(descriptorCollectionProvider);
|
||||||
|
|
||||||
context.Setup(o => o.RequestServices
|
|
||||||
.GetService(typeof(IActionDescriptorCollectionProvider)))
|
|
||||||
.Returns(new ActionDescriptorCollectionProvider(context.Object.RequestServices));
|
|
||||||
return context.Object;
|
return context.Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNetCore.Testing.xunit;
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
{
|
{
|
||||||
|
|
@ -976,6 +978,40 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal(typeof(string).FullName, feedback.Type);
|
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)
|
private IEnumerable<string> GetSortedMediaTypes(ApiExplorerResponseType apiResponseType)
|
||||||
{
|
{
|
||||||
return apiResponseType.ResponseFormats
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
namespace ApiExplorerWebSite
|
namespace ApiExplorerWebSite
|
||||||
|
|
@ -26,6 +28,12 @@ namespace ApiExplorerWebSite
|
||||||
|
|
||||||
public void OnResourceExecuting(ResourceExecutingContext context)
|
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>();
|
var descriptions = new List<ApiExplorerData>();
|
||||||
foreach (var group in _descriptionProvider.ApiDescriptionGroups.Items)
|
foreach (var group in _descriptionProvider.ApiDescriptionGroups.Items)
|
||||||
{
|
{
|
||||||
|
|
@ -43,7 +51,6 @@ namespace ApiExplorerWebSite
|
||||||
|
|
||||||
public void OnResourceExecuted(ResourceExecutedContext context)
|
public void OnResourceExecuted(ResourceExecutedContext context)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ApiExplorerData CreateSerializableData(ApiDescription description)
|
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.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|
@ -33,9 +34,10 @@ namespace ApiExplorerWebSite
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddSingleton<ApiExplorerDataFilter>();
|
services.AddSingleton<ApiExplorerDataFilter>();
|
||||||
|
services.AddSingleton<IActionDescriptorChangeProvider>(ActionDescriptorChangeProvider.Instance);
|
||||||
|
services.AddSingleton(ActionDescriptorChangeProvider.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Configure(IApplicationBuilder app)
|
public void Configure(IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
app.UseCultureReplacer();
|
app.UseCultureReplacer();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
|
|
||||||
|
|
||||||
namespace ApplicationModelWebSite
|
namespace ApplicationModelWebSite
|
||||||
{
|
{
|
||||||
public class ControllerDescriptionAttribute : Attribute, IControllerModelConvention
|
public class ControllerDescriptionAttribute : Attribute, IControllerModelConvention
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue