File: ApplicationModel\ValueSnapshot.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.
 
namespace Aspire.Hosting.ApplicationModel;
 
/// <summary>
/// Provides an asynchronously initialized value that:
/// - Can be awaited via GetValueAsync() until the first value or exception is set.
/// - Exposes the latest value after it has been set (supports re-setting).
/// - Tracks whether a value or exception was ever set via IsValueSet.
/// - Supports setting an exception that will be thrown by GetValueAsync.
/// 
/// Thread-safe for concurrent SetValue / SetException / GetValueAsync calls.
/// </summary>
public sealed class ValueSnapshot<T> where T : notnull
{
    private readonly TaskCompletionSource<T> _firstValueTcs =
        new(TaskCreationOptions.RunContinuationsAsynchronously);
 
    private readonly object _lock = new();
    private Task<T>? _currentValue;
 
    /// <summary>
    /// True once a value or exception has been set at least once.
    /// </summary>
    public bool IsValueSet => _firstValueTcs.Task.IsCompleted;
 
    /// <summary>
    /// Await the current value:
    /// - If a value has already been set, returns it immediately.
    /// - If an exception has been set, throws it.
    /// - Otherwise waits until the first value or exception is set.
    /// Always returns the latest value at the moment of completion or throws the exception.
    /// </summary>
    public Task<T> GetValueAsync(CancellationToken cancellationToken = default)
    {
        lock (_lock)
        {
            // Return updated value if it exists, otherwise return the first value task
            return _currentValue ?? _firstValueTcs.Task.WaitAsync(cancellationToken);
        }
    }
 
    /// <summary>
    /// Sets (or updates) the value. The first successful call completes any
    /// pending GetValueAsync waiters. Subsequent calls replace the current value.
    /// </summary>
    public void SetValue(T value)
    {
        lock (_lock)
        {
            if (!_firstValueTcs.TrySetResult(value))
            {
                _currentValue = Task.FromResult(value);
            }
        }
    }
 
    /// <summary>
    /// Sets an exception that will be thrown by GetValueAsync.
    /// The first successful call (either SetValue or SetException) completes any
    /// pending GetValueAsync waiters.
    /// </summary>
    public void SetException(Exception exception)
    {
        ArgumentNullException.ThrowIfNull(exception);
 
        lock (_lock)
        {
            if (!_firstValueTcs.TrySetException(exception))
            {
                _currentValue = Task.FromException<T>(exception);
            }
        }
    }
}