BufferBlock.ReceiveAsync挂在HostedService中

时间:2019-01-08 10:41:59

标签: c# multithreading asp.net-core async-await

我正在尝试在ASP.NET Core应用程序中使用IHostedService作为即发即弃的电子邮件发件人。看起来最好的方法是使用BufferBlock类。问题是,即使我将新项目发布到ReceiveAsync中,BufferBlock也无法完成。

这是HostedService基类:

public abstract class HostedService
{
    private Task _executingTask;
    private CancellationTokenSource _cts;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        _executingTask = ExecuteAsync(_cts.Token);
        return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        if (_executingTask == null)
        {
            return;
        }

        _cts.Cancel();
        await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));
        cancellationToken.ThrowIfCancellationRequested();
    }

    protected abstract Task ExecuteAsync(CancellationToken cancellationToken);
}

我的EmailService的来源如下:

public sealed class EmailService : HostedService, IEmailService
{
    private readonly ISendEmail _emailClient;
    private readonly BufferBlock<MailMessage> _emailQueue;

    public EmailService(ISendEmail emailClient)
    {
        _emailClient = emailClient;
        _emailQueue = new BufferBlock<MailMessage>();
    }

    public void EnqueueEmail(MailMessage email)
    {
        var accepted = _emailQueue.Post(email);
    }

    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            var nextEmail = await _emailQueue.ReceiveAsync(cancellationToken).ConfigureAwait(false);
            await _emailClient.SendMailAsync(nextEmail);
        }
    }
}

IEmailService界面只是一个简单的即弃方法:

public interface IEmailService : IHostedService
{
    void EnqueueEmail(MailMessage email);
}

所以这一切就足够了。在我的控制器中,我应该能够注入IEmailService,然后根据需要使消息入队。问题是当我运行以下测试时:

[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(10)]
public async Task Emails_are_sent_after_they_are_enqueued(int emailCount)
{
    for (var i = 0; i < emailCount; ++i)
    {
        _emailService.EnqueueEmail(new MailMessage());
    }

    await _testEmailClient.WaitForEmailsToBeSentAsync(emailCount);
}

ReceiveAsync方法永远不会完成。我尝试使用ConfigureAwait(false),但这似乎没有效果。

在我的测试中,HostedService由ASP.NET Core管道启动,并输入ExecuteAsync。我希望ReceiveAsync中有可用项目时,BufferBlock会完成,但是我一定缺少一些线程上的细微之处。

1 个答案:

答案 0 :(得分:0)

问题是我的IoC容器正在连接IEmailService的多个实例,并且正在调用ReceiveAsync的实例与正在调用Post的实例不同。这是因为EmailServiceIEmailServiceIHostedService的实例。

答案是完全抛弃IEmailService。要在生产代码中使用EmailService,我可以注入一个IEnumerable<IHostedService>实例,然后使用EmailService从该集合中提取我的OfType<EmailService>().First()