.NET Разработчик
6.51K subscribers
427 photos
2 videos
14 files
2.04K links
Дневник сертифицированного .NET разработчика.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 2322. #ЧтоНовенького
Запись Логов по Требованию с Буферизацией. Начало

Обычно нужны подробные логи, когда что-то идёт не так, но когда всё хорошо, зачем платить за хранение логов, которые никогда не проверяются. Хотя выборка логов (log sampling) помогает сократить их объём, сохраняя только часть, иногда нужен другой подход.

Буферизация логов в .NET 9 временно сохраняет логи в памяти и позволяет позже решить, записывать ли их. Буферизация даёт контекстный контроль над тем, какие логи сохраняются на основе фактических результатов выполнения.

Концепция
Буферизация логов представляет новый шаблон потока:
1. Код генерирует логи обычным образом,
2. Нужные логи сохраняются в памяти, а не пишутся немедленно,
3. Позже, на основе определённых условий, вы решаете:
- Очистить буфер и записать все захваченные логи,
- Дать времени жизни буфера истечь, фактически отбрасывая ненужные логи.
Так, например, вы можете буферизировать подробные логи для транзакции, сохраняя их только в случае сбоя транзакции и отбрасывая в случае её успеха.

Две стратегии буферизации
.NET 9 предоставляет две стратегии буферизации: глобальная и по запросу.

1. Глобальная буферизация
Хранит логи в кольцевых буферах, общих для всего приложения. Это подходит для буферизации логов, связанных с длительными процессами или фоновыми задачами. Для базовых сценариев включите буферизацию:
builder.Logging
.AddGlobalLogBuffer(LogLevel.Information);

Эта конфигурация буферизует все логи от уровня Information. Для более сложных сценариев определите конкретные правила:
builder.Logging.AddGlobalBuffer(o =>
{
// Буферизация логов уровня Information, категории "PaymentService"
o.Rules.Add(
new LogBufferingFilterRule(
categoryName: "PaymentService",
logLevel: LogLevel.Information));

o.MaxBufferSizeInBytes = 100 * 1024 * 1024;
o.MaxLogRecordSizeInBytes = 50 * 1024;
o.AutoFlushDuration = TimeSpan.FromSeconds(30);
});

- MaxBufferSizeInBytes – максимальный размер буфера.
- MaxLogRecordSizeInBytes – максимальный размер отдельных записей лога.
- AutoFlushDuration – как долго буферизация лога будет оставаться отключенной после запуска операции записи (flush). В примере выше буферизация будет оставаться отключенной в течение 30 секунд, что позволит приложению нормально писать все логи.

Запись глобального буфера
Чтобы записать буферизованные логи, внедрите класс GlobalLogBuffer и вызовите его метод Flush():
public class PaymentService
{
private ILogger<PaymentService> _logger;
private GlobalLogBuffer _buffer;

public PaymentService(
ILogger<PaymentService> logger,
GlobalLogBuffer buffer)
{
_logger = logger;
_buffer = buffer;
}

public async Task ProcessPayment(
PaymentRequest req)
{
try
{
_logger.LogInformation(
"Старт обработки платежа для транзакции {TransId}", req.TransactionId);
// Обработка платежа...
_logger.LogInformation("Платёж обработан");
}
catch (Exception ex)
{
_logger.LogError(ex, "Сбой обработки");
// Записываем буфер для записи детальных логов
_buffer.Flush();
throw;
}
}
}

В этом примере подробные логи о процессе оплаты сохраняются только в случае возникновения исключения.

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

Источник:
https://devblogs.microsoft.com/dotnet/emit-logs-on-demand-with-log-buffering/
👍17
День 2323. #ЧтоНовенького
Запись Логов по Требованию с Буферизацией. Окончание

Начало

2. Буферизация по запросу
Для веб-приложений ASP.NET Core буферизация по запросу обеспечивает более детальный контроль, поддерживая отдельные буферы для каждого HTTP-запроса. Подходит для отслеживания полного контекста отдельных взаимодействий пользователей.

Добавляем буферизацию по запросу:
builder.Services.AddPerIncomingRequestBuffer(o =>
{
// Логи Information от API контроллеров
o.Rules.Add(
new LogBufferingFilterRule(
categoryPrefix: "MyApp.Controllers",
logLevel: LogLevel.Information));

o.AutoFlushDuration = TimeSpan.FromSeconds(5);
});

Ключевое отличие от глобальной буферизации - когда HTTP-запрос завершается, буфер очищается и все его логи удаляются.

Очистка буфера по запросу
Чтобы выдать буферизованные логи для определённого запроса, внедрите класс PerRequestLogBuffer:
[ApiController]
[Route("[controller]")]
public class OrderController : ControllerBase
{
private ILogger<OrderController> _logger;
private PerRequestLogBuffer _buffer;

public OrderController(
ILogger<OrderController> logger,
PerRequestLogBuffer buffer)
{
_logger = logger;
_buffer = buffer;
}

[HttpPost]
public IActionResult CreateOrder(
OrderRequest request)
{
try
{
_logger.LogInformation("Заказ для клиента {CustomerId}", request.CustomerId);
// Обработка заказа…
_logger.LogInformation("Заказ {OrderId} создан", orderId);

return Ok(new { OrderId = orderId });
}
catch (Exception ex)
{
_logger.LogError(ex, "Сбой заказа");
_buffer.Flush();
return StatusCode(500, "Сбой создания заказа");
}
}
}

Запись (flush) буфера по запросу также записывает глобальный буфер, гарантируя, что все соответствующие логи будут записаны при возникновении ошибки.

Динамические обновления конфигурации
Если вы используете провайдер конфигурации, поддерживающий перезагрузки (например, File Configuration Provider), вы можете обновить правила буферизации без перезапуска приложения.

Это особенно ценно в производственных сценариях, где может потребоваться временно увеличить детализацию логов для диагностики проблемы, а затем вернуться к обычным уровням после её устранения.

Рекомендации
1. Стратегия буферизации
Буферизуйте подробные логи с уровня Information, но позвольте логам Error и Critical записываться немедленно. Это гарантирует, что важные проблемы всегда будут видны, а подробный контекст будет доступен только при необходимости.

2. Создайте явные триггеры записи (flush)
Определите конкретные условия, при которых контекст лога имеет значение, и добавьте явные вызовы записи:
- При исключениях или ошибках,
- Когда операции превышают ожидаемую продолжительность,
- Когда обнаружено что-то подозрительное.

3. Добавьте выборку логов для максимальной эффективности
Для приложений с большим объёмом логов используйте буферизацию и выборку:
- Фильтруйте обычные логи для уменьшения общего объёма,
- Буферизируйте подробные логи для записи контекста очистки при необходимости.

Ограничения
-.NET 9 и выше,
- Точный порядок логов может не сохраняться (временные метки сохраняются),
- Области (scope) логов не поддерживаются,
- Некоторые расширенные свойства записи логов (например, ActivitySpanId) не сохраняются.

Производительность
Буферизация логов — это компромисс между памятью и хранилищем:
- Буферизованные логи потребляют память, пока они не будут либо записаны, либо отброшены,
- Записывая логи только в определённых сценариях, вы можете радикально сократить расходы на хранение.
Для больших распределённых приложений накладные расходы памяти минимальны по сравнению с потенциальной экономией хранилища.

Источник: https://devblogs.microsoft.com/dotnet/emit-logs-on-demand-with-log-buffering/
👍9
День 2324. #TipsAndTricks
Развенчиваем Миф Производительности SQL "Сначала Фильтр Потом JOIN"

В интернете часто можно встретить описание «трюка, повышающего производительность запросов в SQL», который звучит "Сначала Фильтр Потом JOIN". В нём утверждается, что вместо того, чтобы сначала объединять таблицы, а затем применять фильтр к результатам, нужно делать наоборот.

Например, вместо:
SELECT *
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.total > 500;

использовать:
SELECT *
FROM (
SELECT * FROM orders WHERE total > 500
) o
JOIN users u ON u.id = o.user_id;

Смысл в том, что БД сначала уберёт ненужные данные из одной таблицы, а потом выполнит соединение меньшего объёма, экономя время и память. Звучит логично. Но дело в том, что для современных БД этот совет не имеет смысла.

Вот пример плана выполнения (EXPLAIN ANALYZE) обоих запросов в PostgreSQL над таблицами с 10000 записями в users и 5000000 в orders.

«Неоптимальный» план запроса:
Hash Join  (cost=280.00..96321.92 rows=2480444 width=27) (actual time=1.014..641.202 rows=2499245 loops=1)
Hash Cond: (o.user_id = u.id)
-> Seq Scan on orders o (cost=0.00..89528.00 rows=2480444 width=14) (actual time=0.006..368.857 rows=2499245 loops=1)
Filter: (total > '500'::numeric)
Rows Removed by Filter: 2500755
-> Hash (cost=155.00..155.00 rows=10000 width=13) (actual time=0.998..0.999 rows=10000 loops=1)
Buckets: 16384 Batches: 1 Memory Usage: 577kB
-> Seq Scan on users u (cost=0.00..155.00 rows=10000 width=13) (actual time=0.002..0.341 rows=10000 loops=1)
Planning Time: 0.121 ms
Execution Time: 685.818 ms


«Оптимальный» план запроса:
Hash Join  (cost=280.00..96321.92 rows=2480444 width=27) (actual time=1.019..640.613 rows=2499245 loops=1)
Hash Cond: (orders.user_id = u.id)
-> Seq Scan on orders (cost=0.00..89528.00 rows=2480444 width=14) (actual time=0.005..368.260 rows=2499245 loops=1)
Filter: (total > '500'::numeric)
Rows Removed by Filter: 2500755
-> Hash (cost=155.00..155.00 rows=10000 width=13) (actual time=1.004..1.005 rows=10000 loops=1)
Buckets: 16384 Batches: 1 Memory Usage: 577kB
-> Seq Scan on users u (cost=0.00..155.00 rows=10000 width=13) (actual time=0.003..0.348 rows=10000 loops=1)
Planning Time: 0.118 ms
Execution Time: 685.275 ms

Как видите, планы запросов идентичны, и «оптимизация» ничего не добилась.

Основные операции:
- Последовательное сканирование (Seq Scan) таблицы orders с применением фильтра;
- Последовательное сканирование таблицы users;
- Операция хеширования (Hash) меньшей таблицы (users);
- Хеш-соединение по user_id.

Оптимизаторы запросов умнее вас
Современные БД используют стоимостную оптимизацию. Оптимизатор имеет статистику о таблицах: количество строк, распределение данных, наличие индекса, селективность столбцов и т.п. – и использует её для оценки стоимости различных стратегий выполнения. Современные БД, такие как PostgreSQL, MySQL и SQL Server, уже автоматически выполняют «выталкивание предикатов» и переупорядочивание соединений. Т.е. оба запроса переписываются по одному и тому же оптимальному плану. Поэтому ручная оптимизация в подзапрос не ускоряет работу, а просто затрудняет чтение SQL-кода.

Итого
Пишите понятный, читаемый SQL. Позвольте оптимизатору делать свою работу. В непонятных ситуациях используйте EXPLAIN ANALYZE, чтобы понять, что на самом деле делает БД и действительно ли один запрос быстрее другого.

Источник: https://www.milanjovanovic.tech/blog/debunking-the-filter-early-join-later-sql-performance-myth
👍9
День 2325. #TipsAndTricks
Не Изобретайте Велосипед — Конфигурация
Часто в различных решениях dotnet core, можно встретить код вроде следующего:
// Program.cs

if(EnvironmentHelper.IsLocal)
services
.AddSingleton<IClient, MockClient>();
else
services
.AddSingleton<IClient, ClientService>();

EnvironmentHelper выглядит так:
public static class EnvironmentHelper
{
public static bool IsLocal =>
{
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.Build();
return config.GetValue<bool>("IsDevelopmentEnvironment");
}
}

С этим кодом есть проблема. Каждый раз, когда вызывается EnvironmentHelper.IsLocal, он создаёт новый экземпляр ConfigurationBuilder и считывает appsettings.json с диска. Код используется по всей кодовой базе. Нехорошо. Мы можем избежать этого и использовать встроенные инструменты фреймворка вместо того, чтобы придумывать собственные решения.

Примечание: Вообще, идея регистрировать различные реализации в зависимости от того, в какой среде исполняется код, тоже не очень, но это уже другая история.

При регистрации сервисов можно использовать перегрузку, которая даёт доступ к провайдеру сервисов:
builder.Services
.AddSingleton<IClient>(provider =>
{
var env = provider.GetRequiredService<IHostEnvironment>();
if(env.IsDevelopment())
return provider.GetRequiredService<MockClient>();
return provider.GetRequiredService<ClientService>();
});

Здесь мы извлекаем IHostEnvironment из провайдера сервисов. Но это требует регистрации как MockClient, так и ClientService, поскольку мы используем контейнер для разрешения экземпляров.

Лучшим подходом является использование построителя. Он содержит свойство Environment:
var builder = WebApplication.CreateBuilder(args);

if(builder.Environment.IsDevelopment())
builder.Services
.AddSingleton<IClient, MockClient>();
else
builder.Services
.AddSingleton<IClient, ClientService>();


По умолчанию фреймворк устанавливает среду на основе значения переменной среды ASPNETCORE_ENVIRONMENT/DOTNET_ENVIRONMENT, поэтому нет никакой необходимости задействовать appsettings.json вообще.

Бонус
Если же вы не можете избавиться от текущей реализации EnvironmentHelper из-за объёма рефакторинга, можно использовать такой «костыль», чтобы хотя бы не создавать ConfigurationBuilder при каждом обращении:
public static class EnvironmentHelper
{
private static readonly Lazy<bool> _isLocal;

static EnvironmentHelper()
{
_isLocal = new Lazy<bool>(() => {
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.Build();
return config.GetValue<bool>("IsDevelopmentEnvironment");
},
LazyThreadSafetyMode.ExecutionAndPublication);
}

public static bool IsLocal => _isLocal.Value;
}


Источник: https://josef.codes/dont-reinvent-the-wheel-configuration-dotnet-core/
👍13
День 2326. #SystemDesign101
Распространённые Алгоритмы Балансировки Нагрузки


Статические алгоритмы
1. Круговой Алгоритм (Round Robin)
Клиентские запросы отправляются в различные экземпляры сервисов в последовательном порядке. Сервисы обычно должны быть без сохранения состояния.
Недостаток
- Эта простейшая версия алгоритма будет эффективно работать только в сферической среде в вакууме, где все серверы обладают почти одинаковой конфигурацией, а все входящие запросы (задачи, процессы) имеют одинаковые приоритет и продолжительность.

2. Закреплённый Круговой Алгоритм (Sticky Round Robin)
Усовершенствование кругового алгоритма. Если первый запрос Алисы отправляется в сервис A, следующие запросы также отправляются в сервис A.

3. Взвешенный Круговой Алгоритм (Weighted Round Robin)
Администратор может указать вес для каждого сервиса. Сервисы с более высоким весом обрабатывают больше запросов, чем другие.

4. Хэш IP/URL
Применяет хеш-функцию к IP или URL входящих запросов. Запросы направляются в соответствующие экземпляры на основе результата хеш-функции.
Преимущества
- Постоянство сессии – алгоритм гарантирует, что запросы от одного клиента всегда попадают на один и тот же сервер.
- Облегчает кеширование данных на стороне сервера для конкретных клиентов.
Недостатки
- Если много пользователей приходят из одного диапазона IP, один сервер может быть перегружен.
- Неэффективен в средах, где IP клиентов часто меняются (мобильные сети).
- Может привести к неравномерной нагрузке, если некоторые клиенты генерируют больше трафика.

Динамические алгоритмы
5. Наименьшее Количество Соединений (Least Connections)

Новый запрос отправляется в экземпляр сервиса с наименьшим количеством одновременных подключений.
Преимущества
- Более мощные серверы естественным образом будут обрабатывать больше запросов и, следовательно, иметь больше соединений. И напротив, менее мощные серверы будут получать меньше запросов, что предотвращает их перегрузку.
- Гибкость – если один сервер начинает работать медленнее, он будет получать меньше новых запросов.
Недостатки
- Алгоритм считает все соединения одинаковыми, не учитывая, что некоторые запросы могут быть более ресурсоёмкими.
- Вновь добавленный сервер может получить слишком много запросов, т.к. изначально у него 0 соединений.

6. Наименьшее Время Ответа (Least Time)
Новый запрос отправляется в экземпляр сервиса с самым быстрым временем ответа. Аналогичный алгоритм - Наименьший объем трафика (Least Bandwidth).
Преимущества
- Учёт текущей производительности серверов и динамическая адаптация обеспечивают оптимальный баланс и наилучший пользовательский опыт.
- Хорошо работает с серверами разной мощности и приложениями с разными характеристиками.
Недостаток
- Сложность реализации – требует постоянного мониторинга и анализа производительности серверов, отсюда повышенная нагрузка на балансировщик.

Источники:
-
https://github.com/ByteByteGoHq/system-design-101
-
https://proglib.io/p/6-glavnyh-algoritmov-balansirovki-nagruzki-2024-08-06
👍23
День 2327. #ЧтоНовенького
Вам не Хватало Vim в Windows? Представляем Edit!

Edit — это новый текстовый редактор командной строки в Windows. Edit имеет открытый исходный код. Вы можете посмотреть код на GitHub или установить его, выполнив следующую команду:
winget install Microsoft.Edit


Edit будет доступен для предварительного просмотра в программе Windows Insider в ближайшие месяцы. После этого он будет поставляться как часть Windows 11.

Как использовать?
Введите edit в командной строке или edit <your-file-name>, чтобы открыть файл. Теперь вы можете редактировать файлы непосредственно в командной строке без переключения контекста.

Возможности
Edit — это небольшой, легкий текстовый редактор. Он занимает менее 250 КБ. Он пока находится на ранней стадии разработки, но у него есть несколько готовых функций:
1. Поддержка режима мыши
Пункты меню другие элементы UI поддерживают использование мыши, а также горячие клавиши.
2. Открытие нескольких файлов
Вы можете открыть несколько файлов в Edit и переключаться между ними с помощью Ctrl+P (или щелкнув на список файлов в правом нижнем углу).
3. Поиск и замена
Вы можете найти и заменить текст с помощью Ctrl+R или выбрать Edit > Replace в меню. Также есть поддержка регистра и регулярных выражений.
4. Перенос по словам
Edit поддерживает перенос по словам. Чтобы использовать перенос по словам, вы можете использовать Alt+Z или выбрать View > Word Wrap в меню.

Зачем нам ещё один CLI-редактор?
Edit - текстовый редактор CLI по умолчанию в 64-разрядных версиях Windows. 32-разрядные версии Windows поставляются с редактором MS-DOS, но в 64-разрядных версиях редактор CLI не установлен.

В Microsoft решили, что нужен немодальный редактор для Windows (в отличие от модального редактора, в котором новым пользователям пришлось бы запоминать различные режимы работы и как переключаться между ними). К сожалению, это ограничило выбор списком редакторов, которые либо не имели собственной поддержки для Windows, либо были слишком большими, чтобы включать их в каждую версию ОС. В результате родился Edit.

Источник: https://devblogs.microsoft.com/commandline/edit-is-now-open-source/
👍8👎1
День 2328. #УрокиРазработки
Уроки 50 Лет Разработки ПО

Урок 55. У вас нет времени, чтобы совершить все ошибки, сделанные до вас

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

Кривая обучения
Кривая обучения описывает, как человек обретает навыки выполнения новой задачи или применения нового приёма в зависимости от своего опыта. В жизни мы постоянно сталкиваемся с бесчисленными кривыми обучения. Всякий раз, пытаясь сделать что-то новое, мы встаём на новую кривую. Не нужно ожидать, что весь потенциал метода раскроется с первой попытки. Когда проектные группы пробуют использовать незнакомые методы, в их планах должно быть предусмотрено время, необходимое на то, чтобы освоиться. Если им не удастся освоить новую практику, то затраченное время будет потеряно навсегда.

Вы, несомненно, заинтересованы в повышении общей продуктивности, которой позволяет добиться ваш набор приёмов. На картинке выше показано, что в самом начале вы имеете определенный уровень продуктивности, который хотите повысить с помощью усовершенствованного процесса, практики, метода или инструмента. Первый шаг — обучение и обретение некоего опыта. Ваша продуктивность немедленно падает, поскольку в периоды обучения вы не выполняете полезную работу. Продуктивность продолжает снижаться, пока вы тратите время на создание новых процессов, пытаетесь понять, как заставить работать новую технику, приобретаете новый инструмент и учитесь использовать его и т. д. По мере осваивания нового способа работы вы начинаете замечать первые успехи, а также некоторые неудачи — пилообразная часть кривой роста продуктивности. Если все пойдет хорошо, то в итоге ваши вложения окупятся и вы почувствуете, что эффективность, результативность и качество возросли. Помните о реальности кривой обучения, внедряя новые практики, и не поддавайтесь искушению сдаться до того, как вложения в обучение начнут окупаться.

Хорошие практики
Забавно, когда кто-то жалуется на другого: «Он всегда думает, что его способ лучше». Конечно, он так думает! Зачем кому-то намеренно делать что-то, выбирая плохие способы? Это было бы глупо. Проблема не в том, что кто-то считает свой способ лучшим, а в том, если он не допускает мысли, что другие могут знать лучшие способы, и не желает учиться у них.

Рецензирование коллегами даёт хорошую возможность наблюдать за способами работы, которые используют другие. В ходе таких встреч можно увидеть, как кто-то использует незнакомые функции, хитрые приёмы программирования или что-то ещё, что зажигает в вашем мозгу лампочку. Это простой способ учиться и совершенствоваться.

Люди часто заводят разговоры о лучших практиках, которые сразу же перерастают в споры о том, чья практика лучше для той или иной цели, в том или ином контексте. Всё это хорошо, но «лучшая практика» — слишком строгий термин.

Совет - собирайте арсенал хороших практик. Чтобы попасть в него, приём просто должен быть лучше, чем тот, который вы используете сейчас. По мере накопления инструментов и методов придерживайтесь тех, которые вы успешно использовали в прошлом. Заменяйте текущую технику новой, только когда новая позволяет получить превосходные результаты во всех случаях. Часто техники могут мирно сосуществовать, и тогда у вас есть возможность выбирать между ними в зависимости от ситуации. Так что овладейте обеими техниками и используйте самую простую из них, позволяющую выполнять работу.

Источник: Карл Вигерс “Жемчужины Разработки”. СПб.: Питер, 2024. Глава 7.
👍11
День 2329. #ЗаметкиНаПолях
Используем EF Core для Сохранения Дат в UTC и Отображения в Местном Времени. Начало

Если вы используете Entity Framework Core, вы можете легко сохранять даты в формате UTC в БД, а затем преобразовывать их обратно в местный часовой пояс в приложении ASP.NET Core. Это особенно важно, если ваше приложение охватывает несколько часовых поясов.

ValueConverter
ValueConverter позволяет преобразовывать значение сущности в значение провайдера, например БД SQL Server, а затем преобразовывать его обратно в модель для приложения ASP.NET Core. Так мы можем преобразовывать время в UTC при сохранении в БД, а затем обратно в местный часовой пояс при использовании в приложении.

Нам нужно создать класс, наследующий от ValueConverter. Обобщённый тип ValueConverter в качестве параметров типа типы модели и провайдера. Поскольку мы ожидаем тип DateTime и в БД, и приложении, мы можем использовать его для обоих.

Затем нужно переопределить один из базовых конструкторов ValueConverter, чтобы выполнить преобразование. Он ожидает два параметра:
1. Значение ExpressionFrom, т.е. как вы сохраняете значение в БД. Мы преобразуем значение DateTime в универсальное время.
2. Значение ExpressionTo, т.е. как оно отображается в приложении. Нам нужно указать, что тип DateTime — это UTC (так он сохраняется в БД), а затем нужно преобразовать его в местный часовой пояс.

Вот код нашего DateTimeUtcConverter:
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

public class DateTimeUtcConverter
: ValueConverter<DateTime, DateTime>
{
public DateTimeUtcConverter() : base(
d => d.ToUniversalTime(),
d => DateTime
.SpecifyKind(d, DateTimeKind.Utc)
.ToLocalTime()
)
{}
}


Применение
У нас есть такая сущность Product, установленная в DbContext:
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public DateTime Created { get; set; }
}


При создании новой сущности Product в БД, если бы мы установили свойство Created на DateTime.Now, это сохранило бы DateTime в БД с использованием локального часового пояса приложения. Это стало бы проблемой, если бы вы когда-либо изменили часовой пояс для своего приложения.

Но настройка сущности Product позволяет нам указать класс DateTimeUtcConverter в методе расширения HasConverter. Это гарантирует, что свойство Created будет сохранено как UTC в БД и преобразовано обратно в локальный часовой пояс при использовании в приложении:
public class ProductConfiguration
: IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> b)
{
b.HasKey(s => s.Id);

b.Property(s => s.Created)
.HasColumnName("CreatedUtc")
.HasColumnType("datetime")
.HasConversion(new DateTimeUtcConverter());
}
}


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

Источник:
https://www.roundthecode.com/dotnet-tutorials/use-ef-core-easily-save-dates-utc-show-local-time
👍8
День 2330. #ЗаметкиНаПолях
Используем EF Core для Сохранения Дат в UTC и Отображения в Местном Времени. Окончание

Начало

А как насчёт применить конвертацию ко всем DateTime?
Если у вас есть несколько свойств DateTime, которые необходимо хранить как UTC в БД, добавление конвертера к каждому из свойств может занять много времени. Кроме того, в будущем при добавлении свойств DateTime необходимо помнить добавлять конвертер.

Однако можно применить конвертацию ко всем свойствам DateTime в DbContext. Для этого нужно переопределить метод OnModelCreating, создать новый экземпляр DateTimeUtcConverter и выполнить цикл по каждой сущности и каждому из её свойств, применяя DateTimeUtcConverter к свойствам типа DateTime:
public class EfCoreDateTimeDbContext
: DbContext
{

protected override void OnModelCreating(
ModelBuilder mb)
{

var converter = new DateTimeUtcConverter();

foreach (var t in mb.Model.GetEntityTypes())
{
foreach (var p in t.GetProperties())
{
if (p.ClrType == typeof(DateTime))
p.SetValueConverter(сonverter);
}
}

base.OnModelCreating(mb);
}
}


Обнуляемое DateTime
Если у вас есть свойства DateTime, допускающие null, вы не сможете использовать DateTimeUtcConverter как есть. Нужно создать новый ValueConverter.

DateTimeUtcNullableConverter наследует от ValueConverter, но используя обнуляемый DateTime как для модели, так и для провайдера. Мы также проверяем, имеет ли значение дата при предоставлении параметров ExpressionFrom и ExpressionTo, и возвращаем исходный экземпляр, если его нет:
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

public class DateTimeUtcNullableConverter
: ValueConverter<DateTime?, DateTime?>
{
public DateTimeUtcNullableConverter() : base(
d => d.HasValue ? d.Value.ToUniversalTime() : d,
d => d.HasValue ? DateTime
.SpecifyKind(d.Value, DateTimeKind.Utc)
.ToLocalTime() : d)
{}
}

Для добавления конвертации ко всем свойствам типа DateTime? в DbContext, обновим перегрузку OnModelCreating, добавив новый конвертер:
public class EfCoreDateTimeDbContext
: DbContext
{

protected override void OnModelCreating(
ModelBuilder mb)
{

var converter = new DateTimeUtcConverter();
var nullableConverter =
new DateTimeUtcNullableConverter();

foreach (var t in mb.Model.GetEntityTypes())
{
foreach (var p in t.GetProperties())
{
if (p.ClrType == typeof(DateTime))
p.SetValueConverter(сonverter);

if (p.ClrType == typeof(DateTime?))
p.SetValueConverter(nullableConverter);
}
}

base.OnModelCreating(mb);
}
}


Источник: https://www.roundthecode.com/dotnet-tutorials/use-ef-core-easily-save-dates-utc-show-local-time
👍8
День 2331. #ЧтоНовенького
Вышел .NET Aspire 9.3
Команда .NET выпустила версию 9.3 .NET Aspire, в которой представлены обновления для диагностики, интеграции и рабочих процессов развёртывания. Этот выпуск направлен на улучшение опыта разработчиков путём интеграции GitHub Copilot в панель управления Aspire, расширения возможностей трассировки и упрощения развёртываний в Azure.

Интеграция GitHub Copilot в панель управления Aspire (на первой картинке выше) позволяет разработчикам анализировать журналы, исследовать ошибки в распределённых сервисах и выявлять проблемы производительности с помощью ИИ, не покидая среду панели управления разработчика. По словам команды .NET, Copilot дополняет диагностику на основе OpenTelemetry, представляя саммари журналов, интерпретируя коды ошибок и помогая выявлять основные причины в сложных сценариях трассировки.

В панель управления Aspire было добавлено контекстное меню в представлении Resource Graph (на второй картинке выше), предлагающее быстрый доступ к данным телеметрии, специфичным для ресурсов командам и URL. Как отмечено в релизе, страница Traces теперь отображает исходящие вызовы к зависимостям, таким как базы данных и кэши, даже если эти сервисы не выдают собственную телеметрию. Эти обновления призваны предоставить разработчикам более широкую наблюдаемость поведения приложений.

Дополнительные улучшения удобства использования включают сохранение имён пользовательских ресурсов в URL в логах консоли, поддержание состояния фильтра на странице ресурсов и выдачу предупреждений при приостановке сбора метрик.

Улучшения интеграции Aspire включают поддержку размещения самостоятельно управляемого экземпляра YARP через пакет Aspire.Hosting.Yarp. Кроме того, интеграция с MySQL теперь позволяет создавать базу данных во время настройки хоста приложения:
builder.AddMySql("mysql").AddDatabase("mydb");

Для контейнеризированных сервисов в выпуске представлены упрощенные API конфигурации для Postgres, Redis и SQL Server, позволяющие разработчикам указывать порты хоста и пароли с помощью метода RunAsContainer:
var sql = builder.AddAzureSqlServer("sql");
sql.RunAsContainer(c =>
{
c.WithHostPort(12455);
});


Интеграция с Azure также была расширена. Разработчики теперь могут создавать и добавлять контейнеры Blob Storage в AppHost. Были введены два новых API — AddAzureKeyVaultKeyClient и AddAzureKeyVaultCertificateClient — для упрощения доступа к Azure Key Vault для операций с ключами и сертификатами.

Функция Custom URLs в модели приложения была обновлена для поддержки относительных путей и большего контроля над видимостью. Новая перегрузка WithUrlForEndpoint позволяет прикреплять несколько конечных точек к одному ресурсу.

Для Azure App Service версия 9.3 представляет предварительную поддержку для развёртывания проектов .NET в ней. Поток развёртывания поддерживается через API AddAzureAppServiceEnvironment(…) и позволяет настраивать общие сценарии, такие как проекты .NET с одной конечной точкой, опубликованные в реестре контейнеров Azure:
builder.AddAzureAppServiceEnvironment("env");

builder.AddProject<Projects.Api>("api")
.WithExternalHttpEndpoints()
.PublishAsAzureAppServiceWebsite((infra, site) =>
{
site.SiteConfig.IsWebSocketsEnabled = true;
});


Другие изменения в этом выпуске включают дополнительные улучшения CLI, улучшения диагностических визуальных элементов и расширенные параметры конфигурации контейнера.

Источник: https://www.infoq.com/news/2025/06/dotnet-aspire-93-release/
👍8
День 2332. #ЗаметкиНаПолях #Git
Объединяем Репозитории Git, Сохраняя Историю
Объединение двух репозиториев Git может быть полезным в сценариях, когда вы хотите объединить связанные проекты в один репозиторий для более простого управления, совместной работы и контроля версий.

Этот подход особенно полезен, когда два репозитория имеют общую цель или тесно связаны, поскольку он упрощает управление зависимостями и гарантирует, что весь связанный код находится в одном месте. Кроме того, он может оптимизировать рабочие процессы, уменьшая необходимость переключения между репозиториями и упрощая отслеживание изменений в объединённой кодовой базе.

Используя Git, вы можете объединить два репозитория, сохранив их истории коммитов. Этот процесс гарантирует, что история обоих репозиториев останется нетронутой, что позволит вам отслеживать изменения и понимать эволюцию кодовой базы. Сохраняя историю коммитов, вы сохраняете ценный контекст о прошлых модификациях, авторстве и причинах конкретных изменений.
# Клонируем первый репозиторий
git clone https://github.com/mygit/project1.git project1
cd project1

# Добавляем второй репозиторий как удалённый (remote) и скачиваем
git remote add project2 https://github.com/mygit/project2.git
git fetch project2

# Сливаем второй репозиторий в первый
git merge project2/main --allow-unrelated-histories

# TODO: Разрешаем любые возникшие конфликты слияния и фиксируем изменения

# Делаем push объединённого репозитория в новую удалённую ветку
git push origin main


Источник: https://www.meziantou.net/merging-2-git-repositories-into-one.htm
👍15
День 2333. #Оффтоп
Проклятие Знания или Исправляем Всё. Начало

Всё начинается невинно. Вы переименовываете пакет файлов с помощью короткого скрипта или создаёте псевдоним для обычной команды git, чтобы сэкономить два нажатия клавиш. Может быть, вы создаёте небольшую shell-функцию для форматирования JSON из буфера обмена.

Вы даже не пытаетесь умничать. Вы просто решаете мелкие проблемы. Заставляете машину делать то, что она должна была делать изначально. И тут что-то происходит. Вы пересекаете черту. Вы смотрите на свои инструменты, среду, ОС — даже на IDE — и внезапно всё становится объектом критики. Это можно было бы переделать, это можно бы улучшить… (если захотеть).

Затем кто-то бросает вам вызов. Возможно, в качестве троллинга, возможно, шутки ради, но также с долей надежды. И тут что-то меняется. Внезапно всё переходит в разряд «Вы должны». И с этого момента мир ломается новыми и специфическими способами, которые можете видеть только вы.

Технические возможности как моральный груз
До того, как я научился программировать, сломанное ПО раздражало, но это можно было игнорировать. Годами я просто «использовал» компьютер, как потребитель. Я был тем, кого компании пытались обманом заставить купить их продукты или подписаться на услуги. Но не техническим гиком.

А сейчас я стал видеть закономерности, которые я бы не хотел видеть, находить упущения, которые я могу приписать определённому пониманию (или отсутствию такового) определённой концепции, и я могу слышать те голоса, что были в голове того компьютерно-безграмотного человека, создавшего программу, которую мне нужно отладить.

Я замечаю недостатки, как хороший хирург замечает хромоту. Какого черта этот сайт отправляет 10 Мб JavaScript кода для статической страницы? Почему вывод CLI не парсится awk? Почему эта конфигурация жёстко закодирована, когда она могла бы быть декларативной? Это не просто вопросы, это обвинения. И, к сожалению, они не прекращаются.

Теперь, когда я научился замечать это, моё восприятие ПО полностью изменилось:
- Каждая часть ПО становится списком TODO.
- Каждая система становится основой для лучшей.
- Каждое неудобство становится обвинением в бездействии.

Нужно представить себе Сизифа счастливым
Как Сизиф, мы обречены толкать камень наших систем в гору — одно исправление, один рефакторинг, один скрипт за раз. Но в отличие от истории о Сизифе, проклятие не наложено на вас каким-то богом. Мы сами создали этот камень. И мы продолжаем полировать его по пути наверх.

Я потерял счёт проектам, начатым с мысли, типа «я мог бы сделать это лучше»:
- Генератор статических веб-страниц, потому что существующие переусложнены.
- Инструмент для заметок, потому что мне не нравилось, как другие структурировали метаданные.
- Исполнитель заданий CLI, потому что Make — это что-то непонятное, а Taskfile — это ад YAML.
Список можно продолжать.

Да, я решал реальные, невинные проблемы. Но оглядываясь назад, я также подпитывал кое-что еще: навязчивое желание установить контроль. Каждый новый инструмент, который я создавал, был песочницей, которой я владел. Никаких странных ошибок. Никаких ограничений старых фреймворков. Никаких решений, с которыми я не был бы полностью согласен. Пока, конечно, все эти проекты не устаревали сами.

Кафка однажды написал, что «клетка отправилась на поиски птицы». Вот чем могут стать эти проекты. Пустые системы, которые мы продолжаем строить, ожидая цели, ясности,… спасения? Я не уверен, как ещё можно назвать это стремление создавать их.

Продолжение следует…

Источник:
https://notashelf.dev/posts/curse-of-knowing
👍18
День 2334. #Оффтоп
Проклятие Знания или Исправляем Всё. Продолжение

Начало

Энтропия непобедима
А теперь вернёмся назад. Назад, когда мы не знали, что можно лучше. ПО не остаётся сделанным. Каждое написанное вами решение начинает гнить в тот момент, когда оно появляется. Не сейчас, не потом, но в итоге. Библиотеки устаревают. API меняются. Происходит регресс производительности. Ваш некогда идеальный инструмент тихо ломается, потому что libfoo.dll с тех пор сменила десяток версий.

У меня были скрипты, которые молча давали сбой, потому что веб-сайт менял свой HTML-макет.
У меня были форматы конфигурации, которые ломались из-за изменения версий библиотек.
У меня были контейнеры Docker, которые умирали, потому что изменялся URL зеркала Alpine Linux.

В каждом случае немедленной эмоциональной реакцией было не просто неудобство, а что-то, что больше напоминало чувство вины. Я создал это, и я лучше знаю. Как я мог этого не предвидеть? Пора это исправить.

Если вы замените каждую часть системы с течением времени, это всё тот же инструмент? Он все ещё служит той же цели? А вы?

Иллюзия завершённости
Я думаю, мы лжём себе:
«Если я просто правильно настрою эту программу, мне больше никогда не придётся к ней прикасаться»
«Если я просто напишу этот инструмент, мой рабочий процесс станет безупречным»
«Если я автоматизирую это, я сэкономлю время навсегда»
«Напиши один раз, запусти везде». Ну да, конечно.

Признаю, это соблазнительная ложь. Она представляет программирование как своего рода завоевание. Серию сражений, в которых вы побеждаете, или испытаний, которые вы проходите. Но воображаемая война никогда не заканчивается. Вы не строите замок. Вы роете траншеи. И они затапливаются каждый раз, когда идет дождь. Испытания никогда не завершаются.

Техническая работа как эмоциональная регуляция
«У вас есть власть над своим разумом, а не над внешними событиями. Осознайте это, и вы обретёте силу.»

- Марк Аврелий

Но программирование соблазняет нас, заставляя верить, что мы можем контролировать внешние события. Вот где начинаются страдания. Здесь происходит что-то более глубокое. Это касается не только ПО.

Я считаю, что иногда создание вещей — это способ самоуспокоения. Мы пишем новый инструмент или сценарий, потому что отчаянно нуждаемся в маленькой победе. Мы пишем новый инструмент, потому что мы подавлены. Рефакторим его не потому, что код в беспорядке, а потому, что наша жизнь в беспорядке. Мы гонимся за идеальной системой, потому что она даёт нам что-то, за что можно ухватиться, когда все остальное рушится.

Я писал целые приложения, чтобы не думать о том, почему я был несчастлив. Программирование даёт вам мгновенную обратную связь. Вы запускаете что-то, и оно работает. Или не работает, и вы это исправляете. В любом случае, вы что-то делаете.

Такого рода деятельность вызывает привыкание. Особенно, когда остальная жизнь не даёт удовлетворения. Мы программируем, потому что можем, даже когда не должны. Потому что, по крайней мере, это даёт нам что-то, что можно исправить.

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

Источник:
https://notashelf.dev/posts/curse-of-knowing
👍27
День 2335. #Оффтоп
Проклятие Знания или Исправляем Всё. Окончание

Начало
Продолжение

Выгорание, которого вы не ожидали
Выгорание происходит не только от переутомления. Оно происходит от чрезмерной ответственности. А программирование, если оно достаточно глубоко усвоено, заставляет всё казаться вашей ответственностью. Раздутый веб-сайт. Неэффективный скрипт. Неуклюжий процесс адаптации на вашей работе. Вы можете это исправить. Так почему же вы этого не делаете?

Правда, которую вы прекрасно знаете, заключается в том, что вы не можете исправить всё. Вы знаете это, вы всегда это знали, независимо от вашего уровня мастерства. Но попробуйте сказать это той части вашего мозга, которая видит любую неэффективность как моральный провал.

Ницше предупреждал о том, что не стоит слишком долго смотреть в бездну. Но он не предупреждал, что произойдет, если бездна — это Makefile или проект из 30 тыс. строк кода.

Учимся отпускать
Так где же выход? Это похоже на описание ада Сартром, где ад — это другие люди и то, как они взаимодействуют с вашим ПО? Или это какой-то странный ад, где люди создают ПО, с которым вам приходится взаимодействовать?

Первый шаг — признать, что не всё сломанное можно исправить.
Не каждый инструмент нужно заменять.
Не каждый плохой опыт — это призыв к действию.

Иногда нормально просто использовать вещь. Иногда достаточно знать, почему она сломалась, даже если вы её не почините. Иногда самое дисциплинированное, что вы можете сделать, — это уйти от проблемы, которую знаете, как решить. В этом есть какая-то сила.

Не апатия, нет. И не лень. Просто… некоторая сдержанность.

Новый вид навыка
А что, если настоящий навык — это не техническое мастерство? Или, ещё лучше, если это эмоциональная ясность?

- Знать, какие проблемы стоят вашей энергии.
- Знать, какие проекты стоит поддерживать.
- Знать, когда вы создаёте, чтобы помочь, а когда вы создаёте, чтобы превозмочь.
- Знать, когда остановиться.

Это то, чему я пытаюсь научиться сейчас. После волнения. После одержимости. После выгорания. Я пытаюсь позволить вещам оставаться немного сломанными. Потому что я понял, что не хочу всё чинить. Я просто хочу чувствовать себя хорошо в мире, который часто не идеален. Я могу что-то исправить, но не всё.

Вы учитесь программировать. Вы учитесь чинить вещи. Но самое трудное, чему вы когда-либо научитесь, — это знать, когда оставить их сломанными.

И, возможно, это самый важный навык из всех.

Источник:
https://notashelf.dev/posts/curse-of-knowing
👍19
День 2336. #ЗаметкиНаПолях
Автоматически Перезапускаем Неудавшиеся Задания в GitHub Actions
GitHub Actions не предоставляет встроенного способа автоматического перезапуска. Если у вас возникают какие-то неполадки при выполнении заданий, это может быть проблемой, поскольку приходится перезапускать рабочий процесс вручную.

Однако вы можете попробовать использовать событие workflow_run для запуска нового рабочего процесса, когда предыдущий завершается неудачей. Это позволяет автоматически перезапускать неудавшийся рабочий процесс.

В следующем примере показан сценарий перезапуска неудавшегося рабочего процесса, если это первая попытка перезапуска. Если вы хотите продолжать попытки перезапуска, вы можете удалить условие github.event.workflow_run.run_attempt == 1. Кроме того, сценарий проверяет определённый код выхода (1), чтобы определить, что выполнение рабочего процесса завершилось неудачей. Вы можете настроить это условие в соответствии со своими потребностями.
# .github/workflows/retry.yaml (YAML)
name: retry
on:
workflow_run:
workflows: ["**"]
types:
- completed
branches:
- main

defaults:
run:
shell: pwsh

jobs:
retry:
runs-on: ubuntu-latest
permissions:
actions: write # Retry actions
checks: read # Get info about the run
steps:
- name: retry
if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.run_attempt == 1 }}
run: |
# Get info about the run
$output = gh run view --repo "${{ github.event.repository.full_name }}" "${{ github.event.workflow_run.id }}"
$output

# Rerun the failed workflow if needed
if ($output -match "Process completed with exit code 1") {
gh run rerun "${{ github.event.workflow_run.id }}" --repo "${{ github.event.repository.full_name }}"" --failed
}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Обратите внимание, что вы всё равно получите уведомление о неудачном выполнении рабочего процесса, но вам не придётся перезапускать его вручную.

Источник: https://www.meziantou.net/automatically-rerun-failed-github-actions-workflows.htm
👍6
День 2337. #ЗаметкиНаПолях
RabbitMQ в .NET с Нуля. Начало
RabbitMQ — это брокер сообщений. Он как надёжный почтовый сервис для вашего ПО. Вместо того, чтобы одна система напрямую вызывала другую (что создаёт тесную связь), RabbitMQ выступает в качестве посредника:
- одна часть вашего приложения отправляет сообщение,
- другая часть получает и обрабатывает его.
Это называется асинхронной связью, и отлично подходит для производительности, надёжности и масштабируемости.

Ключевые компоненты:
- Производитель (Producer) – отправляет сообщения.
- Потребитель (Consumer) – получает сообщения.
- Очередь (Queue) – место, где сообщения ждут обработки.
- Обменник (Exchange) – «диспетчер», который направляет сообщения в соответствующие очереди.
- Привязка (Binding) – правила, которые связывают обменники с очередями.

Установка RabbitMQ (локально с Docker)
1. Выполните команду:
docker run -d --hostname rabbitmq
--name rabbitmq \
-p 5672:5672 -p 15672:15672 rabbitmq:3-management

2. Зайдите в панель управления https://localhost:15672. Вход по умолчанию: guest/guest

Реализация в .NET
Создадим простой проект:
- API отправляет «e-mail» в RabbitMQ,
- фоновый сервис прослушивает и «обрабатывает» сообщение.

Добавим RabbitMQ в проект:
Install-Package RabbitMQ.Client


Определим класс сообщения:
public record Email(string To, string Subject, string Body);


Производитель
Его роль – отправлять сообщения в RabbitMQ. Он должен:
- подключиться к RabbitMQ,
- создать очередь (или проверить её существование),
- сериализовать сообщение (например, в JSON),
- отправить сообщение в очередь.

public class EmailPublisher
{
private const string QUEUE =
"email-queue";

public async Task Publish(Email email)
{
var fctry = new ConnectionFactory() {
HostName = "localhost" };
using var conn =
await fctry.CreateConnectionAsync();
using var channel =
await conn.CreateChannelAsync();

// Создаём очередь
await channel.QueueDeclareAsync(
queue: QUEUE,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);

// Создаём сообщение
var msg = JsonSerializer.Serialize(email);
var body = Encoding.UTF8.GetBytes(msg);

// Публикуем
await channel.BasicPublishAsync(
exchange: string.Empty,
routingKey: QUEUE,
mandatory: true,
basicProperties:
new BasicProperties { Persistent = true },
body: body);
}
}


Параметры очереди:
- durable: сохранять на диске, чтобы очередь не терялась при перезапуске брокера,
- exclusive: может ли использоваться другими соединениями,
- autoDelete: удалять ли сообщения при отключении последнего потребителя.

Продолжение следует…

Источник:
https://thecodeman.net/posts/rabbitmq-in-dotnet-from-scratch
👍12
День 2338. #ЗаметкиНаПолях
RabbitMQ в .NET с Нуля. Продолжение

Начало

Получатель
Прослушивает очередь и обрабатывает сообщения по мере их поступления. Он должен:
- подключиться к RabbitMQ,
- подписаться на очередь,
- ожидать сообщений,
- десериализовывать и обрабатывать сообщения.

Рекомендуемый и наиболее удобный способ получения сообщений — настроить подписку с помощью интерфейса IAsyncBasicConsumer. Затем сообщения будут доставляться автоматически по мере их поступления. Один из способов реализации потребителя — использовать класс AsyncEventingBasicConsumer, в котором доставки и другие события жизненного цикла потребителя реализованы как события C#:
public class EmailConsumer : BackgroundService
{
private const string QUEUE =
"email-queue";

protected override async Task
ExecuteAsync(CancellationToken ct)
{
var fctry = new ConnectionFactory() {
HostName = "localhost" };
using var conn =
await fctry.CreateConnectionAsync(ct);
using var channel =
await conn.CreateChannelAsync(cancellationToken: ct);

await channel.QueueDeclareAsync(
queue: QUEUE,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null,
cancellationToken: ct);

var consumer =
new AsyncEventingBasicConsumer(channel);
consumer.ReceivedAsync +=
async (sender, eventArgs) =>
{
var body = eventArgs.Body.ToArray();
var json = Encoding.UTF8.GetString(body);
var email =
JsonSerializer.Deserialize<Email>(json);

Console.WriteLine(
$"Email: {email?.To}, Тема: {email?.Subject}");

// Отправляем email…
Task.Delay(1000).Wait();

await ((AsyncEventingBasicConsumer)sender)
.Channel
.BasicAckAsync(
eventArgs.DeliveryTag,
multiple: false);
};

await channel.BasicConsumeAsync(
queue: QUEUE,
autoAck: true,
consumer: consumer,
cancellationToken: ct);
}
}


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

Источник:
https://thecodeman.net/posts/rabbitmq-in-dotnet-from-scratch
👍3
Возникла дискуссия в обсуждении перевода книги. Когда мы говорим про коллекции ConcurrentXXX/ImmutableXXX/FrozenXXX, какой перевод выражения "concurrent (collections)", на ваш взгляд, удачнее?
Anonymous Poll
1%
асинхронные
0%
защищённые
29%
конкурентные
6%
многопоточные
6%
неблокирующие
4%
параллельные
51%
потокобезопасные
3%
другой (напишите в комментариях)
День 2339. #ЗаметкиНаПолях
RabbitMQ в .NET с Нуля. Окончание
Начало
Продолжение

Типы обмена в RabbitMQ
Обменник (Exchange) в RabbitMQ решает, куда отправить сообщение. Каждый тип обмена имеет свою стратегию маршрутизации сообщений.

1. Direct Exchange (маршрутизация один-к-одному)
Сообщение направляется в очереди с точно таким же ключом маршрутизации. Это как отправка почты определённому получателю. Например, отправка email только в очередь, ответственную за маркетинговую рассылку:
channel.ExchangeDeclare(
"direct-exchange", ExchangeType.Direct);
channel.QueueBind(
"market-queue", "direct-exchange", "market");

Если вы отправите сообщение с routingKey: "market" (см. вызов BasicPublishAsync в примере кода производителя), оно попадёт в очередь "market-queue".

2. Fanout Exchange (отправка всем)
Сообщения отправляются во все очереди, связанные с этим обменником, игнорируя routingKey. Например, всеобщая рассылка по всем сервисам (email, СМС):
channel.ExchangeDeclare(
"fanout-exchange", ExchangeType.Fanout);
channel.QueueBind(
"email-queue", "fanout-exchange", "");
channel.QueueBind(
"sms-queue", "fanout-exchange", "");

Любое сообщение, отправленное в "fanout-exchange", попадёт в обе очереди.

3. Topic Exchange (маршрутизация по шаблону)
Использует шаблон routingKey для гибкой настройки:
- * - ровно одно слово,
- # - 0 и более слов.
Например, маршрутизация логов на основе важности и типа системы:
channel.ExchangeDeclare(
"topic-exchange", ExchangeType.Topic);
channel.QueueBind(
"error-queue", "topic-exchange", "log.error.#");
channel.QueueBind(
"auth-queue", "topic-exchange", "log.*.auth");

"log.error.auth" – попадёт в обе очереди.
"log.info.auth" – попадёт в auth-queue.
"log.error.database" – попадёт в error-queue.

4. Headers Exchange (маршрутизация по метаданным)
Вместо routingKey использует заголовки сообщения. Например, маршрутизация сообщений по сложным условиям (x-type="invoice" и region="EU"):
channel.ExchangeDeclare(
"headers-exchange", ExchangeType.Headers);
var args = new Dictionary<string, object>
{
{"x-match", "all"}, // либо "any"
{"x-type", "invoice"},
{"region", "EU"}
};
channel.QueueBind(
"invoice-eu-queue",
"headers-exchange",
string.Empty,
args);

Только сообщения, которые включают и x-type="invoice", и region="EU" в своих заголовках, попадут в очередь "invoice-eu-queue".

Итого
RabbitMQ — мощный инструмент для создания масштабируемых, слабо связанных систем, а .NET делает его удивительно простым в интеграции. Независимо от того, создаете ли вы микросервисы или просто хотите разгрузить основное приложение от выполнения некоторых длительных задач, RabbitMQ вам поможет.

Источник: https://thecodeman.net/posts/rabbitmq-in-dotnet-from-scratch
👍13
День 2340. #TipsAndTricks
Широко известный в узких кругах дотнетчиков блогер Ник Чапсас помимо основных видео на своём ютуб-канале также выпускает шортсы с короткими советами по написанию более лучшего кода. Так вот, таких советов накопилось уже более сотни, поэтому он собрал первую сотню в одно почти часовое видео.

Смотрим и мотаем на ус 😉

https://youtu.be/8F-Pb-SKO5g
👍22