| File: Infrastructure\DefaultActionDescriptorCollectionProvider.cs | Web Access |
| Project: src\aspnetcore\src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj (Microsoft.AspNetCore.Mvc.Core) |
// 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); } }