Middleware for sending PartialViews via email
Implementing Nkosi's suggestions was a great step forward. This way the client can also recieve the response. However, there was one more thing that could be greatly improved. While reading about Queued background tasks I realized that this is exactly what I need to speed up my middleware becasue sending emails made it hang for a short time. Queueing the task in a background service allows the middleware to return immediately and take its time for emails.

I adjusted the example code for my needs by not using a logger here. Instead I placed the Debug.Fail inside the catch clause (and changed a couple of names).

public interface IWorkItemQueue
{
    void Enqueue(Func&lt;CancellationToken, Task&gt; workItem);

    Task&lt;Func&lt;CancellationToken, Task&gt;&gt; DequeueAsync(CancellationToken cancellationToken);
}

public class WorkItemQueue : IWorkItemQueue
{
    private readonly ConcurrentQueue&lt;Func&lt;CancellationToken, Task&gt;&gt; _workItemQueue = new ConcurrentQueue&lt;Func&lt;CancellationToken, Task&gt;&gt;();

    private readonly SemaphoreSlim _signal = new SemaphoreSlim(0);

    public void Enqueue(Func&lt;CancellationToken, Task&gt; workItem)
    {
        if (workItem == null) { throw new ArgumentNullException(nameof(workItem)); }

        _workItemQueue.Enqueue(workItem);
        _signal.Release();
    }

    public async Task&lt;Func&lt;CancellationToken, Task&gt;&gt; DequeueAsync(CancellationToken cancellationToken)
    {
        await _signal.WaitAsync(cancellationToken);
        _workItemQueue.TryDequeue(out var workItem);

        return workItem;
    }
}

public class WorkItemQueueService : IHostedService
{
    private readonly IWorkItemQueue _workItemQueue;

    private readonly CancellationTokenSource _shutdown = new CancellationTokenSource();

    private Task _backgroundTask;

    public WorkItemQueueService(IWorkItemQueue workItemQueue)
    {
        _workItemQueue = workItemQueue;
    }

    #region IHostedService

    public Task StartAsync(CancellationToken cancellationToken)
    {
        // ReSharper disable once MethodSupportsCancellation - this task is not supposted to be cancelled until shutdown
        _backgroundTask = Task.Run(BackgroundProceessing);

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _shutdown.Cancel();
        return Task.WhenAny(_backgroundTask, Task.Delay(Timeout.Infinite, cancellationToken));
    }

    #endregion

    public void Enqueue(Func&lt;CancellationToken, Task&gt; workItem)
    {
        _workItemQueue.Enqueue(workItem);
    }

    private async Task BackgroundProceessing()
    {
        while (!_shutdown.IsCancellationRequested)
        {
            var workItem = await _workItemQueue.DequeueAsync(_shutdown.Token);

            try
            {
                await workItem(_shutdown.Token);
            }
            catch (Exception)
            {
                Debug.Fail("Work item should handle its own exceptions.");
            }
        }
    }
}




The updated MailerMiddleware now enqueues tasks sending emails on that queue so that the service can handle them later and also handles its own exceptions.

public class MailerMiddleware
{
    private readonly RequestDelegate _next;

    private readonly ILogger _logger;

    private readonly IWorkItemQueue _workItemQueue;

    private readonly IEmailClient _emailClient;

    public MailerMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IWorkItemQueue workItemQueue, IEmailClient emailClient)
    {
        _next = next;
        _logger = loggerFactory.CreateLogger&lt;MailerMiddleware&gt;();
        _workItemQueue = workItemQueue;
        _emailClient = emailClient;
    }

    public async Task Invoke(HttpContext context)
    {
        var bodyBackup = context.Response.Body;

        using (var memory = new MemoryStream())
        {
            context.Response.Body = memory;

            await _next(context);

            memory.Seek(0, SeekOrigin.Begin);
            using (var reader = new StreamReader(memory))
            {
                var body = await reader.ReadToEndAsync();

                var email = context.Email();
                if (!(email.To is null || email.Subject is null))
                {
                    _workItemQueue.Enqueue(async cancellationToken =&gt;
                    {
                        try
                        {
                            await _emailClient.SendAsync(new Email&lt;EmailSubject, EmailBody&gt;
                            {
                                To = email.To,
                                Subject = new PlainTextSubject(email.Subject),
                                Body = new ParialViewEmailBody(body),
                            });
                        }
                        catch (Exception ex)
                        {
                            _logger.Log(Abstraction.Layer.Network().Action().Failed(nameof(IEmailClient.SendAsync)), ex);
                        }
                    });
                }

                // Restore Response.Body
                memory.Seek(0, SeekOrigin.Begin);
                await memory.CopyToAsync(bodyBackup);
                context.Response.Body = bodyBackup;
            }
        }
    }

    private class ParialViewEmailBody : EmailBody
    {
        private readonly string _body;

        public ParialViewEmailBody(string body)
        {
            _body = body;
            IsHtml = true;
            Encoding = System.Text.Encoding.UTF8;
        }
        public override string ToString()
        {
            return _body;
        }
    }
}


ASP.NET-Core is really cool ;-)