Что Такое Промежуточное ПО и Его Подводные Камни. Окончание
Начало
Изменение ответа после начала его отправки
В данном случае промежуточное ПО обновляет код ответа после возврата управления из вызова
next():public async Task InvokeAsync(HttpContext ctx)
{
Console.WriteLine($"Запрос: {ctx.Request.Path}");
await _next(ctx);
// Изменение статуса и ответа
ctx.Response.StatusCode =
StatusCodes.Status503ServiceUnavailable;
await ctx.Response.WriteAsync(
"<p>Эта страница недоступна</p>");
Console.WriteLine($"Ответ: {ctx.Response.StatusCode}");
}
Это приводит к возникновению исключения: System.InvalidOperationException: StatusCode cannot be set because the response has already started. (StatusCode нельзя установить, так как отправка ответа уже началась)
После того как отправка ответа началась, изменить код состояния или снова записать данные в ответ уже невозможно. Однако можно выполнить необходимые действия непосредственно перед началом отправки, воспользовавшись событием
context.Response.OnStarting:public async Task InvokeAsync(HttpContext ctx)
{
Console.WriteLine($"Request: {ctx.Request.Path}");
ctx.Response.OnStarting(async() =>
{
if (ctx.Response.HasStarted)
return;
// Изменение статуса и ответа
ctx.Response.StatusCode =
StatusCodes.Status503ServiceUnavailable;
await ctx.Response.WriteAsync(
"<p>Эта страница недоступна</p>");
});
await _next(context);
Console.WriteLine($"Ответ: {ctx.Response.StatusCode}");
}
В методе
OnStarting сначала проверяется свойство HasStarted, и, если оно уже имеет значение true, выполнение метода досрочно завершается. В противном случае в ответ выводится сообщение о том, что страница в данный момент недоступна.Удержание scoped-сервисов после завершения обработки запроса
public class MyScopedService
: IMyScopedService, IDisposable
{
public bool Disposed { get; private set; }
public void Test()
{
_logger.LogInformation(
"MyScopedService.Test() успешно выполнено");
}
public void Dispose()
{
Disposed = true;
}
}
Добавим делегат OnCompleted в промежуточное ПО и создадим новую фоновую задачу, которая будет выполнена с задержкой в 5 секунд:
public class RequestLoggingMiddleware
{
//…
public async Task InvokeAsync(
HttpContext ctx,
IMyScopedService mySvc)
{
_logger.LogInformation($"Запрос: {ctx.Request.Path}");
//…
ctx.Response.OnCompleted(() =>
{
var _ = Task.Run(async() =>
{
await Task.Delay(5000);
try
{
mySvc.Test();
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
});
return Task.CompletedTask;
});
//…
}
}
Код выполняется, и на первый взгляд всё в порядке. Страница отображается корректно, а в логах видно, что метод
MyScopedService.Test() срабатывает при каждом запросе. Однако здесь кроется скрытая проблема, зависящая от того, как именно вы используете этот сервис.Проблема в том, что после завершения формирования ответа scoped-сервис уже оказывается утилизированным (disposed), поскольку он был внедрён в метод
InvokeAsync. Чтобы продемонстрировать это, добавьте в MyScopedService проверку, которая будет выбрасывать исключение, если сервис уже утилизирован.public class MyScopedService
: IMyScopedService, IDisposable
{
//…
public void Test()
{
ObjectDisposedException.ThrowIf(Disposed, this);
_logger.LogInformation(
"MyScopedService.Test() успешно выполнено");
}
//…
}
Страница по-прежнему отображается корректно, но в логах теперь исключение: Cannot access a disposed object (Попытка доступа к объекту, который уже был утилизирован).
Более надёжный подход — создать новую область видимости для подобных фоновых задач, а не повторно использовать экземпляр, привязанный к области видимости текущего запроса:
public class RequestLoggingMiddleware
{
//…
public async Task InvokeAsync(
HttpContext ctx,
IMyScopedService mySvc,
IServiceScopeFactory scopeFactory)
{
//…
ctx.Response.OnCompleted(() =>
{
var _ = Task.Run(async() =>
{
// Создаём новую область видимости
using var scope =
_scopeFactory.CreateScope();
mySvc = scope.ServiceProvider
.GetRequiredService<IMyScopedService>();
await Task.Delay(5000);
try
{
mySvc.Test();
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
_logger.LogInformation("Task completed");
});
return Task.CompletedTask;
});
await _next(ctx);
//…
}
}
Благодаря внедрению
IServiceScopeFactory в промежуточное ПО и созданию новой области видимости (scope) внутри фоновой задачи, mySvc теперь разрешается в рамках этой новой области, а не той, что привязана к исходному запросу; следовательно, сервис не уничтожается к моменту выполнения отложенной операции.Источник: https://www.roundthecode.com/dotnet-tutorials/what-is-middleware-aspnet-core-and-gotchas