File: Sections\SectionRegistry.cs
Web Access
Project: src\aspnetcore\src\Components\Components\src\Microsoft.AspNetCore.Components.csproj (Microsoft.AspNetCore.Components)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.Components.Sections;
 
internal sealed partial class SectionRegistry(ILoggerFactory loggerFactory)
{
    private readonly Dictionary<object, SectionOutlet> _subscribersByIdentifier = new();
    private readonly Dictionary<object, List<SectionContent>> _providersByIdentifier = new();
 
    private readonly ILogger _logger = loggerFactory.CreateLogger<SectionRegistry>();
 
    private HashSet<object>? _mismatchLoggedIdentifiers;
 
    public void AddProvider(object identifier, SectionContent provider, bool isDefaultProvider)
    {
        if (!_providersByIdentifier.TryGetValue(identifier, out var providers))
        {
            providers = new();
            _providersByIdentifier.Add(identifier, providers);
        }
 
        if (isDefaultProvider)
        {
            providers.Insert(0, provider);
        }
        else
        {
            providers.Add(provider);
        }
    }
 
    public void RemoveProvider(object identifier, SectionContent provider)
    {
        if (!_providersByIdentifier.TryGetValue(identifier, out var providers))
        {
            throw new InvalidOperationException($"There are no content providers with the given section ID '{identifier}'.");
        }
 
        var index = providers.LastIndexOf(provider);
 
        if (index < 0)
        {
            throw new InvalidOperationException($"The provider was not found in the providers list of the given section ID '{identifier}'.");
        }
 
        providers.RemoveAt(index);
 
        if (index == providers.Count)
        {
            // We just removed the most recently added provider, meaning we need to change
            // the current content to that of second most recently added provider.
            var contentProvider = GetCurrentProviderContentOrDefault(providers);
            NotifyContentChangedForSubscriber(identifier, contentProvider);
 
            if (_subscribersByIdentifier.TryGetValue(identifier, out var subscriber))
            {
                DetectRenderModeMismatch(identifier, subscriber, contentProvider);
            }
        }
    }
 
    public void Subscribe(object identifier, SectionOutlet subscriber)
    {
        if (_subscribersByIdentifier.ContainsKey(identifier))
        {
            throw new InvalidOperationException($"There is already a subscriber to the content with the given section ID '{identifier}'.");
        }
 
        // Notify the new subscriber with any existing content.
        var provider = GetCurrentProviderContentOrDefault(identifier);
        subscriber.ContentUpdated(provider);
 
        _subscribersByIdentifier.Add(identifier, subscriber);
 
        DetectRenderModeMismatch(identifier, subscriber, provider);
    }
 
    public void Unsubscribe(object identifier)
    {
        if (!_subscribersByIdentifier.Remove(identifier))
        {
            throw new InvalidOperationException($"The subscriber with the given section ID '{identifier}' is already unsubscribed.");
        }
 
        // The section pair is no longer complete, so re-arm the mismatch diagnostic in case the outlet
        // and content are later reconnected in mismatched render modes.
        _mismatchLoggedIdentifiers?.Remove(identifier);
    }
 
    public void NotifyContentProviderChanged(object identifier, SectionContent provider)
    {
        if (!_providersByIdentifier.TryGetValue(identifier, out var providers))
        {
            throw new InvalidOperationException($"There are no content providers with the given section ID '{identifier}'.");
        }
 
        // We only notify content changed for subscribers when the content of the
        // most recently added provider changes.
        if (providers.Count != 0 && providers[^1] == provider)
        {
            NotifyContentChangedForSubscriber(identifier, provider);
 
            if (_subscribersByIdentifier.TryGetValue(identifier, out var subscriber))
            {
                DetectRenderModeMismatch(identifier, subscriber, provider);
            }
        }
    }
 
    private void DetectRenderModeMismatch(object identifier, SectionOutlet subscriber, SectionContent? provider)
    {
        if (provider is null)
        {
            _mismatchLoggedIdentifiers?.Remove(identifier);
            return;
        }
 
        var outletRenderMode = subscriber.SectionRenderMode;
        var contentRenderMode = provider.SectionRenderMode;
 
        if (!RenderModesDiffer(outletRenderMode, contentRenderMode))
        {
            _mismatchLoggedIdentifiers?.Remove(identifier);
            return;
        }
 
        if ((_mismatchLoggedIdentifiers ??= new()).Add(identifier))
        {
            Log.SectionRenderModeMismatch(_logger, DescribeIdentifier(identifier), DescribeRenderMode(outletRenderMode), DescribeRenderMode(contentRenderMode));
        }
    }
 
    private static bool RenderModesDiffer(IComponentRenderMode? left, IComponentRenderMode? right)
        => left?.GetType() != right?.GetType();
 
    private static string DescribeRenderMode(IComponentRenderMode? renderMode)
        => renderMode is null ? "static server-side rendering" : renderMode.GetType().Name;
 
    private static string DescribeIdentifier(object identifier)
    {
        if (identifier is string id)
        {
            return id;
        }
 
        try
        {
            return identifier.ToString() ?? "(unknown)";
        }
        catch (Exception)
        {
            return "(unknown)";
        }
    }
 
    private static SectionContent? GetCurrentProviderContentOrDefault(List<SectionContent> providers)
        => providers.Count != 0
            ? providers[^1]
            : null;
 
    private SectionContent? GetCurrentProviderContentOrDefault(object identifier)
        => _providersByIdentifier.TryGetValue(identifier, out var existingList)
            ? GetCurrentProviderContentOrDefault(existingList)
            : null;
 
    private void NotifyContentChangedForSubscriber(object identifier, SectionContent? provider)
    {
        if (_subscribersByIdentifier.TryGetValue(identifier, out var subscriber))
        {
            subscriber.ContentUpdated(provider);
        }
    }
 
    private static partial class Log
    {
        [LoggerMessage(1, LogLevel.Warning, "The section with ID '{SectionId}' has its SectionOutlet in render mode '{OutletRenderMode}' and its SectionContent in render mode '{ContentRenderMode}'. Sections cannot connect across render mode boundaries, so the outlet will not display this content once the components become interactive.", EventName = "SectionRenderModeMismatch")]
        public static partial void SectionRenderModeMismatch(ILogger logger, string sectionId, string outletRenderMode, string contentRenderMode);
    }
}