TGViewer
.NET Разработчик .NET Разработчик @netdeveloperdiary · 6.75K subscribers
Post #2994 2.2K
День 2492. #ЗаметкиНаПолях
Паттерн Идемпотентный Потребитель. Продолжение

Начало

Реализация «Идемпотентного потребителя»
Вот пример идемпотентного потребителя события создания заметки NoteCreated:
internal class NoteCreatedConsumer(
DbContext dbContext,
// … кэш, логгер и т.п.
)
: IConsumer<NoteCreated>
{
public async Task
ConsumeAsync(ConsumeContext<NoteCreated> ctx)
{
// Проверяем, получали ли мы это сообщение
if (await dbCtx
.MessageConsumers.AnyAsync(c =>
c.MessageId == ctx.MessageId &&
c.ConsumerName == nameof(NoteCreatedConsumer)))
return;

using var transaction = await
dbCtx.Database.BeginTransactionAsync();

// … сохраняем заметку в базе

// Записываем, что сообщение обработано
dbCtx.MessageConsumers
.Add(new MessageConsumer
{
MessageId = ctx.MessageId,
ConsumerName = nameof(NoteCreatedConsumer),
ConsumedAtUtc = DateTime.UtcNow
});

await dbContext.SaveChangesAsync();
await transaction.CommitAsync();

// … обновляем кэш, пишем в лог
}
}


Важные детали
1. Ключ Идемпотентности
if (await dbCtx
.MessageConsumers.AnyAsync(c =>
c.MessageId == ctx.MessageId &&
c.ConsumerName == nameof(NoteCreatedConsumer)))
return;

Используем:
- MessageId из контекста передачи сообщений (ctx.MessageId);
- ConsumerName (чтобы несколько получателей могли безопасно обрабатывать одно и то же сообщение).
При поступлении дублирующего сообщения выполняется быстрый выход, и ничего не происходит.
Также важно иметь ограничение уникальности (MessageId, ConsumerName) в таблице MessageConsumers для предотвращения гонок. Таким образом, даже при параллельной обработке одного и того же сообщения только одна сможет вставить запись.

2. Атомарные побочные эффекты + идемпотентность записи
Обработка и сохранение записи получателя сообщения происходят в одной транзакции, поэтому:
- Если обработка завершается неудачей, в таблице MessageConsumers нет записи, поэтому сообщение можно отправить повторно.
- Если обработка завершается успешно, то и заметка, и строка в MessageConsumers фиксируются одновременно.
- Вы никогда не окажетесь в состоянии, когда работа выполнена, но сообщение не помечено как обработанное, и наоборот.

3. Обработка доставки по принципу «как минимум один раз»
Большинство реалистичных конфигураций выполняются по принципу «как минимум один раз»:
- Потребитель обрабатывает сообщение;
- Сбой подтверждения / тайм-аут;
- Брокер повторно доставляет;
- Ваш код выполняется снова.
При использовании этого шаблона второй запуск обращается к таблице MessageConsumers и завершается раньше времени.

Нет дублирования побочных эффектов.

Это работает, за исключением одного нюанса…

Окончание следует…

Источник:
https://www.milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it
  • 👍 10
More from @netdeveloperdiary
  1. Sep 30, 2026Post #3356
  2. Sep 29, 2026Фото 3 (с) Анатолий Кулаков
  3. Sep 29, 2026День 2799. Конференция DotNext 2026. Часть 1 25 и 26 сентября в Москве прошла очередная ко…
  4. Sep 28, 2026День 2798. #Оффтоп Утиная Типизация в C# с Помощью Перехватчиков. Часть 2 Некоторое время…
  5. Sep 27, 2026День 2797. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Окончание Начало Продол…
  6. Sep 26, 2026День 2796. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Продолжение Начало Три…
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 →