File: ApplicationModel\ResourceCollection.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// 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;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
 
namespace Aspire.Hosting.ApplicationModel;
 
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(ApplicationResourceCollectionDebugView))]
internal sealed class ResourceCollection : IResourceCollection
{
    private readonly List<IResource> _resources = [];
    private readonly Dictionary<string, IResource> _resourcesByName = new(StringComparers.ResourceName);
 
    public ResourceCollection() { }
 
    public ResourceCollection(IEnumerable<IResource> resources)
    {
        foreach (var resource in resources)
        {
            if (!_resourcesByName.TryAdd(resource.Name, resource))
            {
                ThrowDuplicateResource(resource, _resourcesByName[resource.Name]);
            }
 
            _resources.Add(resource);
        }
    }
 
    public IResource this[int index]
    {
        get => _resources[index];
        set
        {
            var old = _resources[index];
 
            // Allow replacing with same name (same slot), but reject if a *different* slot already has this name.
            if (!StringComparers.ResourceName.Equals(old.Name, value.Name) &&
                _resourcesByName.TryGetValue(value.Name, out var existing))
            {
                ThrowDuplicateResource(value, existing);
            }
 
            _resources[index] = value;
            _resourcesByName.Remove(old.Name);
            _resourcesByName[value.Name] = value;
        }
    }
 
    public int Count => _resources.Count;
    public bool IsReadOnly => false;
 
    public void Add(IResource item)
    {
        if (!_resourcesByName.TryAdd(item.Name, item))
        {
            ThrowDuplicateResource(item, _resourcesByName[item.Name]);
        }
 
        _resources.Add(item);
    }
 
    public void Clear()
    {
        _resources.Clear();
        _resourcesByName.Clear();
    }
 
    public bool Contains(IResource item) => _resources.Contains(item);
    public void CopyTo(IResource[] array, int arrayIndex) => _resources.CopyTo(array, arrayIndex);
    public IEnumerator<IResource> GetEnumerator() => _resources.GetEnumerator();
    public int IndexOf(IResource item) => _resources.IndexOf(item);
 
    public void Insert(int index, IResource item)
    {
        if (!_resourcesByName.TryAdd(item.Name, item))
        {
            ThrowDuplicateResource(item, _resourcesByName[item.Name]);
        }
 
        _resources.Insert(index, item);
    }
 
    public bool Remove(IResource item)
    {
        if (_resources.Remove(item))
        {
            _resourcesByName.Remove(item.Name);
            return true;
        }
 
        return false;
    }
 
    public void RemoveAt(int index)
    {
        var item = _resources[index];
        _resources.RemoveAt(index);
        _resourcesByName.Remove(item.Name);
    }
 
    public bool TryGetByName(string name, [NotNullWhen(true)] out IResource? resource)
    {
        if (name is null)
        {
            resource = null;
            return false;
        }
 
        return _resourcesByName.TryGetValue(name, out resource);
    }
 
    IEnumerator IEnumerable.GetEnumerator() => _resources.GetEnumerator();
 
    [DoesNotReturn]
    private static void ThrowDuplicateResource(IResource newResource, IResource existingResource)
    {
        throw new DistributedApplicationException($"Cannot add resource of type '{newResource.GetType()}' with name '{newResource.Name}' because resource of type '{existingResource.GetType()}' with that name already exists. Resource names are case-insensitive.");
    }
 
    private sealed class ApplicationResourceCollectionDebugView(ResourceCollection collection)
    {
        [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
        public ResourceDebugView[] Items => [.. collection.Select(x => new ResourceDebugView { Resource = x })];
 
        [DebuggerDisplay("{Resource}", Name = "{Resource.Name}")]
        public sealed class ResourceDebugView
        {
            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
            public required IResource Resource { get; init; }
        }
    }
}