File: Publishing\PublishingActivityProgressReporter.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.
 
#pragma warning disable ASPIREPUBLISHERS001
 
using System.Diagnostics.CodeAnalysis;
using System.Threading.Channels;
 
namespace Aspire.Hosting.Publishing;
 
/// <summary>
/// Represents a publishing activity.
/// </summary>
[Experimental("ASPIREPUBLISHERS001")]
public sealed class PublishingActivity
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PublishingActivity"/> class.
    /// </summary>
    /// <param name="id">The unique identifier for the publishing activity.</param>
    /// <param name="isPrimary">Indicates whether this activity is the primary activity.</param>
    public PublishingActivity(string id, bool isPrimary = false)
    {
        Id = id;
        IsPrimary = isPrimary;
    }
 
    /// <summary>
    /// Unique Id of the publishing activity.
    /// </summary>
    public string Id { get; private set; }
 
    /// <summary>
    /// Indicates whether the publishing activity is the primary activity.
    /// </summary>
    public bool IsPrimary { get; private set; }
 
    /// <summary>
    /// The status text of the publishing activity.
    /// </summary>
    public PublishingActivityStatus? LastStatus { get; set; }
}
 
/// <summary>
/// Represents the status of a publishing activity.
/// </summary>
[Experimental("ASPIREPUBLISHERS001")]
public sealed record PublishingActivityStatus
{
    /// <summary>
    /// The publishing activity associated with this status.
    /// </summary>
    public required PublishingActivity Activity { get; init; }
 
    /// <summary>
    /// The status text of the publishing activity.
    /// </summary>
    public required string StatusText { get; init; }
 
    /// <summary>
    /// Indicates whether the publishing activity is complete.
    /// </summary>
    public required bool IsComplete { get; init; }
 
    /// <summary>
    /// Indicates whether the publishing activity encountered an error.
    /// </summary>
    public required bool IsError { get; init; }
}
 
/// <summary>
/// Interface for reporting publishing activity progress.
/// </summary>
[Experimental("ASPIREPUBLISHERS001")]
public interface IPublishingActivityProgressReporter
{
    /// <summary>
    /// Creates a new publishing activity with the specified ID.
    /// </summary>
    /// <param name="id">Unique Id of the publishing activity.</param>
    /// <param name="initialStatusText"></param>
    /// <param name="isPrimary">Indicates that this activity is the primary activity.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The publishing activity</returns>
    /// <remarks>
    /// When an activity is created the <paramref name="isPrimary"/> flag indicates whether this
    /// activity is the primary activity. When the primary activity is completed any launcher
    /// which is reading activities will stop listening for updates.
    /// </remarks>
    Task<PublishingActivity> CreateActivityAsync(string id, string initialStatusText, bool isPrimary, CancellationToken cancellationToken);
 
    /// <summary>
    /// Updates the status of an existing publishing activity.
    /// </summary>
    /// <param name="publishingActivity">The activity with updated properties.</param>
    /// <param name="statusUpdate"></param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns></returns>
    Task UpdateActivityStatusAsync(PublishingActivity publishingActivity, Func<PublishingActivityStatus, PublishingActivityStatus> statusUpdate, CancellationToken cancellationToken);
}
 
internal sealed class PublishingActivityProgressReporter : IPublishingActivityProgressReporter
{
    public async Task<PublishingActivity> CreateActivityAsync(string id, string initialStatusText, bool isPrimary, CancellationToken cancellationToken)
    {
        var publishingActivity = new PublishingActivity(id, isPrimary);
        await UpdateActivityStatusAsync(
            publishingActivity,
            (status) => status with
            {
                StatusText = initialStatusText,
                IsComplete = false,
                IsError = false
            },
            cancellationToken
            ).ConfigureAwait(false);
 
        return publishingActivity;
    }
 
    private readonly object _updateLock = new object();
 
    public async Task UpdateActivityStatusAsync(PublishingActivity publishingActivity, Func<PublishingActivityStatus, PublishingActivityStatus> statusUpdate, CancellationToken cancellationToken)
    {
        PublishingActivityStatus? lastStatus;
        PublishingActivityStatus? newStatus;
 
        lock (_updateLock)
        {
            lastStatus = publishingActivity.LastStatus ?? new PublishingActivityStatus
            {
                Activity = publishingActivity,
                StatusText = string.Empty,
                IsComplete = false,
                IsError = false
            };
            
            newStatus = statusUpdate(lastStatus);
            publishingActivity.LastStatus = newStatus;
        }
 
        if (lastStatus == newStatus)
        {
            throw new DistributedApplicationException(
                $"The status of the publishing activity '{publishingActivity.Id}' was not updated. The status update function must return a new instance of the status."
                );
        }
 
        await ActivityStatusUpdated.Writer.WriteAsync(newStatus, cancellationToken).ConfigureAwait(false);
 
        if (publishingActivity.IsPrimary && (newStatus.IsComplete || newStatus.IsError))
        {
            // If the activity is complete or an error and it is the primary activity,
            // we can stop listening for updates.
            ActivityStatusUpdated.Writer.Complete();
        }
    }
 
    internal Channel<PublishingActivityStatus> ActivityStatusUpdated { get; } = Channel.CreateUnbounded<PublishingActivityStatus>();
}