TGViewer
.NET Разработчик .NET Разработчик @netdeveloperdiary · 6.74K subscribers
Post #3336 1.01K
День 2788. #ЗаметкиНаПолях #Middleware
Что Такое Промежуточное ПО и Его Подводные Камни. Окончание

Начало

Изменение ответа после начала его отправки
В данном случае промежуточное ПО обновляет код ответа после возврата управления из вызова 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
  • 👍 3
More from @netdeveloperdiary
  1. Sep 26, 2026День 2796. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Продолжение Начало Три…
  2. Sep 25, 2026День 2795. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Начало Проблема с позиц…
  3. Sep 24, 2026День 2794. #Оффтоп #Здоровье Сегодня будет необычный пост. Завтра в Москве стартует конфер…
  4. Sep 23, 2026День 2793. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  5. Sep 22, 2026День 2792. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  6. Sep 21, 2026🔍Тестовое собеседование с Senior C# разработчиком уже завтра 22 сентября(уже завтра!) в 1…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →