File: AzureDeploymentOperationDetails.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.
 
using System.Collections.Immutable;
using System.Text.Json.Nodes;
 
namespace Aspire.Hosting.Azure;
 
/// <summary>
/// Captures the ARM deployment operation fields Aspire surfaces while Azure provisioning is running.
/// </summary>
// ARM deployment operations are returned by:
//   GET /subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.Resources/deployments/{deploymentName}/operations
// See:
// - https://learn.microsoft.com/rest/api/resources/deployment-operations/list
// - https://learn.microsoft.com/rest/api/resources/deployment-operations/get
// The documented payload is shaped as:
//   {
//     "operationId": "...",
//     "properties": {
//       "provisioningOperation": "Create",
//       "provisioningState": "Failed",
//       "timestamp": "2026-06-11T06:39:22Z",
//       "duration": "PT15S",
//       "statusCode": "BadRequest",
//       "serviceRequestId": "...",
//       "targetResource": {
//         "id": "/subscriptions/.../providers/Microsoft.Search/searchServices/search",
//         "resourceType": "Microsoft.Search/searchServices",
//         "resourceName": "search"
//       },
//       "statusMessage": {
//         "error": {
//           "code": "LocationNotAvailableForResourceType",
//           "message": "..."
//         }
//       }
//     }
//   }
// Nested Bicep modules appear as Create operations targeting Microsoft.Resources/deployments.
// We retain the raw deployment ID and operation fields so Aspire can both render progress and
// recurse into nested deployments to find provider-level failures.
internal sealed record AzureDeploymentOperationDetails(
    string? OperationId,
    string DeploymentId,
    string? ProvisioningOperation,
    string? ProvisioningState,
    DateTimeOffset? Timestamp,
    TimeSpan? Duration,
    string? StatusCode,
    string? ServiceRequestId,
    AzureDeploymentOperationTarget? TargetResource,
    AzureProvisioningFailureDetails? FailureDetails)
{
    internal const string DeploymentResourceType = "Microsoft.Resources/deployments";
    internal const string CreateOperation = "Create";
    internal const string RunningState = "Running";
    internal const string SucceededState = "Succeeded";
    internal const string FailedState = "Failed";
    internal const string CanceledState = "Canceled";
 
    internal bool IsCreateOperation => string.Equals(ProvisioningOperation, CreateOperation, StringComparisons.AzureProvisioningOperation);
 
    // ARM templates commonly expand into nested Microsoft.Resources/deployments operations. Those
    // operations are control-plane scaffolding, not user resources, but they point to child
    // deployments that contain the provider-level status and errors we need to walk.
    internal bool IsNestedDeploymentCreate =>
        IsCreateOperation &&
        string.Equals(TargetResource?.ResourceType, DeploymentResourceType, StringComparisons.AzureResourceType);
 
    internal bool IsTerminal =>
        string.Equals(ProvisioningState, SucceededState, StringComparisons.AzureProvisioningState) ||
        string.Equals(ProvisioningState, FailedState, StringComparisons.AzureProvisioningState) ||
        string.Equals(ProvisioningState, CanceledState, StringComparisons.AzureProvisioningState);
 
    internal JsonObject ToJsonObject()
    {
        var json = new JsonObject
        {
            ["deploymentId"] = DeploymentId
        };
 
        if (!string.IsNullOrEmpty(OperationId))
        {
            json["operationId"] = OperationId;
        }
 
        if (!string.IsNullOrEmpty(ProvisioningOperation))
        {
            json["provisioningOperation"] = ProvisioningOperation;
        }
 
        if (!string.IsNullOrEmpty(ProvisioningState))
        {
            json["provisioningState"] = ProvisioningState;
        }
 
        if (Timestamp is { } timestamp)
        {
            json["timestamp"] = timestamp;
        }
 
        if (Duration is { } duration)
        {
            json["duration"] = duration.ToString();
        }
 
        if (!string.IsNullOrEmpty(StatusCode))
        {
            json["statusCode"] = StatusCode;
        }
 
        if (!string.IsNullOrEmpty(ServiceRequestId))
        {
            json["serviceRequestId"] = ServiceRequestId;
        }
 
        if (TargetResource is { } targetResource)
        {
            json["targetResource"] = targetResource.ToJsonObject();
        }
 
        if (FailureDetails is { } failureDetails)
        {
            json["error"] = failureDetails.ToJsonObject();
        }
 
        return json;
    }
}
 
internal sealed record AzureDeploymentOperationTarget(string? Id, string? ResourceType, string? ResourceName)
{
    internal JsonObject ToJsonObject()
    {
        var json = new JsonObject();
 
        if (!string.IsNullOrEmpty(Id))
        {
            json["id"] = Id;
        }
 
        if (!string.IsNullOrEmpty(ResourceType))
        {
            json["resourceType"] = ResourceType;
        }
 
        if (!string.IsNullOrEmpty(ResourceName))
        {
            json["resourceName"] = ResourceName;
        }
 
        return json;
    }
}
 
internal sealed record AzureDeploymentOperationSummary(
    ImmutableArray<AzureDeploymentOperationDetails> Operations,
    ImmutableArray<AzureDeploymentOperationDetails> RunningOperations,
    ImmutableArray<AzureDeploymentOperationDetails> SucceededOperations,
    ImmutableArray<AzureDeploymentOperationDetails> FailedOperations,
    ImmutableArray<AzureDeploymentOperationDetails> CanceledOperations)
{
    // Operations keeps the raw recursive ARM operation list for diagnostics. The grouped arrays
    // intentionally contain only displayable resource create operations so progress text does not
    // count nested deployment scaffolding as successfully provisioned resources.
    internal static AzureDeploymentOperationSummary Empty { get; } = new([], [], [], [], []);
 
    internal JsonObject ToJsonObject()
    {
        return new JsonObject
        {
            ["operations"] = ToJsonArray(Operations),
            ["running"] = ToJsonArray(RunningOperations),
            ["succeeded"] = ToJsonArray(SucceededOperations),
            ["failed"] = ToJsonArray(FailedOperations),
            ["canceled"] = ToJsonArray(CanceledOperations)
        };
    }
 
    private static JsonArray ToJsonArray(IEnumerable<AzureDeploymentOperationDetails> operations)
    {
        var array = new JsonArray();
        foreach (var operation in operations)
        {
            array.Add(operation.ToJsonObject());
        }
 
        return array;
    }
}