|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.Infrastructure;
internal sealed partial class DefaultActionDescriptorCollectionProvider : ActionDescriptorCollectionProvider
{
private readonly IActionDescriptorProvider[] _actionDescriptorProviders;
private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
private readonly ILogger _logger;
// 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;
public DefaultActionDescriptorCollectionProvider(
IEnumerable<IActionDescriptorProvider> actionDescriptorProviders,
IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders,
ILogger<DefaultActionDescriptorCollectionProvider> logger)
{
_actionDescriptorProviders = actionDescriptorProviders
.OrderBy(p => p.Order)
.ToArray();
_actionDescriptorChangeProviders = actionDescriptorChangeProviders.ToArray();
_lock = new object();
_logger = logger;
// IMPORTANT: this needs to be the last thing we do in the constructor. Change notifications can happen immediately!
ChangeToken.OnChange(
GetCompositeChangeToken,
UpdateCollection);
}
/// <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);
}
if (context.Results.Count == 0)
{
// Emit a log message if after all providers still no action
// descriptors detected in the context.
Log.NoActionDescriptors(_logger);
}
// 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 re-query 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();
}
}
public static partial class Log
{
[LoggerMessage(
EventId = 1,
EventName = "NoActionDescriptors",
Level = LogLevel.Information,
Message = "No action descriptors found. This may indicate an incorrectly configured application or missing application parts. To learn more, visit https://aka.ms/aspnet/mvc/app-parts")]
public static partial void NoActionDescriptors(ILogger logger);
}
}
|