jbogard / MediatR

Simple, unambitious mediator implementation in .NET
Apache License 2.0
10.91k stars 1.16k forks source link

Bug: Publish() command stuck in combination with await *.ToListASYNC() #957

Closed codingyourlife closed 10 months ago

codingyourlife commented 10 months ago

Hi. I have a strange problem where calling Publish() gets stuck (command never finishes) if it is used in combination with ToListASYNC(). It works with ToList(). I prepared a minimum example. Sadly due to db dependency not a click and run but I think you can get it going quite quick.

namespace ShowcasePublishGetsStuck
{
    using System;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using Database;
    using Database.Enums;
    using Database.Models;
    using MediatorEvents.Notifications;
    using MediatR;
    using Microsoft.EntityFrameworkCore;

    public class ShowcaseToListAsyncStuck
    {
        private MyDbContext _db;
        private readonly IMediator _mediator;

        public ShowcaseToListAsyncStuck(
            MyDbContext db,
            IMediator mediator)
        {
            _mediator = mediator;

            DemoGettingStuck(CancellationToken.None).Wait(); //I run it in ctor with Wait. Not sure if important
        }

        public async Task<bool> DemoGettingStuck(CancellationToken cancellationToken)
        {
            await _mediator.Publish(new KycCompanyCreatedEvent(0), cancellationToken);

            _mediator.Publish(new KycCompanyCreatedEvent(0), cancellationToken).Wait(cancellationToken);

            for (int i = 0; i < 1; i++)
            {
                await FunctionThatCallsSimplePublish(cancellationToken); //WORKS!
            }

            var purchasesSync = _db.Purchases
                .Where(x => x.Id == 405)
                .ToList();

            foreach (var _ in purchasesSync)
            {
                await FunctionThatCallsSimplePublish(cancellationToken); //WORKS! YES EVEN THIS WORKS!
            }

            var purchasesAsync = await _db.Purchases
                .Where(x => x.Id == 405)
                .ToListAsync(cancellationToken); //HERE! It is identical just ToListAsync. Should be the same but isn't.

            foreach (var _ in purchasesAsync)
            {
                await FunctionThatCallsSimplePublish(cancellationToken); //FAIL: STUCK!?!?!?!?!?!?!?
                //NEVER REACHES HERE
            }

            return true; //NEVER REACHES HERE
        }

        public async Task FunctionThatCallsSimplePublish(CancellationToken cancellationToken = default)
        {
            await _mediator.Publish(new KycCompanyCreatedEvent(0), cancellationToken);
            await Task.Delay(0);
        }
    }
}

I use .NET 6 and ToListAsync() is available via import Microsoft.EntityFrameworkCore.

I use this nuget package of EntityFrameworkCore and generally this version number: <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.10" />

For me it doesn't make any sense why ToList() + foreach works but ToListAsync() + foreach doesn't.

MediatR I use v11.0.0 but I also tried v12.1.1. and same result. <PackageReference Include="MediatR" Version="11.0.0" />

For completeness here also my handler:

[UsedImplicitly]
public class KycCompanyCreatedEventHandler : INotificationHandler<KycCompanyCreatedEvent>
{
    private readonly IMyProvider _myProvider;
    private readonly MyDbContext _db;

    public KycCompanyCreatedEventHandler(IMyProvider myProvider, MyDbContext db)
    {
        _myProvider = myProvider;
        _db = db;
    }

    public Task Handle(KycCompanyCreatedEvent notification, CancellationToken cancellationToken)
    {
        return Task.CompletedTask; // Breakpoint here not reached on .ToListAsync() foreach
    }
}

Is this known? Is this a bug? A bug where?

jbogard commented 10 months ago

The Publish code is very simple but that Wait in the constructor is a bit worrisome. Don't do any async work in a constructor.

codingyourlife commented 10 months ago

Yes true. Thanks @jbogard !

Right. Async call with .Wait() in constructor causes issues. I cross-posted on Stackoverflow where people linked pages to read about the issue: https://stackoverflow.com/questions/77141268/mediatr-publish-command-stuck-in-combination-with-await-tolistasync.

How I solved it: I created an API endpoint for the admin to trigger the function manually