File: Utils\TransactionalAction.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
// Based on dotnet/sdk src/Cli/dotnet/TransactionalAction.cs
 
using System.Runtime.CompilerServices;
using System.Transactions;
 
namespace Aspire.Cli.Utils;
 
/// <summary>
/// Provides transactional file operations with automatic rollback on failure.
/// Based on patterns from dotnet/sdk.
/// </summary>
public sealed class TransactionalAction
{
    static TransactionalAction()
    {
        DisableTransactionTimeoutUpperLimit();
    }
 
    private class EnlistmentNotification(Action? commit, Action? rollback) : IEnlistmentNotification
    {
        private Action? _commit = commit;
        private Action? _rollback = rollback;
 
        public void Commit(Enlistment enlistment)
        {
            if (_commit != null)
            {
                _commit();
                _commit = null;
            }
 
            enlistment.Done();
        }
 
        public void InDoubt(Enlistment enlistment)
        {
            Rollback(enlistment);
        }
 
        public void Prepare(PreparingEnlistment enlistment)
        {
            enlistment.Prepared();
        }
 
        public void Rollback(Enlistment enlistment)
        {
            if (_rollback != null)
            {
                _rollback();
                _rollback = null;
            }
 
            enlistment.Done();
        }
    }
 
    /// <summary>
    /// Runs an action with transactional semantics.
    /// If the action throws, the rollback action is executed.
    /// </summary>
    /// <typeparam name="T">Return type.</typeparam>
    /// <param name="action">The action to perform.</param>
    /// <param name="commit">Optional action to run on successful commit.</param>
    /// <param name="rollback">Optional action to run on rollback.</param>
    /// <returns>The result of the action.</returns>
    public static T Run<T>(
        Func<T> action,
        Action? commit = null,
        Action? rollback = null)
    {
        ArgumentNullException.ThrowIfNull(action);
 
        // This automatically inherits any ambient transaction
        // If a transaction is inherited, completing this scope will be a no-op
        T result = default!;
        try
        {
            using var scope = new TransactionScope(
                TransactionScopeOption.Required,
                TimeSpan.Zero);
 
            Transaction.Current!.EnlistVolatile(
                new EnlistmentNotification(commit, rollback),
                EnlistmentOptions.None);
 
            result = action();
 
            scope.Complete();
 
            return result;
        }
        catch (TransactionAbortedException)
        {
            throw;
        }
    }
 
    /// <summary>
    /// AOT-compatible accessor for TransactionManager.s_cachedMaxTimeout private field.
    /// This is a workaround for https://github.com/dotnet/sdk/issues/21101.
    /// Uses UnsafeAccessorType (.NET 10+) to access static fields on static classes.
    /// </summary>
    [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "s_cachedMaxTimeout")]
    private static extern ref bool CachedMaxTimeoutField(
        [UnsafeAccessorType("System.Transactions.TransactionManager, System.Transactions.Local")] object? manager);
 
    /// <summary>
    /// AOT-compatible accessor for TransactionManager.s_maximumTimeout private field.
    /// This is a workaround for https://github.com/dotnet/sdk/issues/21101.
    /// Uses UnsafeAccessorType (.NET 10+) to access static fields on static classes.
    /// </summary>
    [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "s_maximumTimeout")]
    private static extern ref TimeSpan MaximumTimeoutField(
        [UnsafeAccessorType("System.Transactions.TransactionManager, System.Transactions.Local")] object? manager);
 
    /// <summary>
    /// Disables the transaction timeout upper limit using AOT-compatible accessors.
    /// This is a workaround for https://github.com/dotnet/sdk/issues/21101.
    /// Use the proper API once available.
    /// </summary>
    public static void DisableTransactionTimeoutUpperLimit()
    {
        CachedMaxTimeoutField(null) = true;
        MaximumTimeoutField(null) = TimeSpan.Zero;
    }
 
    /// <summary>
    /// Runs an action with transactional semantics.
    /// If the action throws, the rollback action is executed.
    /// </summary>
    /// <param name="action">The action to perform.</param>
    /// <param name="commit">Optional action to run on successful commit.</param>
    /// <param name="rollback">Optional action to run on rollback.</param>
    public static void Run(
        Action action,
        Action? commit = null,
        Action? rollback = null)
    {
        Run<object?>(
            action: () =>
            {
                action();
                return null;
            },
            commit: commit,
            rollback: rollback);
    }
}