File: Provisioning\ResourceDeploymentGraph.cs
Web Access
Project: src\src\Aspire.Hosting.Azure\Aspire.Hosting.Azure.csproj (Aspire.Hosting.Azure)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
using Aspire.Hosting.ApplicationModel;
 
namespace Aspire.Hosting.Azure;
 
internal sealed class ResourceDeploymentGraph
{
    private Dictionary<IResource, HashSet<IResource>> Dependencies { get; } = [];
    private HashSet<IResource> Nodes { get; } = [];
 
    public static ResourceDeploymentGraph Build(IEnumerable<IResource> resources)
    {
        var graph = new ResourceDeploymentGraph();
 
        foreach (var resource in resources)
        {
            graph.Nodes.Add(resource);
            graph.Dependencies[resource] = [];
        }
 
        foreach (var resource in resources)
        {
            if (resource is AzureBicepResource bicepResource)
            {
                foreach (var parameter in bicepResource.Parameters.Values)
                {
                    HashSet<IResource> referencedResources;
 
                    referencedResources = [.. ExtractBicepResourceReferences(parameter)];
 
                    foreach (var referencedResource in referencedResources)
                    {
                        graph.Dependencies[resource].Add(referencedResource);
                    }
                }
            }
        }
 
        return graph;
    }
 
    public async Task ExecuteAsync<TContext>(
        Func<IResource, TContext, Task> executor,
        TContext context,
        CancellationToken cancellationToken = default)
    {
        var stateLock = new object();
        var inProgress = new Dictionary<IResource, Task>();
        var completed = new HashSet<IResource>();
 
        while (true)
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            var availableNodes = GetAvailableNodes(completed, inProgress.Keys, stateLock);
 
            if (!availableNodes.Any())
            {
                if (inProgress.Count != 0)
                {
                    var completedTask = await Task.WhenAny(inProgress.Values).ConfigureAwait(false);
                    await HandleTaskCompletion(completedTask, inProgress, completed, stateLock).ConfigureAwait(false);
                    continue;
                }
                break;
            }
 
            foreach (var node in availableNodes)
            {
                var task = ExecuteNodeAsync(node, context, executor);
                inProgress.TryAdd(node, task);
            }
 
            if (inProgress.Count != 0)
            {
                var completedTask = await Task.WhenAny(inProgress.Values).ConfigureAwait(false);
                await HandleTaskCompletion(completedTask, inProgress, completed, stateLock).ConfigureAwait(false);
            }
        }
    }
 
    private IEnumerable<IResource> GetAvailableNodes(
        HashSet<IResource> completed,
        ICollection<IResource> inProgress,
        object stateLock)
    {
        return Nodes.Where(node =>
        {
            lock (stateLock)
            {
                return !completed.Contains(node) &&
                       !inProgress.Contains(node) &&
                       Dependencies[node].All(dep => completed.Contains(dep));
            }
        });
    }
 
    private static async Task HandleTaskCompletion(
        Task completedTask,
        Dictionary<IResource, Task> inProgress,
        HashSet<IResource> completed,
        object stateLock)
    {
        var completedNode = inProgress.First(kvp => kvp.Value == completedTask).Key;
        inProgress.Remove(completedNode);
 
        await completedTask.ConfigureAwait(false);
 
        lock (stateLock)
        {
            completed.Add(completedNode);
        }
    }
 
    private static async Task ExecuteNodeAsync<TContext>(
        IResource node,
        TContext context,
        Func<IResource, TContext, Task> executor)
    {
        await executor(node, context).ConfigureAwait(false);
    }
 
    internal static HashSet<AzureBicepResource> ExtractBicepResourceReferences(object? parameterValue)
    {
        var references = new HashSet<AzureBicepResource>();
 
        switch (parameterValue)
        {
            case BicepOutputReference outputRef:
                references.Add(outputRef.Resource);
                break;
 
            case IValueWithReferences valueWithRefs:
                foreach (var reference in valueWithRefs.References)
                {
                    references.UnionWith(ExtractBicepResourceReferences(reference));
                }
                break;
        }
 
        return references;
    }
}