Here is a component similar in shape with the AsyncLazy<T> type (also available in the Nito.AsyncEx library by Stephen Cleary), that has a behavior tailored to your needs:
/// <summary>
/// Represents an asynchronous operation that is invoked lazily on demand, can be
/// invoked multiple times, and is subject to a non-concurrent execution policy.
/// Concurrent observers receive the result of the same operation.
/// </summary>
public class AsyncCollapseConcurrent
{
    private readonly Func<Task> _taskFactory;
    private volatile Task _task;
    public AsyncCollapseConcurrent(Func<Task> taskFactory)
    {
        ArgumentNullException.ThrowIfNull(taskFactory);
        _taskFactory = taskFactory;
    }
    public Task Task
    {
        get
        {
            Task capturedTask = _task;
            if (capturedTask is not null) return capturedTask;
            Task<Task> newTaskTask = new(_taskFactory);
            Task newTask = newTaskTask.Unwrap().ContinueWith(t =>
            {
                _task = null;
                return t;
            }, default, TaskContinuationOptions.DenyChildAttach |
                TaskContinuationOptions.ExecuteSynchronously,
                TaskScheduler.Default).Unwrap();
            capturedTask = Interlocked
                .CompareExchange(ref _task, newTask, null) ?? newTask;
            if (ReferenceEquals(capturedTask, newTask))
                newTaskTask.RunSynchronously(TaskScheduler.Default);
            return capturedTask;
        }
    }
    public TaskAwaiter GetAwaiter() => Task.GetAwaiter();
    public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext)
        => Task.ConfigureAwait(continueOnCapturedContext);
}
Usage example:
private readonly AsyncCollapseConcurrent _asyncLazy;
//...
_asyncLazy = new(() => SlowRewriteFolderAsync());
//...
await _asyncLazy;
The AsyncCollapseConcurrent ensures that the taskFactory will not be invoked concurrently, by creating a cold nested Task<Task> using the Task<T> constructor, and starting this task only in case the atomic Interlocked.CompareExchange operation succeeds. Otherwise, in case the race to update the _task field is won by another thread, the current thread discards the cold Task<Task> without starting it.
I have used this technique for implementing various AsyncLazy<T> variants, like this (with retry) or this (with expiration).
In case your SlowRewriteFolderAsync method returns a generic Task<TResult>, you can find a compatible generic AsyncCollapseConcurrent<TResult> class here.
>
– Rodrigo Salazar Aug 21 '22 at 22:08