| File: CompositeEndpointDataSource.cs | Web Access |
| Project: src\aspnetcore\src\Http\Routing\src\Microsoft.AspNetCore.Routing.csproj (Microsoft.AspNetCore.Routing) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Routing; /// <summary> /// Represents an <see cref="EndpointDataSource"/> whose values come from a collection of <see cref="EndpointDataSource"/> instances. /// </summary> [DebuggerDisplay("{DebuggerDisplayString,nq}")] public sealed class CompositeEndpointDataSource : EndpointDataSource, IDisposable { private readonly object _lock = new(); private readonly ICollection<EndpointDataSource> _dataSources; private List<Endpoint>? _endpoints; private IChangeToken? _consumerChangeToken; private CancellationTokenSource? _cts; private List<IDisposable>? _changeTokenRegistrations; private bool _disposed; internal CompositeEndpointDataSource(ObservableCollection<EndpointDataSource> dataSources) { dataSources.CollectionChanged += OnDataSourcesChanged; _dataSources = dataSources; } /// <summary> /// Instantiates a <see cref="CompositeEndpointDataSource"/> object from <paramref name="endpointDataSources"/>. /// </summary> /// <param name="endpointDataSources">An collection of <see cref="EndpointDataSource" /> objects.</param> /// <returns>A <see cref="CompositeEndpointDataSource"/>.</returns> public CompositeEndpointDataSource(IEnumerable<EndpointDataSource> endpointDataSources) { _dataSources = new List<EndpointDataSource>(); foreach (var dataSource in endpointDataSources) { _dataSources.Add(dataSource); } } private void OnDataSourcesChanged(object? sender, NotifyCollectionChangedEventArgs e) => HandleChange(collectionChanged: true); /// <summary> /// Returns the collection of <see cref="EndpointDataSource"/> instances associated with the object. /// </summary> public IEnumerable<EndpointDataSource> DataSources => _dataSources; /// <summary> /// Gets a <see cref="IChangeToken"/> used to signal invalidation of cached <see cref="Endpoint"/> instances. /// </summary> /// <returns>The <see cref="IChangeToken"/>.</returns> public override IChangeToken GetChangeToken() { EnsureChangeTokenInitialized(); return _consumerChangeToken; } /// <summary> /// Returns a read-only collection of <see cref="Endpoint"/> instances. /// </summary> public override IReadOnlyList<Endpoint> Endpoints { get { EnsureEndpointsInitialized(); return _endpoints; } } /// <inheritdoc/> public override IReadOnlyList<Endpoint> GetGroupedEndpoints(RouteGroupContext context) { if (_dataSources.Count is 0) { return Array.Empty<Endpoint>(); } // We could try to optimize the single data source case by returning its result directly like GroupDataSource does, // but the CompositeEndpointDataSourceTest class was picky about the Endpoints property creating a shallow copy, // so we'll shallow copy here for consistency. var groupedEndpoints = new List<Endpoint>(); foreach (var dataSource in _dataSources) { groupedEndpoints.AddRange(dataSource.GetGroupedEndpoints(context)); } // There's no need to cache these the way we do with _endpoints. This is only ever used to get intermediate results. // Anything using the DataSourceDependentCache like the DfaMatcher will resolve the cached Endpoints property. return groupedEndpoints; } /// <inheritdoc/> public void Dispose() { // CompositeDataSource is registered as a singleton by default by AddRouting(). // UseEndpoints() adds all root data sources to this singleton. List<IDisposable>? disposables = null; lock (_lock) { _disposed = true; if (_dataSources is ObservableCollection<EndpointDataSource> observableDataSources) { observableDataSources.CollectionChanged -= OnDataSourcesChanged; } foreach (var dataSource in _dataSources) { if (dataSource is IDisposable disposableDataSource) { disposables ??= new List<IDisposable>(); disposables.Add(disposableDataSource); } } if (_changeTokenRegistrations is { Count: > 0 }) { disposables ??= new List<IDisposable>(); disposables.AddRange(_changeTokenRegistrations); } } // Dispose everything outside of the lock in case a registration is blocking on HandleChange completing // on another thread or something. if (disposables is not null) { foreach (var disposable in disposables) { disposable.Dispose(); } } } // Defer initialization to avoid doing lots of reflection on startup. [MemberNotNull(nameof(_endpoints))] private void EnsureEndpointsInitialized() { if (_endpoints is not null) { return; } lock (_lock) { if (_endpoints is not null) { return; } // Now that we're caching the _enpoints, we're responsible for keeping them up-to-date even if the caller // hasn't started listening for changes themselves yet. EnsureChangeTokenInitialized(); // Note: we can't use DataSourceDependentCache here because we also need to handle a list of change // tokens, which is a complication most of our code doesn't have. CreateEndpointsUnsynchronized(); } } [MemberNotNull(nameof(_consumerChangeToken))] private void EnsureChangeTokenInitialized() { if (_consumerChangeToken is not null) { return; } lock (_lock) { if (_consumerChangeToken is not null) { return; } // This is our first time initializing the change token, so the collection has "changed" from nothing. CreateChangeTokenUnsynchronized(collectionChanged: true); } } private void HandleChange(bool collectionChanged) { CancellationTokenSource? oldTokenSource = null; List<IDisposable>? oldChangeTokenRegistrations = null; lock (_lock) { if (_disposed) { return; } // Prevent consumers from re-registering callback to in-flight events as that can // cause a stack overflow. // Example: // 1. B registers A. // 2. A fires event causing B's callback to get called. // 3. B executes some code in its callback, but needs to re-register callback // in the same callback. oldTokenSource = _cts; oldChangeTokenRegistrations = _changeTokenRegistrations; // Don't create a new change token if no one is listening. if (oldTokenSource is not null) { // We have to hook to any OnChange callbacks before caching endpoints, // otherwise we might miss changes that occurred to one of the _dataSources after caching. CreateChangeTokenUnsynchronized(collectionChanged); } // Don't update endpoints if no one has read them yet. if (_endpoints is not null) { // Refresh the endpoints from data source so that callbacks can get the latest endpoints. CreateEndpointsUnsynchronized(); } } // Disposing registrations can block on user defined code on running on other threads that could try to acquire the _lock. if (collectionChanged && oldChangeTokenRegistrations is not null) { foreach (var registration in oldChangeTokenRegistrations) { registration.Dispose(); } } // Raise consumer callbacks. Any new callback registration would happen on the new token created in earlier step. // Avoid raising callbacks inside a lock. oldTokenSource?.Cancel(); } [MemberNotNull(nameof(_consumerChangeToken))] private void CreateChangeTokenUnsynchronized(bool collectionChanged) { var cts = new CancellationTokenSource(); if (collectionChanged) { _changeTokenRegistrations = new(); foreach (var dataSource in _dataSources) { _changeTokenRegistrations.Add(ChangeToken.OnChange( dataSource.GetChangeToken, () => HandleChange(collectionChanged: false))); } } _cts = cts; _consumerChangeToken = new CancellationChangeToken(cts.Token); } [MemberNotNull(nameof(_endpoints))] private void CreateEndpointsUnsynchronized() { var endpoints = new List<Endpoint>(); foreach (var dataSource in _dataSources) { endpoints.AddRange(dataSource.Endpoints); } // Only cache _endpoints after everything succeeds without throwing. // We don't want to create a negative cache which would cause 404s when there should be 500s. _endpoints = endpoints; } // Use private variable '_endpoints' to avoid initialization private string DebuggerDisplayString => GetDebuggerDisplayStringForEndpoints(_endpoints); }