File: EndpointNameAddressScheme.cs
Web Access
Project: src\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.Frozen;
using System.Text;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Routing;

internal sealed class EndpointNameAddressScheme : IEndpointAddressScheme<string>, IDisposable
{
    private readonly DataSourceDependentCache<FrozenDictionary<string, Endpoint[]>> _cache;

    public EndpointNameAddressScheme(EndpointDataSource dataSource)
    {
        _cache = new DataSourceDependentCache<FrozenDictionary<string, Endpoint[]>>(dataSource, Initialize);
    }

    // Internal for tests
    internal FrozenDictionary<string, Endpoint[]> Entries => _cache.EnsureInitialized();

    public IEnumerable<Endpoint> FindEndpoints(string address)
    {
        ArgumentNullException.ThrowIfNull(address);

        // Capture the current value of the cache
        var entries = Entries;

        entries.TryGetValue(address, out var result);
        return result ?? Array.Empty<Endpoint>();
    }

    private static FrozenDictionary<string, Endpoint[]> Initialize(IReadOnlyList<Endpoint> endpoints)
    {
        // Collect duplicates as we go, blow up on startup if we find any.
        var hasDuplicates = false;

        var entries = new Dictionary<string, Endpoint[]>(StringComparer.Ordinal);
        for (var i = 0; i < endpoints.Count; i++)
        {
            var endpoint = endpoints[i];

            var endpointName = GetEndpointName(endpoint);
            if (endpointName == null)
            {
                continue;
            }

            if (!entries.TryGetValue(endpointName, out var existing))
            {
                // This isn't a duplicate (so far)
                entries[endpointName] = new[] { endpoint };
                continue;
            }
            else
            {
                // Ok this is a duplicate, because we have two endpoints with the same name. Collect all the data
                // so we can throw an exception. The extra allocations here don't matter since this is an exceptional case.
                hasDuplicates = true;

                var newEntry = new Endpoint[existing.Length + 1];
                Array.Copy(existing, newEntry, existing.Length);
                newEntry[existing.Length] = endpoint;
                entries[endpointName] = newEntry;
            }
        }

        if (!hasDuplicates)
        {
            // No duplicates, success!
            return entries.ToFrozenDictionary(StringComparer.Ordinal);
        }

        // OK we need to report some duplicates.
        var builder = new StringBuilder();
        builder.AppendLine(Resources.DuplicateEndpointNameHeader);

        foreach (var group in entries)
        {
            if (group.Key is not null && group.Value.Length > 1)
            {
                builder.AppendLine();
                builder.AppendLine(Resources.FormatDuplicateEndpointNameEntry(group.Key));

                foreach (var endpoint in group.Value)
                {
                    builder.AppendLine(endpoint.DisplayName);
                }
            }
        }

        throw new InvalidOperationException(builder.ToString());

        static string? GetEndpointName(Endpoint endpoint)
        {
            if (endpoint.Metadata.GetMetadata<ISuppressLinkGenerationMetadata>()?.SuppressLinkGeneration == true)
            {
                // Skip anything that's suppressed for linking.
                return null;
            }

            return endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName;
        }
    }

    public void Dispose()
    {
        _cache.Dispose();
    }
}