See the question and my original answer on StackOverflow

Here is a solution based on a TaskScheduler, not related to Winforms nor WPF. It allows you to use all Task-related functions and tooling:

static void Main(string[] args)
{
    // one instance only is needed
    var scheduler = new SingleThreadTaskScheduler(thread =>
    {
        // configure it for STA
        thread.SetApartmentState(System.Threading.ApartmentState.STA);
    });

    using var fsw = new FileSystemWatcher(@"c:\temp");
    fsw.Created += onEvent;
    fsw.Changed += onEvent;
    fsw.Deleted += onEvent;
    fsw.Renamed += onRenamed;
    fsw.EnableRaisingEvents = true;

    // press any key to stop
    Console.ReadKey(true);

    void onEvent(object sender, FileSystemEventArgs e)
    {
        Task.Factory.StartNew(() => { Console.WriteLine(e.FullPath);  /* do something in STA */ }, CancellationToken.None, TaskCreationOptions.None, scheduler);
    }

    void onRenamed(object sender, RenamedEventArgs e)
    {
        Task.Factory.StartNew(() => { Console.WriteLine(e.FullPath);  /* do something in STA */ }, CancellationToken.None, TaskCreationOptions.None, scheduler);
    }
}

public sealed class SingleThreadTaskScheduler : TaskScheduler, IDisposable
{
    private readonly AutoResetEvent _stop = new AutoResetEvent(false);
    private readonly AutoResetEvent _dequeue = new AutoResetEvent(false);
    private readonly ConcurrentQueue<Task> _tasks = new ConcurrentQueue<Task>();
    private readonly Thread _thread;

    public event EventHandler Executing;

    public SingleThreadTaskScheduler(Action<Thread> threadConfigure = null)
    {
        _thread = new Thread(SafeThreadExecute) { IsBackground = true };
        threadConfigure?.Invoke(_thread);
        _thread.Start();
    }

    public DateTime LastDequeue { get; private set; }
    public bool DequeueOnDispose { get; set; }
    public int DisposeThreadJoinTimeout { get; set; } = 1000;
    public int WaitTimeout { get; set; } = 1000;
    public int DequeueTimeout { get; set; }
    public int QueueCount => _tasks.Count;

    public void ClearQueue() => Dequeue(false);
    public bool TriggerDequeue()
    {
        if (DequeueTimeout <= 0)
            return _dequeue != null && _dequeue.Set();

        var ts = DateTime.Now - LastDequeue;
        if (ts.TotalMilliseconds < DequeueTimeout)
            return false;

        LastDequeue = DateTime.Now;
        return _dequeue != null && _dequeue.Set();
    }

    public void Dispose()
    {
        _stop.Set();
        _stop.Dispose();
        _dequeue.Dispose();
        if (DequeueOnDispose)
        {
            Dequeue(true);
        }

        if (_thread != null && _thread.IsAlive)
        {
            _thread.Join(DisposeThreadJoinTimeout);
        }
    }

    private int Dequeue(bool execute)
    {
        var count = 0;
        do
        {
            if (!_tasks.TryDequeue(out var task))
                break;

            if (execute)
            {
                Executing?.Invoke(this, EventArgs.Empty);
                TryExecuteTask(task);
            }
            count++;
        }
        while (true);
        return count;
    }

    private void SafeThreadExecute()
    {
        try
        {
            ThreadExecute();
        }
        catch
        {
            // continue
        }
    }

    private void ThreadExecute()
    {
        do
        {
            if (_stop == null || _dequeue == null)
                return;

            _ = Dequeue(true);

            // note: Stop must be first in array (in case both events happen at the same exact time)
            var i = WaitHandle.WaitAny(new[] { _stop, _dequeue }, WaitTimeout);
            if (i == 0)
                break;

            // note: we can dequeue on _dequeue event, or on timeout
            _ = Dequeue(true);
        }
        while (true);
    }

    protected override void QueueTask(Task task)
    {
        if (task == null)
            throw new ArgumentNullException(nameof(task));

        _tasks.Enqueue(task);
        TriggerDequeue();
    }

    protected override IEnumerable<Task> GetScheduledTasks() => _tasks;
    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => false;
}