.NET Разработчик
6.53K subscribers
442 photos
3 videos
14 files
2.12K links
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 2383. #ЗаметкиНаПолях
Алгоритмы Выбора Главного Узла в Распределённых БД. Окончание

1. Алгоритм Забияки
2. Кольцевой Алгоритм
3. Алгоритм Paxos
4. Алгоритм Raft

5. Zookeeper и Zab
ZooKeeper не является БД. Он не хранит записи пользователей, индексы запросов и не реплицирует записи журнала. Вместо этого он играет важнейшую роль в распределённой экосистеме: координирует работу. Когда системам требуется выбрать брокера, управлять отказоустойчивостью лидера или синхронизировать конфигурацию, ZooKeeper — незаменимый сервис.

Внутренне ZooKeeper использует протокол Zab (ZooKeeper Atomic Broadcast). Zab обрабатывает как выбор лидера, так и репликацию состояния, гарантируя безопасность, согласованность и отказоустойчивость даже метаданных координации (см. диаграмму ниже).

Zab обеспечивает надёжность выборов лидера в ZooKeeper. Прежде чем новый лидер сможет начать обработку запросов на запись, он должен синхронизироваться с кворумом ведомых, чтобы убедиться, что у него самое последнее зафиксированное состояние.

Каждая транзакция в ZooKeeper помечается zxid (идентификатором транзакции ZooKeeper), который объединяет эпоху лидера и индекс журнала. Это позволяет узлам определять, кто имеет наиболее актуальную картину мира.

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

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

- Алгоритмы Забияки и Кольцевой используют детерминированные правила, основанные на идентификаторах узлов. Paxos, Raft и Zab используют принятие решений на основе кворума.
- Для обеспечения отказоустойчивости алгоритм Забияки предполагает надёжные соединения и точное обнаружение сбоев. Узел, ложно помеченный как мёртвый, может привести к появлению нескольких лидеров или частым перевыборам. Кольцевой алгоритм останавливается, если любой узел в кольце выходит из строя без предупреждения. В отличие от этого, Paxos, Raft и Zab допускают частичные сбои и продолжают принимать безопасные решения.
- С точки зрения производительности, Raft и ZooKeeper/Zab предсказуемо справляются с перевыборами. Алгоритмы Забияки и Кольцевой неэффективны в средах с высокой текучестью. Paxos, особенно в базовой версии, может испытывать трудности при наличии нескольких предлагающих узлов и нечётком лидерстве, если не использовать такие усовершенствования, как Multi-Paxos.
- Алгоритмы Забияки и Кольцевой просты в реализации. Raft требует больше усилий. Paxos довольно сложно настроить правильно. ZooKeeper/Zab скрывает сложность за API, но лежащие в его основе механизмы нетривиальны.

Источник:
https://blog.bytebytego.com/p/top-leader-election-algorithms-in
👍5
День 2384. #TipsAndTricks
Как Продолжить Выполнение Процесса После Завершения Задания GitHub Action

После завершения задания GitHub Actions обработчик завершает все запущенные им дочерние процессы. Он идентифицирует эти процессы, проверяя переменную окружения RUNNER_TRACKING_ID. Любой процесс, где эта переменная установлена переменной считается дочерним процессом обработчика и будет остановлен.

Чтобы процесс продолжил работу после завершения задания, запустите его без переменной окружения RUNNER_TRACKING_ID:
var psi = new ProcessStartInfo("sample_app");
psi.EnvironmentVariables.Remove("RUNNER_TRACKING_ID");
Process.Start(psi);


Либо:
# Вариант 1
$env:RUNNER_TRACKING_ID = $null

# Вариант 2
$psi = New-Object System.Diagnostics.ProcessStartInfo "sample_app"
$psi.EnvironmentVariables.Remove("RUNNER_TRACKING_ID")
[System.Diagnostics.Process]::Start($psi)


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

Источник: https://www.meziantou.net/how-to-keep-processes-running-after-a-github-action-job-ends.htm
👍2
День 2385. #TipsAndTricks
5 Современных Возможностей C#, Которые Улучшат Ваш Код

C# стал гораздо более выразительным и мощным языком, чем был ещё несколько лет назад. Вот 5 новинок языка, которые отличают инженеров, просто «знающих C#», от тех, кто использует его на полную.

1. Структуры только для чтения для критически важных для производительности типов-значений
Обычная структура копируется без необходимости, что приводит к скрытым потерям производительности. Отметив её как только для чтения, вы сообщаете компилятору, что её поля никогда не изменятся, что позволяет среде выполнения не создавать защитные копии.
Раньше:
public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}

Сейчас:
public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}

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

2. Ключевое слово scoped для предотвращения использования ref-переменных вне области видимости
C# позволяет использовать ref-переменные для повышения производительности, но распространённой ошибкой является случайное их использование вне области видимости (возвращение ссылки на что-то, что больше не является безопасным). scoped гарантирует во время компиляции, что ссылка не проживёт дольше положенного.
Без scoped:
ref int Dangerous(ref int number)
{
// Может быть использовано извне и привести к проблемам
return ref number;
}

scoped:
ref int Safe(scoped ref int number)
{
// Компилятор проверяет, что number может
// использоваться только внутри метода
// и выдаёт ошибку компиляции
return ref number;
}


3. Обязательные свойства для принудительной инициализации объекта
Сколько раз вы создавали объект и забывали установить одно из его критически важных свойств? При использовании обязательных свойств компилятор принуждает инициализировать объект.
Раньше:
public class User
{
public string Name { get; set; }
public string Email { get; set; }
}
// Можно забыть инициализировать свойства
var user = new User { Name = "John" };

Сейчас:
public class User
{
public required string Name { get; init; }
public required string Email { get; init; }
}
// Компилятор проверяет полноту инициализации
var user = new User {
Name = "John", Email = "john@mail.com" };


4. Возврат типа ref readonly, чтобы избежать защитного копирования
При возврате больших структур возврат по значению часто приводит к ненужному копированию. Возврат типа ref readonly даёт вызывающим функциям ссылку на объект, который они могут читать, но не могут изменять.
Без ref readonly:
public BigStruct GetData() => 
_bigStruct; // Копирует структуру

С ref readonly:
public ref readonly BigStruct GetData() => 
ref _bigStruct; // Нет копирования

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

5. Статические абстрактные члены в интерфейсах для обобщённой математики
Это кажется узкоспециализированной функцией, но открывает доступ к настоящей обобщённой математике без рефлексии и упаковки. До появления этой функции нельзя было писать обобщённые алгоритмы, работающие с числовыми типами:
T Add<T>(T a, T b) where T : INumber<T> 
=> a + b;

Благодаря статическим абстрактным членам, INumber<T> определяет операторы и константы обобщённо, обеспечивая чистые, независимые от типов математические библиотеки.

Итого
Современный C# — это не просто новый синтаксис; это достижение ясности, безопасности и производительности, которых просто не могли предложить старые версии. Начните внедрять эти функции в свой код, и вы не только начнёте писать лучшее ПО, но и по-новому взглянете на возможности языка.

Источник: https://blog.stackademic.com/5-modern-c-features-that-will-make-your-code-feel-like-magic-d1ef6a374d13
👍30
Что будет выведено в результате выполнения кода на картинке в первом комментарии?
#CSharp #Quiz
Anonymous Quiz
3%
4, 4
3%
4, 5
48%
5, 4
19%
5, 5
5%
5, 0
5%
0, 0
13%
Ошибка компиляции
4%
Ошибка времени выполнения
👎22👍19
День 2386. #ЗаметкиНаПолях
Неожиданная Несогласованность в Записях
На днях Джон Скит пытался найти ошибку в своём коде, и она оказалась следствием его непонимания принципов работы записей в C#. Как показывает вчерашний опрос, он не единственный, кто ожидал, что они будут работать именно так.

Когда записи появились в C#, одновременно появился и оператор «обратимого изменения» with. Идея заключается в том, что типы записей неизменяемы, но вы можете легко и эффективно создать новый экземпляр с теми же данными, что и существующий экземпляр, но с другими значениями некоторых свойств:
public record HighScoreEntry(
string PlayerName, int Score, int Level);

HighScoreEntry entry = new("Jon", 5000, 50);
var updatedEntry =
entry with { Score = 6000, Level = 55 };

Это не изменяет данные первоначальной записи, т.е. entry.Score останется 5000.

Проблема
В качестве очень простого (и весьма надуманного) примера можно создать запись, которая определяет, является ли значение чётным или нечётным при инициализации:
public record Number(int Value)
{
public bool Even { get; } =
(Value & 1) == 0;
}

На первый взгляд, всё нормально:
var n2 = new Number(2);
var n3 = new Number(3);
Console.WriteLine(n2);
// Number { Value = 2, Even = True }
Console.WriteLine(n3);
// Number { Value = 3, Even = False }

Но если мы изменим код на использование with:
var n3 = n2 with { Value = 3 };
Console.WriteLine(n3);
// Number { Value = 3, Even = True }

Джон (и не только он) всегда предполагал, что оператор with вызывает конструктор с новыми значениями. На самом деле это не так. Оператор with выше преобразуется в примерно такой код:
var n3 = n2.<Clone>$();
n3.Value = 3;

Метод <Clone>$ (по крайней мере, в этом случае) вызывает сгенерированный конструктор копирования (Number(Number)), который копирует как Value, так и резервное поле для Even (т.е. значение Even). Всё это документировано, но пока компилятор не выдаёт никаких предупреждений о возможных несоответствиях, которые это может вызвать.

Конечно, вычисляемое свойство работает правильно. Значение вычисляется каждый раз при обращении к нему:
public record Number(int Value)
{
public bool Even => (Value & 1) == 0;
}


Что делать?
Пока просто знать об этом и жить дальше. Легко сказать, что можно просто использовать вычисляемые свойства. Проблема, если кто-то, не знающий об этом, добавит/изменит свойство на вычисляемое при инициализации. Либо если запись предоставлена в какой-то внешней библиотеке (не заглядывая в исходный код библиотеки, нет возможности определить какое свойство там использовано). Сложно представить, что это поведение будет изменено в языке. Но Джон попросил Майкрософт хотя бы добавить предупреждение в таких случаях, а также написал собственный анализатор.

Источник:
https://codeblog.jonskeet.uk/2025/07/19/unexpected-inconsistency-in-records/
👍31👎2
День 2387. #Оффтоп
Давно не рекомендовал вам видео. А тут вчера у Дудя вышло прекрасное интервью с Андреем Дороничевым. Это своего рода сиквел популярного фильма Дудя про Кремниевую долину. Андрей с тех пор ушёл из гугла и создал стартап, где использует ИИ для решения разных больших задач (в данный момент – лекарство от рака). В интервью не только про него самого, но и 100500 "тупых вопросов" про ИИ (что он уже может и сможет в будущем, под угрозой ли наши профессии, будет ли восстание машин и т.п.), а также про личностный рост и про IT сферу вообще.

Мне очень понравилось, поэтому делюсь. Выделите 2 часа 40 минут в своём графике. Это интересно.

https://youtu.be/1SLvIof4-Zw
👎30👍17
День 2388. #ЗаметкиНаПолях
Разграничиваем Данные в Модульном Монолите. Начало
Модульные монолиты обещают производительность монолита и чёткие границы микросервисов. Каждый модуль самодостаточен: его модель предметной области, поведение и данные находятся внутри границ. Но одно из самых сложных мест для поддержания этих границ — база данных. Ничто не мешает разработчику создать JOIN-оператор между таблицами разных модулей или обойти общедоступный API.

Ранее мы рассматривали 4 уровня изоляции данных и как модули должны предоставлять явные API для доступа к своим данным. Теперь подробно рассмотрим границы в БД.

В модульном монолите каждый модуль владеет своими данными. Если модуль A обращается к таблицам модуля B, это ограничение теряется, и модули становятся тесно связанными. Вместо этого модуль B должен предоставлять общедоступный API и скрывать логику хранения своих данных от клиентов. Помимо чистого кода, соблюдение границ на уровне БД защищает вас от ошибок и упрощает последующее извлечение модуля в отдельный сервис.

Схемы БД действуют как папки: они позволяют организовывать объекты и предоставлять доступ к БД множеству пользователей, но пользователь может получить доступ к любой схеме только при наличии соответствующих привилегий. Это означает, что мы можем намеренно привязать каждый модуль к своей схеме.

Стратегия
- Создайте схему для каждого модуля и выделенную роль БД.
- Предоставьте этой роли привилегии только для её схемы и задайте для неё путь поиска по умолчанию (для PostgreSQL).
- Используйте EF с отдельными DbContext для каждого модуля, установив схему и строку подключения по умолчанию для каждого модуля.
- Для сквозных запросов создайте представление в БД, доступное только для чтения и действующее как общедоступный API.
- Необязательно: используйте политики безопасности на уровне строк для ограничения доступа внутри таблицы.
Эти методы позволят установить чёткие границы, одновременно снижая эксплуатационные издержки.

Схемы, роли и пути поиска
PostgreSQL (и многие другие БД) позволяет создавать несколько схем в одной БД. Модуль может определить свою собственную схему и роль, которой она принадлежит. Роль имеет только привилегии пользователя и уровня таблицы в этой схеме. Например, для модуля заказов:
CREATE ROLE orders_role LOGIN PASSWORD 'orders_secret';
CREATE SCHEMA orders AUTHORIZATION orders_role;
GRANT USAGE ON SCHEMA orders TO orders_role;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA orders TO orders_role;
ALTER ROLE orders_role SET search_path = orders;

Команда ALTER ROLE устанавливает путь поиска по умолчанию для роли, чтобы в запросах имена объектов, не имеющие схемы, разрешались в схему модуля. Если вы не хотите полагаться на путь поиска, используйте имена со схемой (orders.table_name).

Безопасность на уровне строк (Row-level security, RLS) позволяет фильтровать строки на основе выражения политики. RLS эффективен для многопользовательских сценариев или конфиденциальных данных, но он увеличивает сложность. Начните со схем и ролей и добавляйте RLS только при необходимости.

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

Источник:
https://www.milanjovanovic.tech/blog/how-to-keep-your-data-boundaries-intact-in-a-modular-monolith
👍14
День 2389. #ЗаметкиНаПолях
Разграничиваем Данные в Модульном Монолите. Окончание

Начало

Настройка схем EF и нескольких DbContext
- Используйте отдельные DbContext на каждый модуль. Установка схемы по умолчанию также влияет на последовательности и миграции.
- Укажите строку подключения для каждого модуля, используя роль модуля. Даже если модули используют одну БД, различные роли гарантируют, что неправильно настроенный контекст не сможет получить доступ к другой схеме.
- Настройте таблицу истории миграций в каждом контексте.

Пошаговая инструкция
Допустим, у нас два модуля (Orders и Shipping), и мы хотим установить границы между ними.

1. Создадим схемы и роли, предоставив каждой роли привилегии только на её схему:
-- Схема и роль Orders 
CREATE ROLE orders_role LOGIN PASSWORD 'orders_secret';
CREATE SCHEMA orders AUTHORIZATION orders_role;
GRANT USAGE ON SCHEMA orders TO orders_role;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA orders TO orders_role;
ALTER ROLE orders_role SET search_path = orders;

Аналогично для роли Shipping.

2. Добавим строки подключения:
{
"ConnectionStrings": {
"Orders": "Host=…;Database=…;Username=orders_role;Password=orders_secret",
"Shipping": "…"
}
}


3. Определим контексты для каждого модуля. Для Orders:
public class OrdersDbContext : DbContext
{
public DbSet<Order>
Orders { get; set; } = default!;


protected override void
OnModelCreating(ModelBuilder mb)
{
// схема по умолчанию
mb.HasDefaultSchema("orders");
// либо явно таблиц
mb.Entity<Order>().ToTable("orders");

base.OnModelCreating(mb);
}
}


4. Регистрируем каждый контекст со своей строкой подключения и указываем таблицу миграций:
builder.Services
.AddDbContext<OrdersDbContext>(opts =>
opts.UseNpgsql(
builder.Configuration.GetConnectionString("Orders"),
o => o.MigrationsHistoryTable("__EFMigrationsHistory", "orders")));


5. Управляем миграциями отдельно. При создании миграции указываем контекст:
dotnet ef migrations add InitialOrders --context OrdersDbContext --output-dir Data/Migrations/Orders

EF Core сгенерирует классы миграций, которые создадут таблицы в указанной схеме (благодаря HasDefaultSchema). Не забудьте применить миграции в правильном порядке при развёртывании. Вы можете автоматизировать этот процесс, используя средство выполнения миграций, итеративно перебирающее контексты.

Сквозные запросы
Даже в модульной системе иногда требуется экран, охватывающий несколько модулей, например, страница истории заказов, отображающая данные о заказе и доставке. Не поддавайтесь искушению использовать JOIN между схемами. Полезны два подхода:
1. Выделенная модель чтения. Один модуль владеет моделью представления и подписывается на события других. Этот шаблон хорошо работает, когда модули могут быть позже извлечены в микросервисы.
2. Представления в БД. Поскольку модули используют общую БД, мы можем создать в общедоступной схеме представление, доступное только для чтения, которое объединяет соответствующие таблицы. Мы предоставляем право SELECT для представления специальной роли или модулю. Это представление действует как управляемое общедоступное API. Потребители запрашивают представление, но не могут напрямую получить доступ к базовым таблицам. Однако, если впоследствии вы разделите БД, представление придётся заменить вызовом сервиса.
CREATE VIEW public.order_summary AS
SELECT o.id, o.total, s.status
FROM orders.orders o
JOIN shipping.shipments s ON s.order_id = o.id;

-- даём права чтения роли отчётов
GRANT SELECT ON public.order_summary TO reporting_role;

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

Источник: https://www.milanjovanovic.tech/blog/how-to-keep-your-data-boundaries-intact-in-a-modular-monolith
👍8
День 2390. #TipsAndTricks
Как не Возвращать Все Свойства в SqlRaw
В Entity Framework SqlRaw есть небольшое, иногда раздражающее ограничение: SQL-запрос должен возвращать данные для всех свойств типа сущности.
Иногда этого не нужно, поэтому давайте посмотрим, как это очень просто обойти.

Представьте, что у нас есть такой объект:
public sealed record MyEntity
{
public double? PropA { get; init; }
public double? PropB { get; init; }
}

По сути, мы указываем, что ни PropA, ни PropB не являются обязательными и, следовательно, если они отсутствуют в результирующем наборе, должны быть равны NULL. Но SqlRaw ожидает, что все свойства сущности присутствуют в предложении SELECT.

То есть:
dbContext
.Database
.SqlQuery<MyEntity>(
"SELECT PropA FROM MyTable")
.ToListAsync(token);

Завершится с ошибкой, что EF не может сопоставить запрос с типом, поскольку отсутствует PropB.

Чтобы обойти эту проблему, просто явно возвращайте PropB как NULL:
dbContext
.Database
.SqlQuery<MyEntity>(
"SELECT PropA, NULL AS PropB FROM MyTable")
.ToListAsync(token);


Вот и всё, EF снова счастлив. Конечно, это упрощённый пример, но суть, надеюсь, вы поняли.

Источник: https://steven-giesel.com/blogPost/c6bea409-9e49-4915-8529-8a8a8574ba80/how-to-not-return-all-properties-in-sqlraw
👍22
День 2391. #Архитектура
Вам Возможно не Нужен Redis

Redis, пожалуй, самая востребованная технология из всех, один из лидеров того, что вам «нужно» иметь в своём арсенале. Это очень хорошо спроектированная и впечатляющая технология. И при всём этом в ваших проектах он скорее всего не нужен!

Очень часто Redis используется просто потому, что считается отличным решением, и всё. Но при ближайшем рассмотрении реального варианта использования выясняется, что Redis ничего не улучшил и не решил основную проблему, а просто усложнил систему. Виктор Бломквист в своём блоге рассказал о трёх компаниях, в которых он работал. Везде был разный стек и разная нагрузка, но объединяло их необъяснимое стремление обязательно использовать Redis.

1. Tantan
Tantan - второе по величине приложение для знакомств в Китае. В то время, когда туда был добавлен Redis, система имела 50–100 мощных серверов БД с PostgreSQL. Каждый хранил подмножество пользовательских «свайпов», сегментированных по UserId, так что данные для конкретного пользователя хранились только на одном сервере.

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

Первая мысль - разместить эти данные в Redis. Один мощный сервер Redis должен справиться с нагрузкой, поэтому понадобится всего пара (для резервирования). Однако в процессе внедрения возник вопрос: «Почему бы просто не хранить эти данные на шардах PostgreSQL рядом со свайпами?». Сами данные были бы микроскопическими по сравнению с тем, что уже делали эти серверы. После обсуждения в команде все согласились. Добавление Redis лишь усложнило бы относительно простой стек. После развёртывания дополнительная нагрузка на серверах баз данных была едва заметна.

2. Bannerflow
Bannerflow - компания, занимающаяся рекламой в интернет. Команда разрабатывала новый набор микросервисов для настройки и публикации рекламы в социальных сетях, таких как Facebook. Команда решила добавить экземпляр Redis для кэширования. Обратите внимание, что это было для системы с нагрузкой, составляющей даже меньше 0,1% от Tantan.

После ухода основного разработчика никто в команде уже не мог толком объяснить, зачем был нужен Redis. Глядя на код, количество вызовов или любую другую метрику, причины его добавления были не очевидны. И команда пришла к мнению, что при наличии свободного времени лучше всего будет удалить его.

3. MAJORITY
Финтех компания. Первым применением было кэширование результата внешнего вызова API для поиска геолокации пользователей, чтобы сервис мог обрабатывать запросы местоположения быстрее и дешевле. Кэширование этих данных вполне разумно. По чистой случайности, этот конкретный сервис выполнял два действия для поиска: вызов БД и вызов Redis. Это значительно упростило сравнение и оценку.

У сервиса была собственная БД, которая делила кластер с другими сервисами. Нагрузка на эту БД была настолько низкой, что она даже не отображалась при анализе нагрузки кластера в Azure. Перенос использования Redis в БД привёл бы к увеличению нагрузки примерно в два раза, что значительно в относительных числах, но в абсолютных это ничто, т.к. исходная была практически нулевой!

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

Но при более внимательном рассмотрении стало легко увидеть, что для этих блокировок можно было бы использовать механизм блокировок внутри основной БД (Azure SQL). Это бы увеличило нагрузку на БД, но, как и в предыдущем примере, это не было высокопроизводительным вариантом использования.

Источник: https://www.viblo.se/posts/no-need-redis/
👍10
День 2392. #ЗаметкиНаПолях
PDF из HTML в .NET (бесплатно). Начало

Создание качественных PDF-отчётов в .NET не обязательно должно быть мучением. Тем, кто хоть немного работает с HTML, проще будет использовать преобразование из HTML в PDF.

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

Это даёт вам полный контроль над макетом, стилем и содержимым:
- Богатая стилизация с помощью CSS,
- Легкий просмотр/отладка в браузере,
- Поддержка диаграмм/изображений через JS/CSS,
- Полный контроль над макетом (медиазапросы, разрывы страниц и т. д.).

Минусы:
- Требуется установка браузера (например, Chromium),
- Медленнее, чем нативные библиотеки PDF,
- Немного сложнее в настройке.

1. Настройка
Создадим проект минимальных API и установим NuGet-пакеты:
Install-Package Handlebars.Net
Install-Package PuppeteerSharp


2. Создание шаблона
Это простой HTML-документ с несколькими заместителями для Handlebars. Вы заметите их по синтаксису {{variable}}. Они будут заменены реальными данными при рендеринге шаблона.
<!-- Templates/InvoiceTemplate.html -->
<html lang="en">
<head>
<style>
body {
font-family: Arial;
}
</style>
</head>
<body>
<h1>Invoice #{{Number}}</h1>

<p>Date: {{formatDate IssuedDate}}</p>

<h2>From:</h2>
<p>{{SellerAddress.CompanyName}}</p>
<p>{{SellerAddress.Email}}</p>

<h2>To:</h2>
<p>{{CustomerAddress.CompanyName}}</p>
<p>{{CustomerAddress.Email}}</p>

<h2>Items:</h2>
<table>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
{{#each LineItems}}
<tr>
<td>{{Name}}</td>
<td>{{formatCurrency Price}}</td>
</tr>
{{/each}}
</table>

<p><strong>Total: {{formatCurrency Total}}</strong></p>
</body>
</html>


Вызовы функций, вроде {{formatDate IssuedDate}} – это пользовательские вспомогательные функции, которые можно определить в Handlebars. Они регистрируются так:
Handlebars.RegisterHelper("formatDate", (context, arguments) =>
{
if (arguments[0] is DateOnly date)
return date.ToString("dd/MM/yyyy");
return arguments[0]?.ToString() ?? "";
});


Это позволяет, например, форматировать даты, валюты и т.п. при необходимости. Вспомогательные функции регистрируются перед компиляцией шаблона (его мы рассмотрим в продолжении), достаточно одного раза при запуске приложения.
Кроме того, в Handlebars есть встроенные управляющие конструкции, например:
- {{#if …}} ... {{/if}} для условий,
- {{#each …}} ... {{/each}} для перебора коллекций.

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

Источник:
https://www.milanjovanovic.tech/blog/pdf-reporting-in-dotnet-with-html-templates-and-puppeteersharp
👍9
День 2393. #ЗаметкиНаПолях
PDF из HTML в .NET (бесплатно). Продолжение

Начало. Создание шаблона

3. Рендеринг шаблона
Мы используем Handlebars для компиляции шаблона с данными, а затем PuppeteerSharp для рендеринга в PDF.

Сначала прочитаем файл шаблона, скомпилируем его с помощью Handlebars и заполним данными:
var tmplt = File.ReadAllText(
"Templates/InvoiceTemplate.html");

var data = new {
IssuedDate = new DateOnly(2025, 08, 19),
Number = 12345,

LineItems = new[] {
new { Name = "Software License", Price = 99d },
new { Name = "Support Plan", Price = 49d }
}
};

// ... регистрируем вспомогательные функции Handlebars до вызова компиляции ...

var compiledTmplt = Handlebars.Compile(tmplt);

var html = compiledTmplt(data);

Это даст нам финальный HTML-код, в котором все заместители заменены реальными данными.

Примечание: не обязательно использовать Handlebars. Можно использовать любой шаблонизатор, например, Razor или Scriban.

4. Преобразование в PDF
Мы запустим браузер без UI, установим получившийся HTML код как его контент и сгенерируем PDF-файл:
// Скачиваем бинарные файлы браузера 
var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();

// Запускаем браузер и создаём страницу
using var browser = await Puppeteer.LaunchAsync(
new LaunchOptions { Headless = true });
using var page = await browser.NewPageAsync();

// Устанавливаем контент страницы
await page.SetContentAsync(html);

// Не обязательно: ждём загрузки шрифтов
await page.EvaluateExpressionHandleAsync(
"document.fonts.ready");

// Настраиваем PDF
byte[] pdf = await page.PdfDataAsync(
new PdfOptions {
Format = PaperFormat.A4,
PrintBackground = true,
MarginOptions = new MarginOptions
{
Top = "50px",
Right = "20px",
Bottom = "50px",
Left = "20px"
}
});

В начале нам нужно скачать движок Chromium для рендеринга HTML страницы. PuppeteerSharp требует загрузки исполняемых файлов браузера Chromium во время выполнения. Это можно сделать, вызвав BrowserFetcher.DownloadAsync(). Это займёт какое-то время в первый раз, далее PuppeteerSharp будет использовать скачанные бинарные файлы.

В итоге в переменной pdf мы получим массив байтов, содержащий данные PDF-файла. Вы можете сохранить его в файл или вернуть из конечной точки API. Например, так:
return Results.File(pdf, "application/pdf", "invoice.pdf");


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

Источник:
https://www.milanjovanovic.tech/blog/pdf-reporting-in-dotnet-with-html-templates-and-puppeteersharp
👍6
День 2394. #ЗаметкиНаПолях
PDF из HTML в .NET (бесплатно). Окончание

Создание шаблона
Рендеринг в PDF

5. Улучшения: изображения, верхний/нижний колонтитул, стили
Можно добавлять изображения в шаблон с помощью тега <img>. Простой подход — использовать изображение в кодировке base64 непосредственно в HTML-коде. Обратите внимание, что мы передаём данные изображения через переменную LogoBase64:
<img
src="data:image/png;base64,{{LogoBase64}}"
alt="Logo"
style="height:50px; max-width:200px; object-fit:contain;"
/>

Мы также можем отображать динамические верхние и нижние колонтитулы, используя встроенную поддержку PuppeteerSharp. Можно определить их в объекте PdfOptions при создании PDF-файла:
var pdfOptions = new PdfOptions
{
HeaderTemplate =
@"""
<div style='font-size: 14px; text-align: center; padding: 10px;'>
<span style='margin-right: 20px;'><span class='title'></span></span>
<span><span class='date'></span></span>
</div>
""",
FooterTemplate =
@"""
<div style='font-size: 14px; text-align: center; padding: 10px;'>
<span style='margin-right: 20px;'>Документ создан <span class='date'></span></span>
<span>Страница <span class='pageNumber'></span> из <span class='totalPages'></span></span>
</div>
""",
DisplayHeaderFooter = true
};

PuppeteerSharp использует CSS-классы, такие как title, date, pageNumber и totalPages для внедрения динамических значений. В некоторых других библиотеках это может отличаться, поэтому сверьтесь с документацией.

Наконец, вы можете использовать CSS для расширенной стилизации. CSS может быть встроен в HTML-код или оформлен в отдельный CSS-файл. При необходимости вы также можете ссылаться на внешние таблицы стилей, используя тег <link>.

6. Вопросы производительности
Первое, что следует рассмотреть, — это использование headless-браузера, например, Chromium. Он может работать медленнее, чем нативные PDF-библиотеки, особенно для больших документов или высокого уровня параллелизма. Кроме того, это увеличивает нагрузку на процессор, поскольку необходимо загружать и запускать исполняемые файлы браузера. Однозначно стоит рассмотреть возможность вынесения этого процесса из основного приложения. Вы можете использовать фоновый сервис или отдельный микросервис для генерации PDF-файлов. Облачная функция может быть хорошим решением при масштабировании. Но всё зависит от вашего конкретного сценария использования.

Итого
HTML + PuppeteerSharp — один из самых практичных подходов к созданию PDF-отчётов в .NET. Он позволяет:
- Создавать безупречные макеты с использованием знакомых веб-технологий,
- Вставлять динамические данные с помощью Handlebars,
- Выводить высококачественные PDF-файлы с полной стилизацией, таблицами и изображениями.
И всё это без использования коммерческих библиотек. По сравнению с платными библиотеками, вроде QuestPdf или IronPdf, вы пожертвуете производительностью и придётся выполнить некоторую первоначальную настройку. Но для большинства внутренних инструментов, отчётов или генерации счетов для клиентов это того стоит.

Источник: https://www.milanjovanovic.tech/blog/pdf-reporting-in-dotnet-with-html-templates-and-puppeteersharp
👍7
День 2395. #SystemDesign101
Как Работает SSO?

Технология единого входа (Single Sign-On SSO) — технология, позволяющая пользователю аутентифицироваться один раз, чтобы получить доступ ко множеству связанных сервисов или приложений, без необходимости повторно вводить свои учётные данные для каждого из них.

Рассмотрим типичный процесс входа в SSO.
Шаг 1: Пользователь запрашивает защищённый ресурс в приложении, например, Gmail, которое является провайдером сервиса (Service Provider, SP).

Шаг 2: Сервер Gmail обнаруживает, что пользователь не вошёл в систему, и перенаправляет браузер к провайдеру идентификации (Identity Provider, IdP) компании с запросом на аутентификацию.

Шаг 3: Браузер перенаправляет пользователя к IdP.

Шаг 4: IdP отображает страницу входа, где пользователь вводит свои учётные данные.

Шаг 5: IdP создаёт защищённый токен и возвращает его браузеру. IdP также создаёт сессию для будущего доступа. Браузер перенаправляет токен в Gmail.

Шаг 6: Gmail проверяет токен, чтобы убедиться, что он получен от провайдера идентификации.

Шаг 7: Gmail возвращает защищённые ресурсы в браузер в зависимости от того, к чему пользователю разрешён доступ.

На этом процесс входа с помощью SSO завершается. Теперь посмотрим, что произойдёт, когда пользователь перейдёт в другое приложение с интегрированной системой единого входа, например, в Slack.

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

Шаг 10: Браузер перенаправляет пользователя к провайдеру идентификации.

Шаги 11-13: Поскольку пользователь уже вошёл в систему провайдера идентификации, процесс входа пропускается и сразу создаётся новый токен для Slack. Новый токен отправляется в браузер, который перенаправляет его в Slack.

Шаг 14–15: Slack проверяет токен и предоставляет пользователю соответствующий доступ.

Источник: https://blog.bytebytego.com/
👍20
День 2396. #ЧтоНовенького #NET10
Ещё Два Метода-Расширения LINQ в .NET 10

В предварительной версии 6 .NET 10 появились ещё два метода LINQ: InfiniteSequence и Sequence.

1. InfiniteSequence
InfiniteSequence — это простой генератор, позволяющий получить бесконечную последовательность из начальной точки, задавая размер шага:
var steps = 
Enumerable.InfiniteSequence(0, 5)
.Take(10)
.ToList();

foreach (var step in steps)
Console.WriteLine(step);

В результате будут выведены значения 0, 5, 10, 15, …, 45.
Замечание: будьте осторожны и не вызывайте ToList/Count и подобные функции без Take, иначе вы получите исключение «Недостаточно памяти».

2. Sequence
Это почти то же самое, что InfiniteSequence, с той разницей, что можно определить включительно верхнюю границу:
var steps = 
Enumerable.Sequence(0, 45, 5)
.ToList();

foreach (var step in steps)
Console.WriteLine(step);

Результат будет таким же, как в предыдущем примере.

Источник: https://steven-giesel.com/blogPost/5d88d808-03ac-431a-82fa-756b59b38a7d/two-more-linq-extensions-in-dotnet-10
👍28
День 2397. #ЗаметкиНаПолях
Сокращаем Шаблонный Код и Поддерживаем Согласованность Проекта. Начало


TL/DR: Если вы работаете в небольшой компании и ведёте всего один или два проекта, эта публикация может быть для вас неактуальна.

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

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

Преимущества
- Сосредоточенность на продукте: разработчики могут сосредоточиться на разработке функций, а не на настройке и поддержке общего кода.
- Более эффективное организационное воздействие: каждый дополнительный проект, ссылающийся на общую библиотеку, увеличивает её ценность.
- Ускоренная поставка: повторное использование существующих компонентов ускоряет переход от идеи к производству.
- Единообразие: способствует внедрению единых практик и стандартов во всех проектах.
- Сокращение затрат на обслуживание: меньше дублирующегося кода означает меньше задач по поддержке и тестированию.
- Расширенное сотрудничество: способствует обмену знаниями и командной работе.
- Меньше шаблонного кода: кодовые базы легче читать и поддерживать, поскольку не нужно повторять одну и ту же конфигурацию в нескольких проектах. Кроме того, благодаря сокращению шаблонного кода становится понятнее, что относится к конкретному проекту.
- Обновляемость: предоставление новых функций или исправление ошибок без необходимости внесения изменений в каждый проект.

Большую часть времени разработчики думают только о создании пакетов для кода. Однако использовать по ссылке можно не только библиотеки кода, но также файлы конфигурации, стандарты кодирования, конвейеры CI/CD и многое другое.

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

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

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

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

Источник:
https://www.meziantou.net/reduce-boilerplate-and-maintain-project-consistency.htm
👍9👎1
День 2398. #ЗаметкиНаПолях
Сокращаем Шаблонный Код и Поддерживаем Согласованность Проекта. Продолжение

Начало

1. Стандарты кодирования .NET и статический анализ
Идея заключается в создании NuGet-пакета, содержащего файлы .editorconfig, анализаторы и конфигурацию MSBuild. Этот пакет предоставляет:
- Единый стиль кодирования для всех проектов, которые на него ссылаются;
- Набор анализаторов Roslyn и базовую конфигурацию для обеспечения статического анализа в соответствии с передовыми практиками и требованиями безопасности;
- Свойства, элементы и таргеты MSBuild для настройки проекта, такие как включение обнуляемых ссылочных типов, применение стиля кода при сборке, обработка предупреждений как ошибок в непрерывной интеграции, включение аудита NuGet, включение ссылок на исходный код и многое другое.

Этот пакет позволяет удалить сотни строк шаблонного кода и конфигурации из каждого проекта:
- Разделы C# и VB.NET из файла .editorconfig (~100 строк);
- Файлы конфигурации, такие как Directory.Build.props и Directory.Build.targets (~70 строк);
- Конфигурацию анализатора (до нескольких сотен строк).

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

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

Можно добавлять некоторые функции в стандарт кодирования, например, запретить использование типов Newtonsoft.Json в проектах. Для этого достаточно добавить новое свойство в файл проекта:
<PropertyGroup>
<BanNewtonsoftJsonSymbols>true</BanNewtonsoftJsonSymbols>
</PropertyGroup>


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

Если вы используете .NET, настоятельно рекомендую почитать про MSBuild. А использование пакетов SDK для MSBuild открывает множество дополнительных возможностей.

2. Конфигурация Renovate
Renovate — это инструмент для автоматизации обновления зависимостей. Его можно настроить с помощью файла renovate.json. Файл конфигурации может быть довольно большим, особенно если у вас много зависимостей или вы хотите настроить поведение Renovate. Это может привести к дублированию данных в нескольких проектах, если у каждого проекта будет свой файл renovate.json. Кроме того, каждая команда может настраивать Renovate по-своему, что приводит к несоответствиям. Например, команды могут использовать разные форматы заголовков пул-реквестов, что затрудняет создание правил для их фильтрации.

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

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

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

Источник:
https://www.meziantou.net/reduce-boilerplate-and-maintain-project-consistency.htm
👍11
День 2399. #ЗаметкиНаПолях
Сокращаем Шаблонный Код и Поддерживаем Согласованность Проекта. Продолжение

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

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

Если вы знакомы с шаблоном .NET Aspire, вы, возможно, видели проект ServiceDefaults. Он предоставляет набор общих сервисов и конфигураций, которые можно использовать повторно в нескольких проектах в рамках одного решения. Но можно ведь создать NuGet-пакет, который можно использовать в любом проекте.
Пример проекта пакета на GitHub.

4. Задачи MSBuild
MSBuild — это расширяемая система сборки, позволяющая создавать пользовательские задачи и таргеты. На сайте nuget.org представлено несколько пакетов для добавления задач MSBuild в ваш проект. Например, вы можете автоматически устанавливать версию своего проекта с помощью MinVer, GitVersion.MsBuild или Nerdbank.GitVersioning. А, например, пакет Workleap.OpenApi.MSBuild интегрируется в процесс сборки (dotnet build), чтобы гарантировать актуальность файла спецификации OpenAPI, соответствие коду, стандартам компании и отсутствие критических изменений в API.

5. Фрагменты файлов
Иногда удалить шаблонный код невозможно. В этом случае стратегия заключается в том, чтобы поместить его в «область», которую можно легко идентифицировать и обновить. Версионирование и обновление общей части файла можно осуществлять с помощью такого инструмента, как Renovate.

Общие части файлов заключаются в комментарии # reference:<URL> и # endreference. Таким образом, вы можете легко найти общую часть файла и обновить её при необходимости. Версию общей части файла можно контролировать с помощью тегов и обновлять с помощью Renovate. Обратите внимание, что Renovate позволяет запускать пользовательские инструменты после обновления ссылки.
 
# reference:https://…/.editorconfig
root = true

[*]
indent_style = space
trim_trailing_whitespace = true
end_of_line = lf

# endreference

# Настройки специфичные для проекта
[*.cs]
dotnet_diagnostic.CA1008.severity = none


6. Конвейеры CI/CD
Конвейеры — ещё один хороший вариант для совместного использования между проектами. Существуют различные стратегии совместного использования конвейеров.
Комплаенс-конвейеры (например, SAST, DAST, сканирование зависимостей и т.д.) можно перенести на уровень организации. Это гарантирует, что все проекты будут иметь одинаковые проверки соответствия, и вы сможете обновлять их в одном месте. Это также избавит от необходимости обновлять каждый проект при изменении проверок соответствия.
GitHub предоставляет гибкие способы совместного использования частей конвейеров CI/CD:
- композитные действия
- настраиваемые действия
- повторно используемые рабочие процессы

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

Источник:
https://www.meziantou.net/reduce-boilerplate-and-maintain-project-consistency.htm
День 2400. #ЗаметкиНаПолях
Сокращаем Шаблонный Код и Поддерживаем Согласованность Проекта. Окончание

Начало
Продолжение 1-2
Продолжение 3-5

7. Dockerfile
Вам действительно нужен Dockerfile? Некоторые технологии, такие как .NET, позволяют создавать образы Docker без Dockerfile. Например, можно использовать dotnet publish для создания образа и его отправки в реестр. Это не только избавляет от шаблонного кода, но и повышает производительность. Это также позволяет избежать многих распространённых проблем с Dockerfile, таких как кэширование.
# Dockerfile не нужен
dotnet publish -p:PublishProfile=DefaultContainer


8. Helm-чарты
Поскольку вы можете устанавливать стандарты с помощью пакетов MSBuild или SDK, вы можете повторно использовать эти стандарты в Helm-чартах. Например, вы можете настроить проверки готовности и жизнеспособности в общем чарте. Вы также можете добавить параметры, специфичные для вашей инфраструктуры, такие как сертификаты, автоматическое масштабирование, идентификацию пода Azure и т.д. Таким образом, вы можете удалить шаблонный код из Helm-чартов и сделать их более пригодными для повторного использования в разных проектах. В большинстве проектов требуется задать имя образа, ограничения на ресурсы ЦП и памяти. Остальные параметры должны быть общими для большинства проектов.

9. Модули PowerShell
PowerShell — мощный язык сценариев, который можно использовать для автоматизации задач и управления системами. Он очень распространён в непрерывной интеграции (CI) или для локальных операций. Модули PowerShell — отличный способ совместного использования скриптов и функций в разных проектах. Вы можете создать модуль, содержащий общие функции и скрипты, которые можно использовать повторно в нескольких проектах, используя команду
Import-Module -Name <ModuleName> -RequiredVersion <ModuleVersion>

Модули имеют версии и публикуются как NuGet-пакеты, поэтому вы можете легко обновлять их при необходимости:
- Publish-Module
- Install-Module

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

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

Итого
Просмотрите свои проекты и определите, есть ли что-то ещё, что можно использовать по ссылке. Например:
- Конфигурациями IDE (например, .vscode, vsconfig для Visual Studio)
- Git-хуки
- Инструменты разработки, необходимые для запуска проекта (например, dev-контейнеры, GitHub Codespaces, devbox и т. д.). Фактически, обратите внимание на всё, что не является бизнес-кодом, и подумайте, можно ли удалить это из проекта.

Источник:
https://www.meziantou.net/reduce-boilerplate-and-maintain-project-consistency.htm
👍4
День 2401. #ЗаметкиНаПолях
Разница Между Выражениями и Инициализаторами Коллекций

Вы когда-нибудь задумывались, есть ли разница между выражением коллекции:
List<int> list = [1, 2, 3];

и инициализатором коллекции:
List<int> list2 = new() {1,2,3};


Давайте выясним. Для этого посмотрим, во что преобразуется каждая строчка.

Выражение коллекции:
int num = 3;
List<int> list = new List<int>(num);
CollectionsMarshal.SetCount(list, num);
Span<int> span = CollectionsMarshal.AsSpan(list);
int num2 = 0;
span[num2] = 1;
num2++;
span[num2] = 2;
num2++;
span[num2] = 3;
num2++;


Инициализатор коллекции:
List<int> list2 = new List<int>();
list2.Add(1);
list2.Add(2);
list2.Add(3);


Таким образом, выражения для коллекций ([1,2,3]) «умнее» и быстрее в том смысле, что они заранее выделяют список с точным количеством элементов, которые мы хотим добавить. Инициализатор коллекции не делает этого по одной простой причине: компилятор, согласно своей спецификации, обязан вызывать метод Add для каждого элемента в инициализаторе.

Конечно, для 3х элементов особой разницы в производительности не будет. А вот, например, бенчмарк для 17 элементов:
|    Method |     Mean | Allocated |
|-----------|---------:|----------:|
|Expression | 18.09 ns | 128 B |
|Initializer| 72.88 ns | 368 B |

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

Источник: https://steven-giesel.com/blogPost/fea0b033-ccf5-4197-b62c-ffd8ca6d79c7/quick-one-difference-between-collection-expressions-and-collection-initializer
👍41
День 2402. #ЧтоНовенького #NET10
Новинки в .NET 10 Превью 7.
ASP.NET Core

1. В промежуточное ПО обработки исключений ASP.NET Core добавлен новый параметр конфигурации для управления диагностическим выводом ExceptionHandlerOptions.SuppressDiagnosticsCallback. Этот метод обратного вызова получает контекст запроса и исключения, позволяя добавить логику, определяющую, должно ли промежуточное ПО записывать логи исключения и другие телеметрические данные.
Это полезно, когда вы знаете, что исключение является временным или уже где-то обрабатывается, и вы не хотите, чтобы логи этого исключения засоряли ваш дашборд.
Кроме того, изменилось поведение промежуточного ПО обработки исключений по умолчанию: диагностические данные для исключений, обрабатываемых IExceptionHandler, больше не записываются. Согласно отзывам пользователей, логирование обработанных исключений на уровне ошибок часто было нежелательным, если IExceptionHandler.TryHandleAsync возвращал true. Вернуть предыдущее поведение можно, добавив следующий код:
app.UseExceptionHandler(new ExceptionHandlerOptions
{
SuppressDiagnosticsCallback = context => false;
});


2. По умолчанию неаутентифицированные и неавторизованные запросы к известным конечным точкам API, защищенным аутентификацией через cookie, теперь приводят к ответам 401 и 403, а не к перенаправлению на URI входа в систему. Такое перенаправление обычно не имеет смысла для API.
Конечные точки API идентифицируются с помощью нового интерфейса IApiEndpointMetadata, и его реализация была автоматически добавлена для следующих конечные точек:
- в [ApiController];
- в минимальных API, принимающих JSON в теле запроса или выдающих JSON-ответы;
- использующих типы возврата TypedResults;
- в SignalR.
Если вы хотите предотвратить это новое поведение, вы можете переопределить события RedirectToLogin и RedirectToAccessDenied при вызове builder.Services.AddAuthentication().AddCookie(…).

3. Упрощены и расширены API аутентификации по ключу доступа (passkey) в шаблонах веб-приложений Blazor, что упрощает реализацию сценариев входа без пароля. Можно попробовать новый шаблон приложения Blazor с поддержкой ключа доступа:
dotnet new blazor -au Individual


4. ASP.NET Core теперь полностью поддерживает домен верхнего уровня .localhost, что позволяет разработчикам запускать несколько локальных приложений с более чётким разделением доменов. Встроенный веб-сервер Kestrel распознаёт адреса .localhost как локальные (loopback), обеспечивая безопасную и согласованную локальную разработку.

5. MVC, Minimal API и методы HttpRequestJsonExtensions.ReadFromJsonAsync были обновлены для поддержки PipeReader в System.Text.Json, что не требует внесения каких-либо изменений в код приложений.
Для большинства приложений это не должно повлиять на поведение. Однако, если приложение использует кастомный JsonConverter, существует вероятность, что конвертер будет некорректно обрабатывать Utf8JsonReader.HasValueSequence. Это может привести к потере данных и возникновению ошибок, таких как ArgumentOutOfRangeException, при десериализации.
Быстрый способ решения проблемы (особенно если вы не являетесь владельцем используемого кастомного JsonConverter) — установить переключатель AppContext "Microsoft.AspNetCore.UseStreamBasedJsonParsing" в значение true. Это должно быть временным решением, а JsonConverter(ы) следует обновить для поддержки HasValueSequence.

6. Теперь можно использовать атрибуты валидации как для классов, так и для записей, что обеспечивает единообразие генерации кода и поведения валидации. Это повышает гибкость при проектировании моделей с использованием записей в приложениях ASP.NET Core.

Источник: https://www.infoq.com/news/2025/08/dotnet-10-preview-7/
👍9