Exposes a separate change token that will be triggered after action descriptors have been updated.
This commit is contained in:
parent
501df09fd6
commit
0fcf2448c3
|
|
@ -111,7 +111,6 @@ namespace Microsoft.AspNetCore.Mvc.Performance
|
||||||
var dataSource = new MvcEndpointDataSource(
|
var dataSource = new MvcEndpointDataSource(
|
||||||
actionDescriptorCollectionProvider,
|
actionDescriptorCollectionProvider,
|
||||||
new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
|
new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
|
||||||
Array.Empty<IActionDescriptorChangeProvider>(),
|
|
||||||
new MockServiceProvider());
|
new MockServiceProvider());
|
||||||
|
|
||||||
return dataSource;
|
return dataSource;
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.TryAddEnumerable(
|
services.TryAddEnumerable(
|
||||||
ServiceDescriptor.Transient<IActionDescriptorProvider, ControllerActionDescriptorProvider>());
|
ServiceDescriptor.Transient<IActionDescriptorProvider, ControllerActionDescriptorProvider>());
|
||||||
|
|
||||||
services.TryAddSingleton<IActionDescriptorCollectionProvider, ActionDescriptorCollectionProvider>();
|
services.TryAddSingleton<IActionDescriptorCollectionProvider, DefaultActionDescriptorCollectionProvider>();
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Selection
|
// Action Selection
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// 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.AspNetCore.Mvc.Abstractions;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A base class for <see cref="IActionDescriptorCollectionProvider"/> which also provides an <see cref="IChangeToken"/>
|
||||||
|
/// for reactive notifications of <see cref="ActionDescriptor"/> changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="ActionDescriptorCollectionProvider"/> is used as a base class by the default implementation of
|
||||||
|
/// <see cref="IActionDescriptorCollectionProvider"/>. To retrieve an instance of <see cref="ActionDescriptorCollectionProvider"/>,
|
||||||
|
/// obtain the <see cref="IActionDescriptorCollectionProvider"/> from the dependency injection provider and
|
||||||
|
/// downcast to <see cref="ActionDescriptorCollectionProvider"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public abstract class ActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the current cached <see cref="ActionDescriptorCollection"/>
|
||||||
|
/// </summary>
|
||||||
|
public abstract ActionDescriptorCollection ActionDescriptors { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an <see cref="IChangeToken"/> that will be signaled after the <see cref="ActionDescriptors"/>
|
||||||
|
/// collection has changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The <see cref="IChangeToken"/>.</returns>
|
||||||
|
public abstract IChangeToken GetChangeToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
{
|
||||||
|
internal class DefaultActionDescriptorCollectionProvider : ActionDescriptorCollectionProvider
|
||||||
|
{
|
||||||
|
private readonly IActionDescriptorProvider[] _actionDescriptorProviders;
|
||||||
|
private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
|
||||||
|
|
||||||
|
// The lock is used to protect WRITES to the following (do not need to protect reads once initialized).
|
||||||
|
private readonly object _lock;
|
||||||
|
private ActionDescriptorCollection _collection;
|
||||||
|
private IChangeToken _changeToken;
|
||||||
|
private CancellationTokenSource _cancellationTokenSource;
|
||||||
|
private int _version = 0;
|
||||||
|
|
||||||
|
public DefaultActionDescriptorCollectionProvider(
|
||||||
|
IEnumerable<IActionDescriptorProvider> actionDescriptorProviders,
|
||||||
|
IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders)
|
||||||
|
{
|
||||||
|
_actionDescriptorProviders = actionDescriptorProviders
|
||||||
|
.OrderBy(p => p.Order)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
_actionDescriptorChangeProviders = actionDescriptorChangeProviders.ToArray();
|
||||||
|
|
||||||
|
ChangeToken.OnChange(
|
||||||
|
GetCompositeChangeToken,
|
||||||
|
UpdateCollection);
|
||||||
|
|
||||||
|
_lock = new object();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a cached collection of <see cref="ActionDescriptor" />.
|
||||||
|
/// </summary>
|
||||||
|
public override ActionDescriptorCollection ActionDescriptors
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
Initialize();
|
||||||
|
Debug.Assert(_collection != null);
|
||||||
|
Debug.Assert(_changeToken != null);
|
||||||
|
|
||||||
|
return _collection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an <see cref="IChangeToken"/> that will be signaled after the <see cref="ActionDescriptors"/>
|
||||||
|
/// collection has changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The <see cref="IChangeToken"/>.</returns>
|
||||||
|
public override IChangeToken GetChangeToken()
|
||||||
|
{
|
||||||
|
Initialize();
|
||||||
|
Debug.Assert(_collection != null);
|
||||||
|
Debug.Assert(_changeToken != null);
|
||||||
|
|
||||||
|
return _changeToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IChangeToken GetCompositeChangeToken()
|
||||||
|
{
|
||||||
|
if (_actionDescriptorChangeProviders.Length == 1)
|
||||||
|
{
|
||||||
|
return _actionDescriptorChangeProviders[0].GetChangeToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
var changeTokens = new IChangeToken[_actionDescriptorChangeProviders.Length];
|
||||||
|
for (var i = 0; i < _actionDescriptorChangeProviders.Length; i++)
|
||||||
|
{
|
||||||
|
changeTokens[i] = _actionDescriptorChangeProviders[i].GetChangeToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CompositeChangeToken(changeTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Initialize()
|
||||||
|
{
|
||||||
|
// Using double-checked locking on initialization because we fire change token callbacks
|
||||||
|
// when the collection changes. We don't want to do that repeatedly for redundant changes.
|
||||||
|
//
|
||||||
|
// The main call path of this code on the first call is async initialization from Endpoint Routing
|
||||||
|
// which is done in a non-blocking way so in practice no caller will ever block here.
|
||||||
|
if (_collection == null)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_collection == null)
|
||||||
|
{
|
||||||
|
UpdateCollection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCollection()
|
||||||
|
{
|
||||||
|
// Using the lock to initialize writes means that we serialize changes. This eliminates
|
||||||
|
// the potential for changes to be processed out of order - the risk is that newer data
|
||||||
|
// could be overwritten by older data.
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var context = new ActionDescriptorProviderContext();
|
||||||
|
|
||||||
|
for (var i = 0; i < _actionDescriptorProviders.Length; i++)
|
||||||
|
{
|
||||||
|
_actionDescriptorProviders[i].OnProvidersExecuting(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = _actionDescriptorProviders.Length - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
_actionDescriptorProviders[i].OnProvidersExecuted(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sequence for an update is important because we don't want anyone to obtain
|
||||||
|
// the new change token but the old action descriptor collection.
|
||||||
|
// 1. Obtain the old cancellation token source (don't trigger it yet)
|
||||||
|
// 2. Set the new action descriptor collection
|
||||||
|
// 3. Set the new change token
|
||||||
|
// 4. Trigger the old cancellation token source
|
||||||
|
//
|
||||||
|
// Consumers who poll will observe a new action descriptor collection at step 2 - they will see
|
||||||
|
// the new collection and ignore the change token.
|
||||||
|
//
|
||||||
|
// Consumers who listen to the change token will requery at step 4 - they will see the new collection
|
||||||
|
// and new change token.
|
||||||
|
//
|
||||||
|
// Anyone who acquires the collection and change token between steps 2 and 3 will be notified of
|
||||||
|
// a no-op change at step 4.
|
||||||
|
|
||||||
|
// Step 1.
|
||||||
|
var oldCancellationTokenSource = _cancellationTokenSource;
|
||||||
|
|
||||||
|
// Step 2.
|
||||||
|
_collection = new ActionDescriptorCollection(
|
||||||
|
new ReadOnlyCollection<ActionDescriptor>(context.Results),
|
||||||
|
_version++);
|
||||||
|
|
||||||
|
// Step 3.
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
_changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);
|
||||||
|
|
||||||
|
// Step 4 - might be null if it's the first time.
|
||||||
|
oldCancellationTokenSource?.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// 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 Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
|
@ -9,6 +10,11 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
/// Provides a way to signal invalidation of the cached collection of <see cref="Abstractions.ActionDescriptor" /> from an
|
/// Provides a way to signal invalidation of the cached collection of <see cref="Abstractions.ActionDescriptor" /> from an
|
||||||
/// <see cref="IActionDescriptorCollectionProvider"/>.
|
/// <see cref="IActionDescriptorCollectionProvider"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The change token returned from <see cref="GetChangeToken"/> is only for use inside the MVC infrastructure.
|
||||||
|
/// Use <see cref="ActionDescriptorCollectionProvider.GetChangeToken"/> to be notified of <see cref="ActionDescriptor"/>
|
||||||
|
/// changes.
|
||||||
|
/// </remarks>
|
||||||
public interface IActionDescriptorChangeProvider
|
public interface IActionDescriptorChangeProvider
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -16,6 +22,11 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
/// instances.
|
/// instances.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The <see cref="IChangeToken"/>.</returns>
|
/// <returns>The <see cref="IChangeToken"/>.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// The change token returned from <see cref="GetChangeToken"/> is only for use inside the MVC infrastructure.
|
||||||
|
/// Use <see cref="ActionDescriptorCollectionProvider.GetChangeToken"/> to be notified of <see cref="ActionDescriptor"/>
|
||||||
|
/// changes.
|
||||||
|
/// </remarks>
|
||||||
IChangeToken GetChangeToken();
|
IChangeToken GetChangeToken();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,18 +1,28 @@
|
||||||
// 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 Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides the currently cached collection of <see cref="Abstractions.ActionDescriptor"/>.
|
/// Provides the currently cached collection of <see cref="Abstractions.ActionDescriptor"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
/// The default implementation internally caches the collection and uses
|
/// The default implementation internally caches the collection and uses
|
||||||
/// <see cref="IActionDescriptorChangeProvider"/> to invalidate this cache, incrementing
|
/// <see cref="IActionDescriptorChangeProvider"/> to invalidate this cache, incrementing
|
||||||
/// <see cref="ActionDescriptorCollection.Version"/> the collection is reconstructed.
|
/// <see cref="ActionDescriptorCollection.Version"/> the collection is reconstructed.
|
||||||
///
|
///</para>
|
||||||
|
///<para>
|
||||||
|
/// To be reactively notified of changes, downcast to <see cref="ActionDescriptorCollectionProvider"/> and
|
||||||
|
/// subcribe to the change token returned from <see cref="ActionDescriptorCollectionProvider.GetChangeToken"/>
|
||||||
|
/// using <see cref="ChangeToken.OnChange(System.Func{IChangeToken}, System.Action)"/>.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
/// 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.
|
||||||
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public interface IActionDescriptorCollectionProvider
|
public interface IActionDescriptorCollectionProvider
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
// 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.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.Primitives;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Default implementation of <see cref="IActionDescriptorCollectionProvider"/>.
|
|
||||||
/// </summary>
|
|
||||||
public class ActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
|
|
||||||
{
|
|
||||||
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="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)
|
|
||||||
{
|
|
||||||
_actionDescriptorProviders = actionDescriptorProviders
|
|
||||||
.OrderBy(p => p.Order)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
_actionDescriptorChangeProviders = actionDescriptorChangeProviders.ToArray();
|
|
||||||
|
|
||||||
ChangeToken.OnChange(
|
|
||||||
GetCompositeChangeToken,
|
|
||||||
UpdateCollection);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IChangeToken GetCompositeChangeToken()
|
|
||||||
{
|
|
||||||
if (_actionDescriptorChangeProviders.Length == 1)
|
|
||||||
{
|
|
||||||
return _actionDescriptorChangeProviders[0].GetChangeToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
var changeTokens = new IChangeToken[_actionDescriptorChangeProviders.Length];
|
|
||||||
for (var i = 0; i < _actionDescriptorChangeProviders.Length; i++)
|
|
||||||
{
|
|
||||||
changeTokens[i] = _actionDescriptorChangeProviders[i].GetChangeToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CompositeChangeToken(changeTokens);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a cached collection of <see cref="ActionDescriptor" />.
|
|
||||||
/// </summary>
|
|
||||||
public ActionDescriptorCollection ActionDescriptors
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_collection == null)
|
|
||||||
{
|
|
||||||
UpdateCollection();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _collection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateCollection()
|
|
||||||
{
|
|
||||||
var context = new ActionDescriptorProviderContext();
|
|
||||||
|
|
||||||
for (var i = 0; i < _actionDescriptorProviders.Length; i++)
|
|
||||||
{
|
|
||||||
_actionDescriptorProviders[i].OnProvidersExecuting(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = _actionDescriptorProviders.Length - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
_actionDescriptorProviders[i].OnProvidersExecuted(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
_collection = new ActionDescriptorCollection(
|
|
||||||
new ReadOnlyCollection<ActionDescriptor>(context.Results),
|
|
||||||
Interlocked.Increment(ref _version));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,8 +3,10 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
|
@ -13,25 +15,27 @@ using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Routing.Patterns;
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
using Microsoft.Extensions.FileProviders;
|
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
internal class MvcEndpointDataSource : EndpointDataSource
|
internal class MvcEndpointDataSource : EndpointDataSource
|
||||||
{
|
{
|
||||||
private readonly object _lock = new object();
|
|
||||||
private readonly IActionDescriptorCollectionProvider _actions;
|
private readonly IActionDescriptorCollectionProvider _actions;
|
||||||
private readonly MvcEndpointInvokerFactory _invokerFactory;
|
private readonly MvcEndpointInvokerFactory _invokerFactory;
|
||||||
private readonly DefaultHttpContext _httpContextInstance;
|
private readonly DefaultHttpContext _httpContextInstance;
|
||||||
private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
|
|
||||||
|
|
||||||
|
// The following are protected by this lock for WRITES only. This pattern is similar
|
||||||
|
// to DefaultActionDescriptorChangeProvider - see comments there for details on
|
||||||
|
// all of the threading behaviors.
|
||||||
|
private readonly object _lock = new object();
|
||||||
private List<Endpoint> _endpoints;
|
private List<Endpoint> _endpoints;
|
||||||
|
private CancellationTokenSource _cancellationTokenSource;
|
||||||
|
private IChangeToken _changeToken;
|
||||||
|
|
||||||
public MvcEndpointDataSource(
|
public MvcEndpointDataSource(
|
||||||
IActionDescriptorCollectionProvider actions,
|
IActionDescriptorCollectionProvider actions,
|
||||||
MvcEndpointInvokerFactory invokerFactory,
|
MvcEndpointInvokerFactory invokerFactory,
|
||||||
IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders,
|
|
||||||
IServiceProvider serviceProvider)
|
IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
if (actions == null)
|
if (actions == null)
|
||||||
|
|
@ -44,11 +48,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
throw new ArgumentNullException(nameof(invokerFactory));
|
throw new ArgumentNullException(nameof(invokerFactory));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionDescriptorChangeProviders == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(actionDescriptorChangeProviders));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serviceProvider == null)
|
if (serviceProvider == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(serviceProvider));
|
throw new ArgumentNullException(nameof(serviceProvider));
|
||||||
|
|
@ -56,139 +55,199 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
_actions = actions;
|
_actions = actions;
|
||||||
_invokerFactory = invokerFactory;
|
_invokerFactory = invokerFactory;
|
||||||
_actionDescriptorChangeProviders = actionDescriptorChangeProviders.ToArray();
|
|
||||||
_httpContextInstance = new DefaultHttpContext() { RequestServices = serviceProvider };
|
_httpContextInstance = new DefaultHttpContext() { RequestServices = serviceProvider };
|
||||||
|
|
||||||
ConventionalEndpointInfos = new List<MvcEndpointInfo>();
|
ConventionalEndpointInfos = new List<MvcEndpointInfo>();
|
||||||
|
|
||||||
|
// It's possible for someone to override the collection provider without providing
|
||||||
|
// change notifications. If that's the case we won't process changes.
|
||||||
|
if (actions is ActionDescriptorCollectionProvider collectionProviderWithChangeToken)
|
||||||
|
{
|
||||||
|
ChangeToken.OnChange(
|
||||||
|
() => collectionProviderWithChangeToken.GetChangeToken(),
|
||||||
|
UpdateEndpoints);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Endpoint> CreateEndpoints()
|
public List<MvcEndpointInfo> ConventionalEndpointInfos { get; }
|
||||||
|
|
||||||
|
public override IReadOnlyList<Endpoint> Endpoints
|
||||||
{
|
{
|
||||||
var endpoints = new List<Endpoint>();
|
get
|
||||||
StringBuilder patternStringBuilder = null;
|
|
||||||
|
|
||||||
foreach (var action in _actions.ActionDescriptors.Items)
|
|
||||||
{
|
{
|
||||||
if (action.AttributeRouteInfo == null)
|
Initialize();
|
||||||
|
Debug.Assert(_changeToken != null);
|
||||||
|
Debug.Assert(_endpoints != null);
|
||||||
|
return _endpoints;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IChangeToken GetChangeToken()
|
||||||
|
{
|
||||||
|
Initialize();
|
||||||
|
Debug.Assert(_changeToken != null);
|
||||||
|
Debug.Assert(_endpoints != null);
|
||||||
|
return _changeToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Initialize()
|
||||||
|
{
|
||||||
|
if (_endpoints == null)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
{
|
{
|
||||||
// In traditional conventional routing setup, the routes defined by a user have a static order
|
if (_endpoints == null)
|
||||||
// defined by how they are added into the list. We would like to maintain the same order when building
|
|
||||||
// up the endpoints too.
|
|
||||||
//
|
|
||||||
// Start with an order of '1' for conventional routes as attribute routes have a default order of '0'.
|
|
||||||
// This is for scenarios dealing with migrating existing Router based code to Endpoint Routing world.
|
|
||||||
var conventionalRouteOrder = 0;
|
|
||||||
|
|
||||||
// Check each of the conventional patterns to see if the action would be reachable
|
|
||||||
// If the action and pattern are compatible then create an endpoint with the
|
|
||||||
// area/controller/action parameter parts replaced with literals
|
|
||||||
//
|
|
||||||
// e.g. {controller}/{action} with HomeController.Index and HomeController.Login
|
|
||||||
// would result in endpoints:
|
|
||||||
// - Home/Index
|
|
||||||
// - Home/Login
|
|
||||||
foreach (var endpointInfo in ConventionalEndpointInfos)
|
|
||||||
{
|
{
|
||||||
// An 'endpointInfo' is applicable if:
|
UpdateEndpoints();
|
||||||
// 1. it has a parameter (or default value) for 'required' non-null route value
|
}
|
||||||
// 2. it does not have a parameter (or default value) for 'required' null route value
|
}
|
||||||
var isApplicable = true;
|
}
|
||||||
foreach (var routeKey in action.RouteValues.Keys)
|
}
|
||||||
|
|
||||||
|
private void UpdateEndpoints()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var endpoints = new List<Endpoint>();
|
||||||
|
StringBuilder patternStringBuilder = null;
|
||||||
|
|
||||||
|
foreach (var action in _actions.ActionDescriptors.Items)
|
||||||
|
{
|
||||||
|
if (action.AttributeRouteInfo == null)
|
||||||
|
{
|
||||||
|
// In traditional conventional routing setup, the routes defined by a user have a static order
|
||||||
|
// defined by how they are added into the list. We would like to maintain the same order when building
|
||||||
|
// up the endpoints too.
|
||||||
|
//
|
||||||
|
// Start with an order of '1' for conventional routes as attribute routes have a default order of '0'.
|
||||||
|
// This is for scenarios dealing with migrating existing Router based code to Endpoint Routing world.
|
||||||
|
var conventionalRouteOrder = 0;
|
||||||
|
|
||||||
|
// Check each of the conventional patterns to see if the action would be reachable
|
||||||
|
// If the action and pattern are compatible then create an endpoint with the
|
||||||
|
// area/controller/action parameter parts replaced with literals
|
||||||
|
//
|
||||||
|
// e.g. {controller}/{action} with HomeController.Index and HomeController.Login
|
||||||
|
// would result in endpoints:
|
||||||
|
// - Home/Index
|
||||||
|
// - Home/Login
|
||||||
|
foreach (var endpointInfo in ConventionalEndpointInfos)
|
||||||
{
|
{
|
||||||
if (!MatchRouteValue(action, endpointInfo, routeKey))
|
// An 'endpointInfo' is applicable if:
|
||||||
|
// 1. it has a parameter (or default value) for 'required' non-null route value
|
||||||
|
// 2. it does not have a parameter (or default value) for 'required' null route value
|
||||||
|
var isApplicable = true;
|
||||||
|
foreach (var routeKey in action.RouteValues.Keys)
|
||||||
{
|
{
|
||||||
isApplicable = false;
|
if (!MatchRouteValue(action, endpointInfo, routeKey))
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isApplicable)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newPathSegments = endpointInfo.ParsedPattern.PathSegments.ToList();
|
|
||||||
|
|
||||||
for (var i = 0; i < newPathSegments.Count; i++)
|
|
||||||
{
|
|
||||||
// Check if the pattern can be shortened because the remaining parameters are optional
|
|
||||||
//
|
|
||||||
// e.g. Matching pattern {controller=Home}/{action=Index}/{id?} against HomeController.Index
|
|
||||||
// can resolve to the following endpoints:
|
|
||||||
// - /Home/Index/{id?}
|
|
||||||
// - /Home
|
|
||||||
// - /
|
|
||||||
if (UseDefaultValuePlusRemainingSegementsOptional(i, action, endpointInfo, newPathSegments))
|
|
||||||
{
|
|
||||||
var subPathSegments = newPathSegments.Take(i);
|
|
||||||
|
|
||||||
var subEndpoint = CreateEndpoint(
|
|
||||||
action,
|
|
||||||
endpointInfo.Name,
|
|
||||||
GetPattern(ref patternStringBuilder, subPathSegments),
|
|
||||||
subPathSegments,
|
|
||||||
endpointInfo.Defaults,
|
|
||||||
++conventionalRouteOrder,
|
|
||||||
endpointInfo,
|
|
||||||
suppressLinkGeneration: false);
|
|
||||||
endpoints.Add(subEndpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<RoutePatternPart> segmentParts = null; // Initialize only as needed
|
|
||||||
var segment = newPathSegments[i];
|
|
||||||
for (var j = 0; j < segment.Parts.Count; j++)
|
|
||||||
{
|
|
||||||
var part = segment.Parts[j];
|
|
||||||
|
|
||||||
if (part.IsParameter &&
|
|
||||||
part is RoutePatternParameterPart parameterPart &&
|
|
||||||
action.RouteValues.ContainsKey(parameterPart.Name))
|
|
||||||
{
|
{
|
||||||
if (segmentParts == null)
|
isApplicable = false;
|
||||||
{
|
break;
|
||||||
segmentParts = segment.Parts.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace parameter with literal value
|
|
||||||
segmentParts[j] = RoutePatternFactory.LiteralPart(action.RouteValues[parameterPart.Name]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A parameter part was replaced so replace segment with updated parts
|
if (!isApplicable)
|
||||||
if (segmentParts != null)
|
|
||||||
{
|
{
|
||||||
newPathSegments[i] = RoutePatternFactory.Segment(segmentParts);
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
var newPathSegments = endpointInfo.ParsedPattern.PathSegments.ToList();
|
||||||
|
|
||||||
|
for (var i = 0; i < newPathSegments.Count; i++)
|
||||||
|
{
|
||||||
|
// Check if the pattern can be shortened because the remaining parameters are optional
|
||||||
|
//
|
||||||
|
// e.g. Matching pattern {controller=Home}/{action=Index}/{id?} against HomeController.Index
|
||||||
|
// can resolve to the following endpoints:
|
||||||
|
// - /Home/Index/{id?}
|
||||||
|
// - /Home
|
||||||
|
// - /
|
||||||
|
if (UseDefaultValuePlusRemainingSegementsOptional(i, action, endpointInfo, newPathSegments))
|
||||||
|
{
|
||||||
|
var subPathSegments = newPathSegments.Take(i);
|
||||||
|
|
||||||
|
var subEndpoint = CreateEndpoint(
|
||||||
|
action,
|
||||||
|
endpointInfo.Name,
|
||||||
|
GetPattern(ref patternStringBuilder, subPathSegments),
|
||||||
|
subPathSegments,
|
||||||
|
endpointInfo.Defaults,
|
||||||
|
++conventionalRouteOrder,
|
||||||
|
endpointInfo,
|
||||||
|
suppressLinkGeneration: false);
|
||||||
|
endpoints.Add(subEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RoutePatternPart> segmentParts = null; // Initialize only as needed
|
||||||
|
var segment = newPathSegments[i];
|
||||||
|
for (var j = 0; j < segment.Parts.Count; j++)
|
||||||
|
{
|
||||||
|
var part = segment.Parts[j];
|
||||||
|
|
||||||
|
if (part.IsParameter &&
|
||||||
|
part is RoutePatternParameterPart parameterPart &&
|
||||||
|
action.RouteValues.ContainsKey(parameterPart.Name))
|
||||||
|
{
|
||||||
|
if (segmentParts == null)
|
||||||
|
{
|
||||||
|
segmentParts = segment.Parts.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace parameter with literal value
|
||||||
|
segmentParts[j] = RoutePatternFactory.LiteralPart(action.RouteValues[parameterPart.Name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A parameter part was replaced so replace segment with updated parts
|
||||||
|
if (segmentParts != null)
|
||||||
|
{
|
||||||
|
newPathSegments[i] = RoutePatternFactory.Segment(segmentParts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpoint = CreateEndpoint(
|
||||||
|
action,
|
||||||
|
endpointInfo.Name,
|
||||||
|
GetPattern(ref patternStringBuilder, newPathSegments),
|
||||||
|
newPathSegments,
|
||||||
|
endpointInfo.Defaults,
|
||||||
|
++conventionalRouteOrder,
|
||||||
|
endpointInfo,
|
||||||
|
suppressLinkGeneration: false);
|
||||||
|
endpoints.Add(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
var endpoint = CreateEndpoint(
|
var endpoint = CreateEndpoint(
|
||||||
action,
|
action,
|
||||||
endpointInfo.Name,
|
action.AttributeRouteInfo.Name,
|
||||||
GetPattern(ref patternStringBuilder, newPathSegments),
|
action.AttributeRouteInfo.Template,
|
||||||
newPathSegments,
|
RoutePatternFactory.Parse(action.AttributeRouteInfo.Template).PathSegments,
|
||||||
endpointInfo.Defaults,
|
nonInlineDefaults: null,
|
||||||
++conventionalRouteOrder,
|
action.AttributeRouteInfo.Order,
|
||||||
endpointInfo,
|
action.AttributeRouteInfo,
|
||||||
suppressLinkGeneration: false);
|
suppressLinkGeneration: action.AttributeRouteInfo.SuppressLinkGeneration);
|
||||||
endpoints.Add(endpoint);
|
endpoints.Add(endpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
var endpoint = CreateEndpoint(
|
|
||||||
action,
|
|
||||||
action.AttributeRouteInfo.Name,
|
|
||||||
action.AttributeRouteInfo.Template,
|
|
||||||
RoutePatternFactory.Parse(action.AttributeRouteInfo.Template).PathSegments,
|
|
||||||
nonInlineDefaults: null,
|
|
||||||
action.AttributeRouteInfo.Order,
|
|
||||||
action.AttributeRouteInfo,
|
|
||||||
suppressLinkGeneration: action.AttributeRouteInfo.SuppressLinkGeneration);
|
|
||||||
endpoints.Add(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return endpoints;
|
// See comments in DefaultActionDescriptorCollectionProvider. These steps are done
|
||||||
|
// in a specific order to ensure callers always see a consistent state.
|
||||||
|
|
||||||
|
// Step 1 - capture old token
|
||||||
|
var oldCancellationTokenSource = _cancellationTokenSource;
|
||||||
|
|
||||||
|
// Step 2 - update endpoints
|
||||||
|
_endpoints = endpoints;
|
||||||
|
|
||||||
|
// Step 3 - create new change token
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
_changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);
|
||||||
|
|
||||||
|
// Step 4 - trigger old token
|
||||||
|
oldCancellationTokenSource?.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
string GetPattern(ref StringBuilder sb, IEnumerable<RoutePatternPathSegment> segments)
|
string GetPattern(ref StringBuilder sb, IEnumerable<RoutePatternPathSegment> segments)
|
||||||
{
|
{
|
||||||
|
|
@ -442,49 +501,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IChangeToken GetCompositeChangeToken()
|
|
||||||
{
|
|
||||||
if (_actionDescriptorChangeProviders.Length == 1)
|
|
||||||
{
|
|
||||||
return _actionDescriptorChangeProviders[0].GetChangeToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
var changeTokens = new IChangeToken[_actionDescriptorChangeProviders.Length];
|
|
||||||
for (var i = 0; i < _actionDescriptorChangeProviders.Length; i++)
|
|
||||||
{
|
|
||||||
changeTokens[i] = _actionDescriptorChangeProviders[i].GetChangeToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CompositeChangeToken(changeTokens);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IChangeToken GetChangeToken() => NullChangeToken.Singleton;
|
|
||||||
|
|
||||||
public override IReadOnlyList<Endpoint> Endpoints
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
// Want to initialize endpoints once and then cache while ensuring a null collection is never returned
|
|
||||||
// Local copy for thread safety + double check locking
|
|
||||||
var localEndpoints = _endpoints;
|
|
||||||
if (localEndpoints == null)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
localEndpoints = _endpoints;
|
|
||||||
if (localEndpoints == null)
|
|
||||||
{
|
|
||||||
_endpoints = localEndpoints = CreateEndpoints();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return localEndpoints;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<MvcEndpointInfo> ConventionalEndpointInfos { get; }
|
|
||||||
|
|
||||||
private class SuppressLinkGenerationMetadata : ISuppressLinkGenerationMetadata { }
|
private class SuppressLinkGenerationMetadata : ISuppressLinkGenerationMetadata { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,14 +4,13 @@
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
{
|
{
|
||||||
public class ActionDescriptorCollectionProviderTest
|
public class DefaultActionDescriptorCollectionProviderTest
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ActionDescriptors_ReadsDescriptorsFromActionDescriptorProviders()
|
public void ActionDescriptors_ReadsDescriptorsFromActionDescriptorProviders()
|
||||||
|
|
@ -24,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
var expected3 = new ActionDescriptor();
|
var expected3 = new ActionDescriptor();
|
||||||
var actionDescriptorProvider2 = GetActionDescriptorProvider(expected2, expected3);
|
var actionDescriptorProvider2 = GetActionDescriptorProvider(expected2, expected3);
|
||||||
|
|
||||||
var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
|
var actionDescriptorCollectionProvider = new DefaultActionDescriptorCollectionProvider(
|
||||||
new[] { actionDescriptorProvider1, actionDescriptorProvider2 },
|
new[] { actionDescriptorProvider1, actionDescriptorProvider2 },
|
||||||
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||||
|
|
||||||
|
|
@ -46,7 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
// Arrange
|
// Arrange
|
||||||
var actionDescriptorProvider = GetActionDescriptorProvider(new ActionDescriptor());
|
var actionDescriptorProvider = GetActionDescriptorProvider(new ActionDescriptor());
|
||||||
|
|
||||||
var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
|
var actionDescriptorCollectionProvider = new DefaultActionDescriptorCollectionProvider(
|
||||||
new[] { actionDescriptorProvider },
|
new[] { actionDescriptorProvider },
|
||||||
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||||
|
|
||||||
|
|
@ -66,55 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ActionDescriptors_UpdateWhenChangeTokenProviderChanges()
|
public void ActionDescriptors_UpdatesAndResubscripes_WhenChangeTokenTriggers()
|
||||||
{
|
|
||||||
// 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
|
// Arrange
|
||||||
var actionDescriptorProvider = new Mock<IActionDescriptorProvider>();
|
var actionDescriptorProvider = new Mock<IActionDescriptorProvider>();
|
||||||
|
|
@ -143,34 +94,61 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
invocations++;
|
invocations++;
|
||||||
});
|
});
|
||||||
var changeProvider = new TestChangeProvider();
|
var changeProvider = new TestChangeProvider();
|
||||||
var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
|
var actionDescriptorCollectionProvider = new DefaultActionDescriptorCollectionProvider(
|
||||||
new[] { actionDescriptorProvider.Object },
|
new[] { actionDescriptorProvider.Object },
|
||||||
new[] { changeProvider });
|
new[] { changeProvider });
|
||||||
|
|
||||||
// Act - 1
|
// Act - 1
|
||||||
|
var changeToken1 = actionDescriptorCollectionProvider.GetChangeToken();
|
||||||
var collection1 = actionDescriptorCollectionProvider.ActionDescriptors;
|
var collection1 = actionDescriptorCollectionProvider.ActionDescriptors;
|
||||||
|
|
||||||
|
ActionDescriptorCollection captured = null;
|
||||||
|
changeToken1.RegisterChangeCallback((_) =>
|
||||||
|
{
|
||||||
|
captured = actionDescriptorCollectionProvider.ActionDescriptors;
|
||||||
|
}, null);
|
||||||
|
|
||||||
// Assert - 1
|
// Assert - 1
|
||||||
|
Assert.False(changeToken1.HasChanged);
|
||||||
Assert.Equal(0, collection1.Version);
|
Assert.Equal(0, collection1.Version);
|
||||||
Assert.Collection(collection1.Items,
|
Assert.Collection(collection1.Items,
|
||||||
item => Assert.Same(expected1, item));
|
item => Assert.Same(expected1, item));
|
||||||
|
|
||||||
// Act - 2
|
// Act - 2
|
||||||
changeProvider.TokenSource.Cancel();
|
changeProvider.TokenSource.Cancel();
|
||||||
|
var changeToken2 = actionDescriptorCollectionProvider.GetChangeToken();
|
||||||
var collection2 = actionDescriptorCollectionProvider.ActionDescriptors;
|
var collection2 = actionDescriptorCollectionProvider.ActionDescriptors;
|
||||||
|
|
||||||
|
changeToken2.RegisterChangeCallback((_) =>
|
||||||
|
{
|
||||||
|
captured = actionDescriptorCollectionProvider.ActionDescriptors;
|
||||||
|
}, null);
|
||||||
|
|
||||||
// Assert - 2
|
// Assert - 2
|
||||||
|
Assert.NotSame(changeToken1, changeToken2);
|
||||||
|
Assert.True(changeToken1.HasChanged);
|
||||||
|
Assert.False(changeToken2.HasChanged);
|
||||||
|
|
||||||
Assert.NotSame(collection1, collection2);
|
Assert.NotSame(collection1, collection2);
|
||||||
|
Assert.NotNull(captured);
|
||||||
|
Assert.Same(captured, collection2);
|
||||||
Assert.Equal(1, collection2.Version);
|
Assert.Equal(1, collection2.Version);
|
||||||
Assert.Collection(collection2.Items,
|
Assert.Collection(collection2.Items,
|
||||||
item => Assert.Same(expected2, item));
|
item => Assert.Same(expected2, item));
|
||||||
|
|
||||||
// Act - 3
|
// Act - 3
|
||||||
changeProvider.TokenSource.Cancel();
|
changeProvider.TokenSource.Cancel();
|
||||||
|
var changeToken3 = actionDescriptorCollectionProvider.GetChangeToken();
|
||||||
var collection3 = actionDescriptorCollectionProvider.ActionDescriptors;
|
var collection3 = actionDescriptorCollectionProvider.ActionDescriptors;
|
||||||
|
|
||||||
// Assert - 3
|
// Assert - 3
|
||||||
|
Assert.NotSame(changeToken2, changeToken3);
|
||||||
|
Assert.True(changeToken2.HasChanged);
|
||||||
|
Assert.False(changeToken3.HasChanged);
|
||||||
|
|
||||||
Assert.NotSame(collection2, collection3);
|
Assert.NotSame(collection2, collection3);
|
||||||
|
Assert.NotNull(captured);
|
||||||
|
Assert.Same(captured, collection3);
|
||||||
Assert.Equal(2, collection3.Version);
|
Assert.Equal(2, collection3.Version);
|
||||||
Assert.Collection(collection3.Items,
|
Assert.Collection(collection3.Items,
|
||||||
item => Assert.Same(expected3, item));
|
item => Assert.Same(expected3, item));
|
||||||
|
|
@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
private static ActionConstraintCache CreateCache(params IActionConstraintProvider[] providers)
|
private static ActionConstraintCache CreateCache(params IActionConstraintProvider[] providers)
|
||||||
{
|
{
|
||||||
var descriptorProvider = new ActionDescriptorCollectionProvider(
|
var descriptorProvider = new DefaultActionDescriptorCollectionProvider(
|
||||||
Enumerable.Empty<IActionDescriptorProvider>(),
|
Enumerable.Empty<IActionDescriptorProvider>(),
|
||||||
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||||
return new ActionConstraintCache(descriptorProvider, providers);
|
return new ActionConstraintCache(descriptorProvider, providers);
|
||||||
|
|
|
||||||
|
|
@ -932,7 +932,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
private ControllerActionDescriptor InvokeActionSelector(RouteContext context)
|
private ControllerActionDescriptor InvokeActionSelector(RouteContext context)
|
||||||
{
|
{
|
||||||
var actionDescriptorProvider = GetActionDescriptorProvider();
|
var actionDescriptorProvider = GetActionDescriptorProvider();
|
||||||
var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
|
var actionDescriptorCollectionProvider = new DefaultActionDescriptorCollectionProvider(
|
||||||
new[] { actionDescriptorProvider },
|
new[] { actionDescriptorProvider },
|
||||||
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||||
|
|
||||||
|
|
@ -1092,7 +1092,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||||
|
|
||||||
private static ActionConstraintCache GetActionConstraintCache(IActionConstraintProvider[] actionConstraintProviders = null)
|
private static ActionConstraintCache GetActionConstraintCache(IActionConstraintProvider[] actionConstraintProviders = null)
|
||||||
{
|
{
|
||||||
var descriptorProvider = new ActionDescriptorCollectionProvider(
|
var descriptorProvider = new DefaultActionDescriptorCollectionProvider(
|
||||||
Enumerable.Empty<IActionDescriptorProvider>(),
|
Enumerable.Empty<IActionDescriptorProvider>(),
|
||||||
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||||
return new ActionConstraintCache(descriptorProvider, actionConstraintProviders.AsEnumerable() ?? new List<IActionConstraintProvider>());
|
return new ActionConstraintCache(descriptorProvider, actionConstraintProviders.AsEnumerable() ?? new List<IActionConstraintProvider>());
|
||||||
|
|
|
||||||
|
|
@ -133,47 +133,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.True(actionInvokerCalled);
|
Assert.True(actionInvokerCalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "https://github.com/aspnet/Routing/issues/722")]
|
|
||||||
public void GetChangeToken_MultipleChangeTokenProviders_ComposedResult()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var featureCollection = new FeatureCollection();
|
|
||||||
featureCollection.Set<IEndpointFeature>(new EndpointFeature
|
|
||||||
{
|
|
||||||
RouteValues = new RouteValueDictionary()
|
|
||||||
});
|
|
||||||
|
|
||||||
var httpContextMock = new Mock<HttpContext>();
|
|
||||||
httpContextMock.Setup(m => m.Features).Returns(featureCollection);
|
|
||||||
|
|
||||||
var descriptorProviderMock = new Mock<IActionDescriptorCollectionProvider>();
|
|
||||||
descriptorProviderMock.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>(), 0));
|
|
||||||
|
|
||||||
var actionInvokerMock = new Mock<IActionInvoker>();
|
|
||||||
|
|
||||||
var actionInvokerProviderMock = new Mock<IActionInvokerFactory>();
|
|
||||||
actionInvokerProviderMock.Setup(m => m.CreateInvoker(It.IsAny<ActionContext>())).Returns(actionInvokerMock.Object);
|
|
||||||
|
|
||||||
var changeTokenMock = new Mock<IChangeToken>();
|
|
||||||
|
|
||||||
var changeProvider1Mock = new Mock<IActionDescriptorChangeProvider>();
|
|
||||||
changeProvider1Mock.Setup(m => m.GetChangeToken()).Returns(changeTokenMock.Object);
|
|
||||||
var changeProvider2Mock = new Mock<IActionDescriptorChangeProvider>();
|
|
||||||
changeProvider2Mock.Setup(m => m.GetChangeToken()).Returns(changeTokenMock.Object);
|
|
||||||
|
|
||||||
var dataSource = CreateMvcEndpointDataSource(
|
|
||||||
descriptorProviderMock.Object,
|
|
||||||
new MvcEndpointInvokerFactory(actionInvokerProviderMock.Object),
|
|
||||||
new[] { changeProvider1Mock.Object, changeProvider2Mock.Object });
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var changeToken = dataSource.GetChangeToken();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var compositeChangeToken = Assert.IsType<CompositeChangeToken>(changeToken);
|
|
||||||
Assert.Equal(2, compositeChangeToken.ChangeTokens.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("{controller}/{action}/{id?}", new[] { "TestController/TestAction/{id?}" })]
|
[InlineData("{controller}/{action}/{id?}", new[] { "TestController/TestAction/{id?}" })]
|
||||||
[InlineData("{controller}/{id?}", new string[] { })]
|
[InlineData("{controller}/{id?}", new string[] { })]
|
||||||
|
|
@ -287,11 +246,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
actionDescriptorCollectionProviderMock.VerifyGet(m => m.ActionDescriptors, Times.Once);
|
actionDescriptorCollectionProviderMock.VerifyGet(m => m.ActionDescriptors, Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "https://github.com/aspnet/Routing/issues/722")]
|
[Fact]
|
||||||
public void Endpoints_ChangeTokenTriggered_EndpointsRecreated()
|
public void Endpoints_ChangeTokenTriggered_EndpointsRecreated()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var actionDescriptorCollectionProviderMock = new Mock<IActionDescriptorCollectionProvider>();
|
var actionDescriptorCollectionProviderMock = new Mock<ActionDescriptorCollectionProvider>();
|
||||||
actionDescriptorCollectionProviderMock
|
actionDescriptorCollectionProviderMock
|
||||||
.Setup(m => m.ActionDescriptors)
|
.Setup(m => m.ActionDescriptors)
|
||||||
.Returns(new ActionDescriptorCollection(new[]
|
.Returns(new ActionDescriptorCollection(new[]
|
||||||
|
|
@ -300,19 +259,18 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}, version: 0));
|
}, version: 0));
|
||||||
|
|
||||||
CancellationTokenSource cts = null;
|
CancellationTokenSource cts = null;
|
||||||
|
actionDescriptorCollectionProviderMock
|
||||||
|
.Setup(m => m.GetChangeToken())
|
||||||
|
.Returns(() =>
|
||||||
|
{
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
var changeToken = new CancellationChangeToken(cts.Token);
|
||||||
|
|
||||||
var changeProviderMock = new Mock<IActionDescriptorChangeProvider>();
|
return changeToken;
|
||||||
changeProviderMock.Setup(m => m.GetChangeToken()).Returns(() =>
|
});
|
||||||
{
|
|
||||||
cts = new CancellationTokenSource();
|
|
||||||
var changeToken = new CancellationChangeToken(cts.Token);
|
|
||||||
|
|
||||||
return changeToken;
|
|
||||||
});
|
|
||||||
|
|
||||||
var dataSource = CreateMvcEndpointDataSource(
|
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollectionProviderMock.Object);
|
||||||
actionDescriptorCollectionProviderMock.Object,
|
|
||||||
actionDescriptorChangeProviders: new[] { changeProviderMock.Object });
|
|
||||||
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
|
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
|
||||||
string.Empty,
|
string.Empty,
|
||||||
"{controller}/{action}",
|
"{controller}/{action}",
|
||||||
|
|
@ -752,15 +710,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
private MvcEndpointDataSource CreateMvcEndpointDataSource(
|
private MvcEndpointDataSource CreateMvcEndpointDataSource(
|
||||||
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider = null,
|
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider = null,
|
||||||
MvcEndpointInvokerFactory mvcEndpointInvokerFactory = null,
|
MvcEndpointInvokerFactory mvcEndpointInvokerFactory = null)
|
||||||
IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders = null)
|
|
||||||
{
|
{
|
||||||
if (actionDescriptorCollectionProvider == null)
|
if (actionDescriptorCollectionProvider == null)
|
||||||
{
|
{
|
||||||
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
|
actionDescriptorCollectionProvider = new DefaultActionDescriptorCollectionProvider(
|
||||||
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>(), 0));
|
Array.Empty<IActionDescriptorProvider>(),
|
||||||
|
Array.Empty<IActionDescriptorChangeProvider>());
|
||||||
actionDescriptorCollectionProvider = mockDescriptorProvider.Object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var serviceProviderMock = new Mock<IServiceProvider>();
|
var serviceProviderMock = new Mock<IServiceProvider>();
|
||||||
|
|
@ -769,7 +725,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
var dataSource = new MvcEndpointDataSource(
|
var dataSource = new MvcEndpointDataSource(
|
||||||
actionDescriptorCollectionProvider,
|
actionDescriptorCollectionProvider,
|
||||||
mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
|
mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
|
||||||
actionDescriptorChangeProviders ?? Array.Empty<IActionDescriptorChangeProvider>(),
|
|
||||||
serviceProviderMock.Object);
|
serviceProviderMock.Object);
|
||||||
|
|
||||||
return dataSource;
|
return dataSource;
|
||||||
|
|
|
||||||
|
|
@ -351,7 +351,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
|
var actionDescriptorCollectionProvider = new DefaultActionDescriptorCollectionProvider(
|
||||||
new IActionDescriptorProvider[] { actionDescriptorProvider.Object, },
|
new IActionDescriptorProvider[] { actionDescriptorProvider.Object, },
|
||||||
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||||
|
|
||||||
|
|
@ -398,7 +398,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
|
|
||||||
private static ActionConstraintCache GetActionConstraintCache(IActionConstraintProvider[] actionConstraintProviders = null)
|
private static ActionConstraintCache GetActionConstraintCache(IActionConstraintProvider[] actionConstraintProviders = null)
|
||||||
{
|
{
|
||||||
var descriptorProvider = new ActionDescriptorCollectionProvider(
|
var descriptorProvider = new DefaultActionDescriptorCollectionProvider(
|
||||||
Enumerable.Empty<IActionDescriptorProvider>(),
|
Enumerable.Empty<IActionDescriptorProvider>(),
|
||||||
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||||
return new ActionConstraintCache(descriptorProvider, actionConstraintProviders.AsEnumerable() ?? new List<IActionConstraintProvider>());
|
return new ActionConstraintCache(descriptorProvider, actionConstraintProviders.AsEnumerable() ?? new List<IActionConstraintProvider>());
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ 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(
|
var descriptorCollectionProvider = new DefaultActionDescriptorCollectionProvider(
|
||||||
new[] { actionProvider.Object },
|
new[] { actionProvider.Object },
|
||||||
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
Enumerable.Empty<IActionDescriptorChangeProvider>());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1087,7 +1087,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "https://github.com/aspnet/Routing/issues/722")]
|
[Fact]
|
||||||
public async Task ApiExplorer_Updates_WhenActionDescriptorCollectionIsUpdated()
|
public async Task ApiExplorer_Updates_WhenActionDescriptorCollectionIsUpdated()
|
||||||
{
|
{
|
||||||
// Act - 1
|
// Act - 1
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue