.NET Разработчик
Ir al canal en Telegram
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#. Для связи: @SBenzenko Поддержать канал: - https://boosty.to/netdeveloperdiary - https://patreon.com/user?u=52551826 - https://pay.cloudtips.ru/p/70df3b3b
Mostrar más6 739
Suscriptores
+424 horas
+147 días
+1330 días
Carga de datos en curso...
Canales Similares
Nube de Etiquetas
Menciones Entrantes y Salientes
---
---
---
---
---
---
Atraer Suscriptores
septiembre '26
septiembre '26
+50
en 0 canales
agosto '26
+55
en 0 canales
Get PRO
julio '26
+99
en 0 canales
Get PRO
junio '26
+70
en 0 canales
Get PRO
mayo '26
+80
en 0 canales
Get PRO
abril '26
+74
en 0 canales
Get PRO
marzo '26
+65
en 0 canales
Get PRO
febrero '26
+65
en 0 canales
Get PRO
enero '26
+96
en 0 canales
Get PRO
diciembre '25
+83
en 1 canales
Get PRO
noviembre '25
+154
en 0 canales
Get PRO
octubre '25
+105
en 0 canales
Get PRO
septiembre '25
+68
en 0 canales
Get PRO
agosto '25
+94
en 0 canales
Get PRO
julio '25
+90
en 0 canales
Get PRO
junio '25
+132
en 0 canales
Get PRO
mayo '25
+153
en 0 canales
Get PRO
abril '25
+146
en 0 canales
Get PRO
marzo '25
+202
en 0 canales
Get PRO
febrero '25
+245
en 0 canales
Get PRO
enero '25
+148
en 1 canales
Get PRO
diciembre '24
+195
en 0 canales
Get PRO
noviembre '24
+189
en 0 canales
Get PRO
octubre '24
+204
en 0 canales
Get PRO
septiembre '24
+199
en 1 canales
Get PRO
agosto '24
+157
en 0 canales
Get PRO
julio '24
+193
en 0 canales
Get PRO
junio '24
+218
en 0 canales
Get PRO
mayo '24
+236
en 0 canales
Get PRO
abril '24
+264
en 0 canales
Get PRO
marzo '24
+334
en 0 canales
Get PRO
febrero '24
+316
en 0 canales
Get PRO
enero '24
+340
en 1 canales
Get PRO
diciembre '23
+325
en 1 canales
Get PRO
noviembre '23
+53
en 0 canales
Get PRO
octubre '23
+53
en 0 canales
Get PRO
septiembre '23
+125
en 0 canales
Get PRO
agosto '23
+90
en 0 canales
Get PRO
julio '23
+61
en 0 canales
Get PRO
junio '23
+57
en 0 canales
Get PRO
mayo '23
+49
en 0 canales
Get PRO
abril '23
+48
en 0 canales
Get PRO
marzo '23
+58
en 0 canales
Get PRO
febrero '23
+47
en 0 canales
Get PRO
enero '23
+121
en 0 canales
Get PRO
diciembre '22
+101
en 0 canales
Get PRO
noviembre '22
+89
en 0 canales
Get PRO
octubre '22
+110
en 0 canales
Get PRO
septiembre '22
+105
en 0 canales
Get PRO
agosto '22
+72
en 0 canales
Get PRO
julio '22
+58
en 0 canales
Get PRO
junio '22
+45
en 0 canales
Get PRO
mayo '22
+66
en 0 canales
Get PRO
abril '22
+159
en 0 canales
Get PRO
marzo '22
+77
en 0 canales
Get PRO
febrero '22
+55
en 0 canales
Get PRO
enero '22
+93
en 0 canales
Get PRO
diciembre '21
+90
en 0 canales
Get PRO
noviembre '21
+80
en 0 canales
Get PRO
octubre '21
+76
en 0 canales
Get PRO
septiembre '21
+83
en 0 canales
Get PRO
agosto '21
+189
en 0 canales
Get PRO
julio '21
+68
en 0 canales
Get PRO
junio '21
+495
en 0 canales
Get PRO
mayo '21
+65
en 0 canales
Get PRO
abril '21
+53
en 0 canales
Get PRO
marzo '21
+152
en 0 canales
Get PRO
febrero '21
+71
en 0 canales
Get PRO
enero '21
+60
en 0 canales
Get PRO
diciembre '20
+1 706
en 0 canales
| Fecha | Crecimiento de Suscriptores | Menciones | Canales | |
| 15 septiembre | +4 | |||
| 14 septiembre | +2 | |||
| 13 septiembre | +5 | |||
| 12 septiembre | +5 | |||
| 11 septiembre | +2 | |||
| 10 septiembre | +2 | |||
| 09 septiembre | +2 | |||
| 08 septiembre | +4 | |||
| 07 septiembre | +7 | |||
| 06 septiembre | +4 | |||
| 05 septiembre | +1 | |||
| 04 septiembre | +6 | |||
| 03 septiembre | +1 | |||
| 02 septiembre | +3 |
Publicaciones del Canal
| 2 | День 2785. #ЗаметкиНаПолях
Обеспечиваем Изоляцию Тенантов с Помощью PostgreSQL
Механизм защиты на уровне строк (Row-Level Security, RLS) в PostgreSQL добавляет проверку на стороне БД поверх фильтров запросов EF Core. Он контролирует операции чтения и записи при условии, что приложение подключается под ролью, не являющейся владельцем таблицы, и устанавливает идентификатор тенанта при каждом открытии соединения.
Популярна рекомендация использовать глобальные фильтры запросов для реализации мультитенантности. EF Core добавляет условие WHERE tenant_id = @tenant к каждому генерируемому запросу, поэтому разработчикам не нужно помнить об этом предикате. Однако действие фильтра ограничено: SQL-команды, отправляемые через ExecuteSql, присоединённые сущности, а также запросы с вызовом IgnoreQueryFilters() остаются вне зоны его влияния.
Требуется дополнительный уровень проверки, не зависящий от того, помнит ли каждый разработчик о первом механизме. В PostgreSQL такая возможность уже встроена благодаря RLS.
Настройка правила в PostgreSql
Защита на уровне строк (RLS) представляет собой предикат, который PostgreSQL автоматически добавляет к каждой команде, выполняемой для таблицы (за исключением ролей, имеющих привилегию обхода RLS). Приложение не может «забыть» об этом условии, т.к. оно даже не видит его.
Суперпользователи, роли с атрибутом BYPASSRLS и владельцы таблиц обходят это ограничение. Во многих приложениях один и тот же пользователь БД выполняет миграции, обрабатывает запросы и является владельцем таблиц. Поэтому в этом примере приложение подключается под ролью app_user, которая не владеет объектами и имеет лишь необходимые права DML, тогда как миграции выполняются от имени отдельной роли-владельца. Такое разделение также предотвращает возможность выполнения команды DROP POLICY со стороны приложения. Вот пример политики для таблицы счетов (invoices):
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid)
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid);
Условие USING определяет, к каким строкам возможен доступ при чтении, обновлении или удалении, а WITH CHECK отклоняет операции вставки или обновления, если новый tenant_id не совпадает с параметром сессии. Использование NULLIF гарантирует, что отсутствующий или пустой параметр не будет соответствовать ничему — именно такой вариант сбоя предпочтителен в данном случае. Опция FORCE распространяет действие политики и на владельца таблицы, предотвращая случайное изменение строк всех тенантов скриптами миграции.
Функция current_setting возвращает текущее значение параметра конфигурации.
Установка идентификатора тенанта для каждого соединения
Политика использует переменную сессии, поэтому приложение должно её устанавливать; логичнее всего делать это в начале обработки запроса. Однако такой подход быстро приводит к проблемам.
EF Core открывает соединение для выполнения команды и закрывает его по завершении, а Npgsql сбрасывает состояние сессии в пуле; в результате запрос, следующий за командой SET, выполняется в соединении, «не знающем» об этом тенанте. Отключение сброса с помощью параметра No Reset On Close=true делает ситуацию ещё хуже: следующий запрос «наследует» настройки предыдущего тенанта и получает доступ к строкам, которые не должен видеть.
Решение в том, чтобы устанавливать идентификатор тенанта при каждом открытии соединения — независимо от того, какое именно физическое соединение предоставляет Npgsql. Эту задачу выполняет перехватчик соединений (connection interceptor), использующий TenantContext для хранения идентификатора тенанта, авторизованного для текущего запроса:
public class TenantConnectionInterceptor(
TenantContext tenant)
: DbConnectionInterceptor
{
public override void ConnectionOpened(
DbConnection conn,
ConnectionEndEventData eventData)
{
using var cmd = Build(conn);
cmd.ExecuteNonQuery();
}
public override async Task ConnectionOpenedAsync(
DbConnection conn,
ConnectionEndEventData eventData,
CancellationToken ct = default)
{
await using var cmd = Build(conn);
await cmd.ExecuteNonQueryAsync(ct);
}
private DbCommand Build(DbConnection conn)
{
var cmd = conn.CreateCommand();
cmd.CommandText =
"SELECT set_config('app.tenant_id', @tenant, false)";
var prm = cmd.CreateParameter();
prm.ParameterName = "tenant";
prm.Value = tenant.TenantId?.ToString() ?? "";
cmd.Parameters.Add(prm);
return cmd;
}
}
Метод set_config принимает tenant в качестве параметра привязки, а значение false указывает на область видимости - сессия. Конструкция ?? "" обеспечивает блокировку запроса в случае отсутствия tenant.
Регистрируем сервисы с соответствующей областью видимости в Program.cs:
builder.Services.AddScoped<TenantContext>();
builder.Services.AddScoped<TenantConnectionInterceptor>();
builder.Services.AddDbContext<AppDbContext>(
(sp, opts) => opts
.UseNpgsql(connString)
.AddInterceptors(
sp.GetRequiredService<TenantConnectionInterceptor>())
);
Здесь используется AddDbContext, а не AddDbContextPool, т.к. контекст из пула сохранял бы TenantContext от первого запроса. При работе через PgBouncer в режиме пулинга транзакций идентификатор арендатора (tenant) необходимо задавать с помощью set_config(…, true) внутри явной транзакции.
Это влечет за собой дополнительные затраты в виде лишнего сетевого запроса при каждом открытии соединения — примерно 0,4 мс на запрос.
Попытка обойти ограничение
Отключим фильтр:
var affected = await db.Invoices
.IgnoreQueryFilters()
.Where(i => i.Id == otherTenantId)
.ExecuteUpdateAsync(
s => s.SetProperty(i => i.Status, "Cancelled"),
cancellationToken);
// affected == 0
Метод ExecuteUpdateAsync отправляет SQL-запрос немедленно, минуя механизмы отслеживания изменений и SaveChanges, поэтому фильтр не применяется. Поскольку строка принадлежит другому тенанту, политика скрывает её, и операция обновления не затрагивает ни одной записи.
Аналогично при работе с присоединённой (attached) сущностью. EF Core формирует запрос вида UPDATE invoices SET … WHERE id = @p0 на основе первичного ключа, политика добавляет свой предикат, и в итоге ни одна строка не удовлетворяет условиям. EF сообщает об этом как об исключении DbUpdateConcurrencyException (ошибка параллелизма), что выглядит как проигрыш в состоянии гонки при оптимистической блокировке, хотя данная строка изначально не была видна в текущей сессии.
Чтение без фильтрации по-прежнему возвращает только счета текущего тенанта, а попытка вставки записи с идентификатором другого тенанта завершается ошибкой с кодом SQLSTATE 42501.
Чего политика не может сделать, так это определить, какой тенант является «правильным». Если приложение ошибочно определит тенанта, политика применит ограничения именно для этого (неверного) тенанта.
Итого
Для многотенантных приложений рекомендуется использовать фильтр запросов EF в сочетании с политикой RLS. Фильтр включает идентификатор тенанта в SQL-запрос, благодаря чему планы выполнения остаются простыми, намерение разработчика ясно видно в коде, а накладные расходы во время выполнения отсутствуют. Политика же обеспечивает защиту даже в тех случаях, когда стандартный фильтр не задействован — например, при использовании IgnoreQueryFilters() для формирования административного отчёта.
Не забудьте предусмотреть роль, позволяющую обходить политику безопасности; это необходимо для выполнения резервного копирования и фоновых задач, затрагивающих данные разных тенантов.
Источник: https://milanjovanovic.tech/blog/postgres-row-level-security-with-ef-core | 768 |
| 3 | День 2784. #Оффтоп
Утиная Типизация в C# с Помощью Перехватчиков
Если это ходит как утка и крякает как утка — значит, это утка. Применим утиную типизацию и заставим это работать в C# с помощью перехватчиков!
Примечание: этот пост написан в образовательных целях, но вы смело можете использовать описанный подход в проде 😉
В TypeScript можно сделать вот так:
class A {
Do(): void { console.log("A.Do"); }
}
class B {
Do(): void { console.log("B.Do"); }
}
function foo(a: { Do(): void }) {
a.Do();
}
Попробуем сделать что-то подобное в C#:
public class A
{
public void Do() => Console.WriteLine("A.Do");
}
public class B
{
public void Do() => Console.WriteLine("B.Do");
}
public void Foo(??? a) => a.Do();
У классов A и B нет ничего общего: ни базового класса, ни общего интерфейса. Нужно придумать некую «форму» (обозначим её как ???) — нечто такое, что позволит успешно скомпилировать вызовы Foo(new A()) и Foo(new B()) и обеспечит вызов нужного метода Do() в каждом случае, при этом вообще не затрагивая сами классы A и B.
Кроме того, запрещено использовать следующие подходы:
- dynamic (хотя это и невероятно крутая штука);
- аргумент типа object с последующим приведением типов через is или as;
- модификацию классов A или B (например, добавление общего базового класса или интерфейса).
Перехватчики
Начиная с .NET 8, в C# появилась экспериментальная функция компилятора — перехватчики. Генератор исходного кода может создать метод, пометить его атрибутом [InterceptsLocation], указав конкретное место вызова в вашем коде, и компилятор незаметно перенаправит вызов на этот метод. Это происходит без каких-либо затрат ресурсов во время выполнения, так как всё разрешается на этапе компиляции.
Обычно это реализуется через сочетание пользовательских атрибутов (например, DuckType и DuckShape), создаваемых генератором кода, и логики перехватчика: система находит все места вызовов и выполняет необходимые действия.
Таким образом, с точки зрения использования нам всё равно потребуется нечто вроде интерфейса (или свойства) для «описания формы»:
[DuckShape]
public interface IDoable { void Do(); }
public static partial class Ops
{
[DuckTyped]
public static void Foo(IDoable a) => a.Do();
}
Вы пишете Foo(IDoable a) => a.Do() - именно так, как вам хочется. Атрибут [DuckShape] помечает IDoable как структурный контракт, а не как интерфейс, который нужно реализовывать вручную. Атрибут [DuckTyped] сообщает генератору: «Вот настоящая логика; сделай так, чтобы её можно было вызывать с любым объектом, подходящим по структуре».
Полный код генератора/перехватчика на GitHub, а здесь рассмотрим наиболее интересные фрагменты.
Так или иначе, каждый вызов сводится к следующей обобщённой конструкции:
[InterceptsLocation(1, "…")]
public static void Interceptor_1(A value)
{
Ops.Foo((IDoable)(new ShapeAdapter_IDoable_A(value)));
}
ShapeAdapter_IDoable_A - это небольшая генерируемая структура только для чтения, которая реализует IDoable, перенаправляя сразу в A:
internal readonly struct
ShapeAdapter_IDoable_A : IDoable
{
private readonly A _value;
public ShapeAdapter_IDoable_A(A value)
=> _value = value;
public void Do() => _value.Do();
}
Вот и весь фокус. Это ведёт к незначительным накладным расходам (возможно инлайнинг поможет с этим). Таким образом, следующий код работает прекрасно:
Ops.Foo(new A()); // "A.Do"
Ops.Foo(new B()); // "B.Do"
Это также работает для свойств:
[DuckShape]
public interface INameable
{ string Name { get; set; } }
public class Person
{ public string Name { get; set; } = ""; }
[DuckTyped]
public static string Greet(INameable n)
=> $"Hello, {n.Name}!";
Greet(new Person { Name = "Jon" }); // "Hello, Jon!"
Это можно оформить в NuGet, если кто-то увидит в этом реальный смысл.
Итого
Полезно ли это? Вероятно, нет — но не в этом главная цель. Хотя язык не поддерживает это напрямую, подобное поведение можно в определённой мере «эмулировать».
Источник: https://steven-giesel.com/blogPost/5170e165-29e8-437a-b5ce-446a84943809/quack-quack-ducktyping-in-c-with-interceptors | 841 |
| 4 | День 2783. #ЗаметкиНаПолях
Паттерн «Производитель-потребитель» c System.Threading.Channels. Окончание
Начало
Продолжение
Настройка и запуск нескольких потребителей
Зарегистрируйте канал как синглтон, чтобы производитель и потребитель использовали один и тот же экземпляр, а затем добавьте столько экземпляров потребителя, сколько требуется для обеспечения нужной пропускной способности:
builder.Services.AddSingleton(_ =>
Channel.CreateBounded<WorkItem>(
new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait
}));
// 3 потребителя одного канала
builder.Services.AddHostedService<WorkConsumer>();
builder.Services.AddHostedService<WorkConsumer>();
builder.Services.AddHostedService<WorkConsumer>();
Количество потребителей — регулятор уровня параллелизма. Один потребитель обрабатывает задачи строго последовательно, а 3 — до трёх одновременно. Настраивайте их количество с учётом возможностей последующего этапа обработки: если ProcessAsync обращается к БД, поддерживающей не более 10 одновременных операций записи, не стоит запускать 50 потребителей.
Корректное завершение работы
При остановке приложения в канале могут оставаться необработанные элементы. Если просто завершить процесс, они будут потеряны. Решение – закрыть канал записи при остановке и позволить потребителям обработать оставшиеся данные. За обработку сигнала отвечает небольшой фоновый сервис:
public class ChannelCompleter : IHostedService
{
private readonly ChannelWriter<WorkItem> _writer;
public ChannelCompleter(Channel<WorkItem> ch)
=> _writer = ch.Writer;
public Task StartAsync(CancellationToken ct)
=> Task.CompletedTask;
public Task StopAsync(CancellationToken ct)
{
_writer.Complete();
return Task.CompletedTask;
}
}
После вызова Complete() метод WriteAsync будет генерировать исключение при попытке записи новым производителем, а метод ReadAllAsync каждого потребителя продолжит выдавать элементы до тех пор, пока буфер не опустеет, после чего цикл завершится. Предоставьте хосту достаточно времени для завершения обработки всех данных, настроив параметр ShutdownTimeout:
builder.Services.Configure<HostOptions>(o =>
o.ShutdownTimeout = TimeSpan.FromSeconds(30));
Теперь при развёртывании новой версии системы очередь корректно опустошается, а не просто теряет все задачи, находившиеся в процессе обработки.
Когда использовать
Жизненный цикл каналов неразрывно связан с процессом приложения. Если приложение перезапускается, накопленные в буфере элементы исчезают: здесь нет ни записи на диск, ни механизма повторного воспроизведения, ни подтверждения доставки. Это вполне допустимо для задач, потерю которых можно себе позволить или которые можно выполнить заново: например, создание миниатюр изображений, предварительный прогрев кэша или отправка некритичных уведомлений. Однако такой подход неприемлем для обработки платежей или отправки email.
Если вам нужны гарантии сохранности данных при перезапусках, механизмы повторных попыток с обработкой «мертвых» сообщений или распределение задач между разными сервисами — значит, вы переросли возможности каналов. Тогда лучше использовать полноценный брокер сообщений или паттерн Outbox.
Важно осознавать ограничения системы до того, как вы выпустите её в прод, а не после того, как первая же перезагрузка приведет к потере всей очереди.
FAQ
1. В чём разница между ограниченным (bounded) и неограниченным (unbounded) каналом?
Неограниченный канал принимает данные для записи бесконечно, поэтому при быстром производителе и медленном потребителе очередь будет расти, пока не закончится память. Ограниченный канал имеет фиксированную ёмкость; когда он заполнен, операции записи приостанавливаются (или данные отбрасываются, в зависимости от режима обработки переполнения), пока потребитель не обработает накопленные элементы.
2. Чем Channel отличается от BlockingCollection?
BlockingCollection блокирует вызывающий поток, когда коллекция заполнена или пуста, тем самым занимая поток из пула на время ожидания. В канале методы WriteAsync и ReadAsync освобождают поток, поэтому ожидающий производитель или потребитель не потребляет ресурсы потока.
Итого
С появлением System.Threading.Channels реализация паттерна «производитель-потребитель» не требует сторонних библиотек. Ограниченный канал обеспечивает потокобезопасную асинхронную передачу данных с реальным механизмом обратного давления.
Критически важно:
- ограничить размер канала, чтобы всплеск нагрузки не привёл к падению процесса,
- корректно завершать работу писателя при выключении, чтобы накопленные данные обрабатывались, а не отбрасывались.
Если всё сделать правильно, то конечная точка, которая раньше «падала» под наплывом запросов на загрузку, продолжит стабильно работать.
Источник: https://thecodeman.net/posts/producer-consumer-with-channels-in-dotnet | 849 |
| 5 | День 2782. #ЗаметкиНаПолях
Паттерн «Производитель-потребитель» c System.Threading.Channels. Продолжение
Начало
Производитель: конечная точка, передающая задачи
Производитель просто выполняет запись. Единственный важный нюанс — как поступить, если канал переполнен; метод WriteAsync берёт это на себя: он завершается немедленно, если есть свободное место, или ожидает, пока потребитель освободит слот.
[ApiController]
[Route("api/[controller]")]
public class ProcessController : ControllerBase
{
private readonly ChannelWriter<WorkItem> _writer;
public ProcessController(Channel<WorkItem> ch)
=> _writer = channel.Writer;
[HttpPost]
public async Task<IActionResult> Enqueue(
WorkItem item, CancellationToken ct)
{
// Ждёт, если канал полный
await _writer.WriteAsync(item, ct);
return Accepted();
}
}
Если при перегрузке системы вы предпочитаете отклонять запросы (что правильнее публичного API, для предотвращения атак), используйте TryWrite и возвращайте код 429:
if (!_writer.TryWrite(item))
return StatusCode(
StatusCodes.Status429TooManyRequests,
"Сервис занят, попробуйте позже.");
return Accepted();
TryWrite никогда не блокирует выполнение. Он возвращает false сразу же, как только канал оказывается заполненным, что позволяет преобразовать эту ситуацию в чёткий ответ 429 вместо ожидания. Выбор стратегии зависит от того, кто инициирует вызов: если это внутренняя пакетная задача, можно позволить ей подождать, а если публичная конечная точка — лучше сразу отклонить запрос.
Потребитель: сервис, считывающий данные из канала
Потребитель реализован в виде BackgroundService, т.е. запускается и останавливается вместе с приложением. Весь цикл обработки умещается в одну строку благодаря методу ReadAllAsync, который выдаёт элементы до тех пор, пока канал не будет закрыт:
public class WorkConsumer : BackgroundService
{
private ChannelReader<WorkItem> _reader;
private ILogger<WorkConsumer> _logger;
public WorkConsumer(
Channel<WorkItem> channel,
ILogger<WorkConsumer> logger)
{
_reader = channel.Reader;
_logger = logger;
}
protected override async Task ExecuteAsync(
CancellationToken stopToken)
{
await foreach (var item in
_reader.ReadAllAsync(stopToken))
{
try
{
await ProcessAsync(item, stopToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Ошибка обработки {Id}", item.Id);
}
}
}
private async Task ProcessAsync(
WorkItem item, CancellationToken ct)
{
// … обработка элемента …
}
}
Замечания:
- try/catch должен находиться внутри цикла и охватывать 1 элемент: если же он будет охватывать весь await foreach, то первое же исключение прервёт цикл, и потребитель завершит работу, в то время как приложение продолжит принимать новые задачи;
- ReadAllAsync корректно завершает выполнение, когда канал закрывается, что обеспечивает штатное завершение работы (об этом далее…).
Окончание следует…
Источник: https://thecodeman.net/posts/producer-consumer-with-channels-in-dotnet | 954 |
| 6 | День 2782. #ЗаметкиНаПолях
Паттерн «Производитель-потребитель» c System.Threading.Channels. Продолжение
Начало
Производитель: конечная точка, передающая задачи
Производитель просто выполняет запись. Единственный важный нюанс — как поступить, если канал переполнен; метод WriteAsync берёт это на себя: он завершается немедленно, если есть свободное место, или ожидает, пока потребитель освободит слот.
[ApiController]
[Route("api/[controller]")]
public class ProcessController : ControllerBase
{
private readonly ChannelWriter<WorkItem> _writer;
public ProcessController(Channel<WorkItem> ch)
=> _writer = channel.Writer;
[HttpPost]
public async Task<IActionResult> Enqueue(
WorkItem item, CancellationToken ct)
{
// Ждёт, если канал полный
await _writer.WriteAsync(item, ct);
return Accepted();
}
}
Если при перегрузке системы вы предпочитаете отклонять запросы (что правильнее публичного API, для предотвращения атак), используйте TryWrite и возвращайте код 429:
if (!_writer.TryWrite(item))
return StatusCode(
StatusCodes.Status429TooManyRequests,
"Сервис занят, попробуйте позже.");
return Accepted();
TryWrite никогда не блокирует выполнение. Он возвращает false сразу же, как только канал оказывается заполненным, что позволяет преобразовать эту ситуацию в чёткий ответ 429 вместо ожидания. Выбор стратегии зависит от того, кто инициирует вызов: если это внутренняя пакетная задача, можно позволить ей подождать, а если публичная конечная точка — лучше сразу отклонить запрос.
Потребитель: сервис, считывающий данные из канала
Потребитель реализован в виде BackgroundService, т.е. запускается и останавливается вместе с приложением. Весь цикл обработки умещается в одну строку благодаря методу ReadAllAsync, который выдаёт элементы до тех пор, пока канал не будет закрыт:
public class WorkConsumer : BackgroundService
{
private ChannelReader<WorkItem> _reader;
private ILogger<WorkConsumer> _logger;
public WorkConsumer(
Channel<WorkItem> channel,
ILogger<WorkConsumer> logger)
{
_reader = channel.Reader;
_logger = logger;
}
protected override async Task ExecuteAsync(
CancellationToken stopToken)
{
await foreach (var item in
_reader.ReadAllAsync(stopToken))
{
try
{
await ProcessAsync(item, stopToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Ошибка обработки {Id}", item.Id);
}
}
}
private async Task ProcessAsync(
WorkItem item, CancellationToken ct)
{
// … обработка элемента …
}
}
Замечания:
- try/catch должен находиться внутри цикла и охватывать 1 элемент: если же он будет охватывать весь await foreach, то первое же исключение прервёт цикл, и потребитель завершит работу, в то время как приложение продолжит принимать новые задачи;
- ReadAllAsync корректно завершает выполнение, когда канал закрывается, что обеспечивает штатное завершение работы (об этом далее…).
Окончание следует…
Источник: https://thecodeman.net/posts/producer-consumer-with-channels-in-dotnet | 55 |
| 7 | День 2781. #ЗаметкиНаПолях
Паттерн «Производитель-потребитель» c System.Threading.Channels. Начало
Представим эндпоинт, принимающий файлы, изменяющий их размер и возвращающий ответ. В демо-версии всё работает отлично. Но в проде нагрузка растёт, и эндпойнт получает сотни запросов в секунду. Каждый запрос запускает задачу по изменению размера прямо в потоке обработки; CPU загружается до 100%, а остальные функции приложения начинают завершаться по таймауту, т.к. пул потоков перегружен.
Не обязательно обрабатывать запросы сразу. Достаточно принять данные и обработать их в удобном для системы темпе. Это классическая задача типа «производитель-потребитель»: одна сторона передает задачу, а другая выполняет её со скоростью, которую реально может поддерживать. Для простейшей её реализации не нужны брокеры сообщений, достаточно System.Threading.Channels. Далее рассмотрим реализацию.
Каналы в .NET позволяют передавать данные между производителями и потребителями, работающими параллельно в одном процессе. Производители записывают данные в ChannelWriter<T>, потребители считывают их из ChannelReader<T>, а канал обеспечивает потокобезопасную асинхронную передачу между ними. Важный момент — канал с ограниченным размером (bounded) автоматически обеспечивает механизм «обратного давления» (backpressure).
Почему «наивное» решение только всё усугубляет
Первый порыв в решении проблемы – сделать всё асинхронным: запустить задачу через Task.Run и сразу вернуть ответ:
[HttpPost("process")]
public IActionResult Process(UploadRequest request)
{
// Запустил и забыл. Выглядит асинхронно
_ = Task.Run(() => _imgService.Resize(request));
return Accepted();
}
Такой подход обеспечивает быстрый отклик, поэтому кажется удачным решением. Но это не так. Количество запускаемых задач ничем не ограничено, поэтому всплеск нагрузки, который раньше «вешал» CPU, делает это снова — при этом всем отправляется ответ 202, сигнализирующий об успешном принятии запроса. Если происходит перезапуск процесса, эта работа бесследно исчезает. А т.к. за задачами никто не следит, возникающие в них исключения просто пропадают. В итоге вы меняете явное замедление системы на скрытую потерю огромного объёма работы.
Правильное решение — использовать очередь с ограничением размера между двумя компонентами. Когда очередь заполняется, отправитель вынужден ждать; это ожидание служит сигналом о том, что задачи поступают быстрее, чем система успевает их обрабатывать, — и именно эту информацию важно выявлять, а не скрывать.
Основы работы с каналами
У Channel<T> есть два конца. Вы создаёте его и передаёте каждый конец соответствующей стороне:
using System.Threading.Channels;
// Вместимость 100. При заполнении писатели ждут
var ch = Channel.CreateBounded<WorkItem>(
new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = false,
SingleWriter = false
});
ChannelWriter<WorkItem> writer = ch.Writer;
ChannelReader<WorkItem> reader = ch.Reader;
Режим переполнения (FullMode) — ключевой элемент:
- Wait — WriteAsync ждёт появления свободного места (по умолчанию);
- DropWrite — молча отбрасывает записываемый элемент;
- DropOldest — заменяет самый старый элемент из очереди входящим;
- DropNewest — заменяет самый новый элемент из очереди входящим.
Режим Wait подходит, когда важен каждый элемент и лучше замедлить работу производителя, чем потерять данные. Режимы Drop* предназначены для телеметрии и потоков данных в реальном времени, где свежий элемент важнее полной истории: например, при передаче метрик отбросить самое старое показание вполне допустимо.
Параметры SingleReader и SingleWriter служат для оптимизации. Устанавливайте их в true, только когда у вас действительно ровно один читатель или один писатель — так канал использует более быстрый внутренний путь обработки. Если сомневаетесь, оставляйте false: ошибочный true может привести к трудноуловимому состоянию гонки.
Запросы поступают из множества потоков и записываются в один канал. Опустошением канала занимается небольшой фиксированный пул потребителей. Когда канал переполняется, операция записи приостанавливается, и сигнал обратного давления передаётся непосредственно вызывающему коду — именно это и требуется, так как замедление становится явным и предсказуемым.
Продолжение следует…
Источник: https://thecodeman.net/posts/producer-consumer-with-channels-in-dotnet | 1 118 |
| 8 | День 2780. #ЧтоНовенького #VS
Избегаем Путаницы при Переключении Между Окнами Visual Studio
Если вы запускаете несколько экземпляров Visual Studio одновременно для нескольких решений, все они выглядят одинаково. Бывало ли, что вы переключались между окнами и начинали работать не в том? Было бы здорово, если бы для каждого решения можно было задать свою цветовую тему. Эта функция уже реализована, и не только в отношении цвета.
Откройте меню Tools > Options (Инструменты > Параметры). В верхней части окна найдите выпадающий список Applies to (Применить к) и переключите его с профиля пользователя на текущее решение (Current solution). С этого момента любые изменения настроек будут применяться только к открытому решению. Если вы закроете его и откроете позже снова, тема сохранится. А при открытии другого решения вы увидите назначенную для него цветовую схему. Выбирайте цвета с заметным контрастом для решений, с которыми работаете одновременно. Например: темная тема для сервиса, синяя — для клиентской части. Цель — различать их мгновенно, не вчитываясь в заголовок окна.
Выпадающий список Applies to — не просто функция для выбора цвета. Это модель определения области действия в новой системе настроек, а цветовая тема — лишь самый наглядный пример того, как эту модель можно использовать. Новая система объединяет настройки в единый согласованный интерфейс с возможностью поиска и поддержкой формата JSON.
Таким образом, главное нововведение заключается в том, что настройки теперь имеют область действия и привязаны к файлу, а файл можно добавить в систему контроля версий. Настройки, привязанные к конкретному решению, хранятся в файле settings.VisualStudio.json в корневой папке решения — прямо рядом с файлом .sln или .slnx. Можно добавить этот файл в систему контроля версий, и каждый, кто клонирует репозиторий, получит те же настройки. Новый коллега откроет решение, и IDE сразу будет выглядеть и вести себя так, как договорилась команда, — без необходимости изучать инструкции по настройке: «Перейдите в меню Tools > Options и измените вот эти девять параметров». Не хотите навязывать свои предпочтения остальным? Добавьте файл в .gitignore, и настройки останутся только вашими.
Важный нюанс
Настройки уровня решения хранятся отдельно от общих пользовательских настроек, поэтому изменение параметров для конкретного проекта не затронет конфигурацию, которую вы используете повсеместно. Чтобы изменить настройки по умолчанию для всех решений, просто переключите выпадающий список Applies to обратно на ваш профиль пользователя.
Такое разделение позволяет безопасно экспериментировать. Можно задать для одного решения особую тему оформления, проверить, удобно ли с ней работать, и, если результат не понравится, просто удалить настройку уровня решения. Visual Studio автоматически вернётся к вашим пользовательским настройкам — восстанавливать их вручную не придется.
Если хочется большего
Задолго до появления этой функции ту же задачу решало расширение Solution Colors, но с иным подходом. Оно работает только с цветами и обеспечивает более тонкую настройку: вместо смены всей темы оно окрашивает отдельные элементы IDE в цвета, специфичные для конкретного решения. При этом цвет может подбираться автоматически, так что вам даже не придётся ничего решать самостоятельно. Если вам нужен более тонкий контроль над тем, какие именно элементы меняют цвет, это расширение по-прежнему доступно и заслуживает внимания.
Источник: https://devblogs.microsoft.com/visualstudio/stop-alt-tabbing-into-the-wrong-visual-studio/ | 1 216 |
| 9 | День 2779. #ЧтоНовенького #NET11
Потоковая Передача JSON в .NET 11. Окончание
Начало
JSON-строки: один объект на строку
JSON-строки (или NDJSON - Newline Delimited JSON) — формат, который уже используется большинством инструментов потоковой обработки данных — одно значение JSON на строку, разделённые символом \n:
{"Id":1,"Total":42.0}
{"Id":2,"Total":19.5}
{"Id":3,"Total":88.25}
Каждая строка представляет собой полный, независимый JSON-документ. Потребитель читает строку, разбирает её, обрабатывает и забывает о ней. Если соединение обрывается после второй строки, первые две строки остаются действительными и пригодными для использования. Вы можете добавить четвёртую строку в файл, не затрагивая первые три. Конвейеры обработки, логи, загрузчики данных, потоки событий используют этот формат.
В .NET 11 System.Text.Json может создавать его напрямую. Новые перегрузки JsonSerializer.SerializeAsyncEnumerable принимают флаг topLevelValues:
using System.Text;
using System.Text.Json;
static async IAsyncEnumerable<Reading> GetReadings()
{
yield return new("sensor-1", 21.5);
yield return new("sensor-2", 22.0);
}
await using var stream = new MemoryStream();
await JsonSerializer.SerializeAsyncEnumerable(
stream,
GetReadings(),
topLevelValues: true);
Console.WriteLine(
Encoding.UTF8.GetString(stream.ToArray()));
// {"Id":"sensor-1","Value":21.5}
// {"Id":"sensor-2","Value":22}
public sealed record Reading(string Id, double Value);
При использовании topLevelValues: true отсутствуют открывающая и закрывающая квадратные скобки и запятые между элементами. Каждый элемент сериализуется и сопровождается переводом строки. Формат также игнорирует WriteIndented, поэтому каждый объект остается на отдельной строке.
Потоковая передача NDJSON из конечной точки
В ASP.NET Core результат по умолчанию сериализуется в JSON-массив, поэтому для отправки JSON-строк нужно самостоятельно записывать данные в поток ответа:
app.MapGet("/orders/export",
(OrderService service,
HttpResponse response,
CancellationToken ct) =>
{
response.ContentType = "application/x-ndjson";
return JsonSerializer.SerializeAsyncEnumerable(
response.Body,
service.GetAllAsyncStream(ct),
topLevelValues: true);
});
SerializeAsyncEnumerable возвращает Task, поэтому конечная точка просто возвращает его. Заказы поступают из БД через сериализатор по одному. Память остаётся неизменной независимо от того, экспортируется 100 строк или 10 миллионов. Используйте тип содержимого application/x-ndjson (или application/jsonl), чтобы клиенты знали, что они получают, вместо того чтобы предполагать наличие единого массива.
Чтение NDJSON
Чтение работает в любой версии .NET, т.к. строка представляет собой обычный JSON:
using var reader = new StreamReader(stream);
string? line;
while ((line = await reader.ReadLineAsync()) is not null)
{
if (line.Length == 0) continue;
var reading =
JsonSerializer.Deserialize<Reading>(line)!;
await ProcessAsync(reading);
}
Вы обрабатываете каждую запись по мере её поступления и не создаёте большую коллекцию.
Когда использовать?
Когда данные большие или неопределённого размера: экспорт большой таблицы, возврат длинного отчёта, подача данных в конвейер обработки или запись лога или файла событий с возможностью добавления. Формат особенно эффективен, когда потребитель обрабатывает записи по одной и когда разорванное соединение должно оставлять после себя действительные частичные данные.
Небольшие ответы прекрасно поместятся в память, а обычный JSON-массив браузеры и большинство HTTP-клиентов ожидают по умолчанию. Переход на JSON-строки в этом случае только усложнит обработку ответа без каких-либо преимуществ.
FAQ
1. В чем разница между JSON-строками и NDJSON?
Это один и тот же формат. "NDJSON" (Newline Delimited JSON) и "JSON Lines" (JSONL) — два его названия, а application/x-ndjson — это тип содержимого, который вы будете встречать чаще всего.
2. Загружает ли SerializeAsyncEnumerable всю коллекцию в память?
Нет. Он перебирает элементы IAsyncEnumerable<T> по одному, сериализует каждый и записывает его в поток вывода, прежде чем перейти к следующему. Это обеспечивает стабильность использованной памяти независимо от количества передаваемых элементов.
3. Нужен ли NDJSON для потоковой передачи, или достаточно IAsyncEnumerable?
Возвращение IAsyncEnumerable<T> уже обеспечивает потоковую передачу JSON-массива без буферизации, поэтому управление памятью происходит в любом случае. JSON-строки добавляют преимущества формата: каждая запись является независимой, частичный вывод при разрыве соединения всё равно валиден, и можно дописывать данные в файл.
4. Могут ли браузеры читать ответ NDJSON?
Автоматически – нет. Если используется await res.json() — он ожидает один JSON-документ. Браузер должен читать поток ответа и разделять его по символам новой строки, самостоятельно разбирая каждую строку. Для стандартного запроса данных из браузера обычный массив проще; JSON-строки следует использовать для конвейеров обработки и экспорта больших объёмов данных.
Источник: https://thecodeman.net/posts/streaming-json-in-dotnet-with-json-lines | 1 160 |
| 10 | День 2778. #ЗаметкиНаПолях
Потоковая Передача JSON в .NET. Начало
Допустим, у вас есть конечная точка, которая возвращает список: все заказы за последний квартал или поток показаний датчиков. Код выглядит нормально:
app.MapGet("/orders/export",
async (OrderService service) =>
{
List<Order> orders = await service.GetAllAsync();
return Results.Ok(orders);
});
GetAllAsync извлекает все строки в List<Order>. Затем Results.Ok передаёт весь список сериализатору, который формирует JSON-массив в памяти. Т.е. вы храните две копии данных одновременно — объекты и их сериализованную форму, — а клиент ждёт, пока не будет сериализована последняя строка, прежде чем что-либо получить.
Для пары сотен строк это не заметно. При паре десятков тысяч происходит скачок потребления памяти, начинает работать сборщик мусора, и время до получения первого байта растёт, как и нагрузка на процесс при увеличении количества одновременных запросов. Решение в том, чтобы прекратить буферизацию. Сериализуем элемент, отдаём, переходим к следующему.
System.Text.Json может передавать IAsyncEnumerable<T> потоком начиная с .NET 6. Платформа выдаёт JSON-массив потоком, а не буферизирует его:
// возвращает IAsyncEnumerable<Order>
app.MapGet("/orders/export", (OrderService service) =>
service.GetAllAsyncStream());
…
public async IAsyncEnumerable<Order>
GetAllAsyncStream(
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var order in _repo.ReadAllAsync(ct))
yield return order;
}
Это решает проблему использования памяти. Сервер хранит один заказ за раз, а не весь список. Вывод – JSON-массив:
[{"Id":1,"Total":42.0},{"Id":2,"Total":19.5}, …]
Это вполне приемлемо для браузера, вызывающего функцию fetch и использующего await res.json(). Но есть недостаток для больших объёмов данных: массив представляет собой один JSON-документ. Потребитель, желающий обрабатывать элементы по мере их поступления, должен разбирать массив постепенно, и, если соединение обрывается на полпути, остается усечённый, некорректный JSON-документ — завершающий символ ] не доходит. Добавить к нему данные тоже невозможно. Для потоковой обработки, записи в лог или экспорта данных JSON-массив имеет неправильный формат.
Окончание следует…
Источник: https://thecodeman.net/posts/streaming-json-in-dotnet-with-json-lines | 1 218 |
| 11 | День 2777. #ЗаметкиНаПолях
Конечные Точки в ASP.NET Core не Имеют Таймаута
ASP.NET Core по умолчанию не применяет таймаут приложения к входящим запросам. Встроенное промежуточное ПО для таймаута запроса добавляет дедлайн, но вызывает только HttpContext.RequestAborted. Конечная точка должна передать этот токен в операцию, которую вы хотите остановить.
Медленный запрос к БД или зависший вызов API могут продолжать потреблять ресурсы даже после того, как ответ перестанет быть полезным. В .NET 8 было введено промежуточное ПО для таймаута запроса, чтобы обеспечить кооперативный дедлайн обработки.
Зарегистрируем промежуточное ПО и установим тайм-аут в Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRequestTimeouts();
var app = builder.Build();
app.UseRequestTimeouts();
app.MapGet("/reports", async (
CancellationToken ct) =>
{
await Task.Delay(TimeSpan.FromSeconds(10), ct);
return Results.Ok("Ready");
})
.WithRequestTimeout(TimeSpan.FromSeconds(3));
app.Run();
- AddRequestTimeouts только регистрирует необходимые сервисы, но не устанавливает ограничение.
- WithRequestTimeout задаёт для конечной точки 3-секундный таймаут. Минимальные API привязывают параметр CancellationToken к HttpContext.RequestAborted.
- Через 3 секунды Task.Delay обнаруживает отмену и генерирует исключение. Если это исключение достигает промежуточного ПО до начала ответа, ответ по умолчанию — пустой 504 Gateway Timeout.
* Тестируйте это без отладчика, т.к. при подключенном отладчике таймаут не срабатывает.
Токен должен достичь отменяемой работы
Промежуточное ПО таймаута не прерывает поток и не вызывает HttpContext.Abort(). Оно отменяет токен и продолжает ждать конечную точку.
Удалите ct из вызова Task.Delay выше, и обработчик будет ждать полные 10 секунд, прежде чем вернуть 200 OK. Ошибка 504 не возникает, т.к. исключение отмены не достигает промежуточного ПО.
То же правило применяется и к реальным зависимостям. Передавайте токен через сервисы приложения в EF Core:
public Task<Order?> GetByIdAsync(
Guid id,
CancellationToken ct)
{
return dbContext.Orders
.AsNoTracking()
.SingleOrDefaultAsync(
order => order.Id == id,
ct);
}
EF Core пересылает токен провайдеру БД, который решает, можно ли отменить операцию в БД. Токен должен пройти по всей цепочке вызовов, прежде чем провайдер сможет его увидеть (см. также: ошибки при передаче токена отмены). Передавайте токен в HttpClient, клиенты обмена сообщениями и другие асинхронные операции, где безопасно отказаться от операции.
Выберите таймауты для каждой конечной точки
Не все конечные точки должны иметь одинаковый лимит. Небольшое чтение из API и экспорт отчёта имеют разные дедлайны, поэтому задайте для них разные политики:
builder.Services.AddRequestTimeouts(opts =>
{
opts.AddPolicy("short", TimeSpan.FromSeconds(3));
opts.AddPolicy("long", TimeSpan.FromSeconds(30));
});
app.MapGet("/orders/{id:guid}", GetOrder)
.WithRequestTimeout("short");
app.MapGet("/reports/{id:guid}", ExportReport)
.WithRequestTimeout("long");
app.MapGet("/events", StreamEvents)
.DisableRequestTimeout();
Для событий Server-Sent, Web-сокетов, long pooling и больших загрузок обычно требуется более длительная политика таймаутов или метод .DisableRequestTimeout().
Итого
Ошибка 504 от промежуточного ПО таймаута означает, что отмена достигла его. Это не значит, что все нижележащие операции были остановлены. Передавайте токен отмены во все нижележащие операции. Используйте логи или трассировку, чтобы убедиться, что зависимости обработали отмену.
Источник: https://milanjovanovic.tech/blog/your-aspnetcore-endpoints-dont-have-a-timeout | 1 195 |
| 12 | День 2776. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
45. Проверка работоспособности и мониторинг
«Как бы вы реализовали проверку работоспособности и мониторинг .NET-сервиса? Опишите инструменты и методы, которые вы бы использовали для обеспечения надёжного управления работоспособностью приложения».
Хороший ответ
Реализация проверки работоспособности и мониторинга включает в себя настройку конечных точек, к которым могут обращаться балансировщики нагрузки или инструменты мониторинга для проверки состояния приложения.
Можно добавить и настроить проверки работоспособности, используя встроенные функции ASP.NET Core:
var builder = WebApplication.CreateBuilder(args);
// Добавляем проверки
builder.Services.AddHealthChecks()
.AddCheck("Sample Health Check", () =>
HealthCheckResult.Healthy("OK"));
var app = builder.Build();
// Конечная точка
app.MapHealthChecks("/health");
app.Run();
Этот фрагмент кода настраивает простую проверку работоспособности, которая всегда возвращает «Healthy». Можно заменить логику проверки реальными тестами, вроде проверки подключения к БД или доступности внешних зависимостей.
Для более сложных сценариев можно добавить проверки для конкретных сервисов, таких как базы данных, серверы кэширования или API, от которых зависит приложение, например, вот проверка доступности базы:
builder.Services.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>();
Необходимо убедиться, что конечные точки проверки работоспособности хорошо документированы и доступны для соответствующих инструментов мониторинга, но защищены от несанкционированного доступа.
Важно добавить логирование проверок работоспособности для регистрации сбоев или изменений в поведении системы. Это может помочь в диагностике проблем, приводящих к сбоям.
Преимущества
- Проактивный мониторинг: позволяет команде обнаруживать проблемы и реагировать на них до того, как они повлияют на пользователей.
- Наблюдаемость: обеспечивает прозрачность состояния приложения и помогает поддерживать его надёжность и производительность.
- Переключение при сбоях и высокая доступность: обеспечивает автоматическое переключение при сбоях проверок работоспособности в облачных средах.
Часто встречающийся плохой ответ
Важно правильно логировать сообщения об ошибках и убедиться, что приложение автоматически перезапускается в случае сбоя. Этого должно быть достаточно для поддержания его работы.
Почему это неправильно:
- Отсутствие проактивного мониторинга: полагаться исключительно на журналы ошибок и автоматические перезапуски не предотвращает сбои, а лишь реагирует после их возникновения, что может привести к простоям.
- Игнорирование преимуществ проверок работоспособности, которые могут отслеживать состояние приложения в режиме реального времени и предоставлять ранние предупреждения о потенциальных проблемах.
- Неадекватные стратегии отказоустойчивости: автоматические перезапуски могут не устранять основные проблемы и приводить к повторным сбоям без надлежащей диагностики или решения.
Эта ошибка часто возникает из-за непонимания возможностей и важности проверок работоспособности и мониторинга в современных архитектурах приложений, возможно, из-за недостатка опыта работы в средах, где высокая доступность и надёжность имеют решающее значение.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md | 1 273 |
| 13 | День 2775. #Оффтоп
Сортировка I-Can’t-Believe-It-Can-Sort
Думаю, что с каждым чуть ли не ежедневно случается ситуация, когда вы написали код и думаете, что он работает, а он не работает. А бывало ли у вас наоборот?
Однажды один профессор на лекции про алгоритмы сортировки написал наивный и очевидно неверный алгоритм. Два вложенных цикла, внутри которых одно условие и смена элементов местами, если условие выполняется (некий неверный вариант пузырьковой сортировки):
for i = 0 to length - 1:
for j = 0 to length - 1:
if A[i] < A[j]:
swap(A[i], A[j])
Полная бессмыслица, которая, кажется, должна либо не сделать ничего, либо просто перемешать элементы. Однако позже выяснилось, что алгоритм действительно правильно сортирует элементы. Доказательство вот тут. Этот алгоритм стал популярным примером для изучения с помощью инструментов формальной верификации программ, позволяющих доказать, что подобная контринтуитивная структура действительно работает. В итоге алгоритм назвали сортировкой «Не-Могу-Поверить-Что-Это-Сортирует» (I-Can’t-Believe-It-Can-Sort).
Про него и про другие алгоритмы сортировки в новом видео Мэта Паркера.
А если вы хотите подробно изучить все алгоритмы сортировки, можно на пару часиков залипнуть вот сюда. | 1 338 |
| 14 | ХОЧЕШЬ ПОВЫСИТЬ ГРЕЙД В 2026 ГОДУ? 🚀
Чтобы стать Senior C# разработчиком сегодня, нужно не только знать язык программирования и фреймворки. Нужно уметь строить гибкую архитектуру приложения, которую легко тестировать и менять под задачи бизнеса. Стань экспертом в построении гибкой архитектуры приложения!
👉 Стартуем 7 сентября.
Курс ведет действующий архитектор и Principal Engineer Кирилл Ветчинкин.
Ты научишься:
✅ Разбивать приложение на слои в соответствии с Clean Architecture
✅ Формировать Domain Model и применять тактические паттерны DDD
✅ Реализовывать Use Case как Command/Query
✅ Делать синхронные и асинхронные интеграции, не загрязняя ядро приложения
✅ Писать 3 вида тестов для разных слоев приложения
Полная программа ТУТ 👉 https://microarch.ru/courses/ddd/languages/csharp?utm_source=posev&utm_medium=erid:2Vtzqw4xuPH&utm_campaign=1
А главное — ты с нуля разработаешь и запустишь микросервис, который максимально приближен к реальности "Диспетчеризация заказов на курьеров". Это будет крутым проектом в портфолио или основой для рабочих задач.
А еще:
✅ Проверим все домашки
✅ Поддержим в чате
✅ Проведем живые разборы
✅ Ответим на все вопросы
📕 Сертификат об участии по итогам прохождения курса.
🔥 Не откладывай свой рост на потом: https://microarch.ru/courses/ddd/languages/csharp?utm_source=posev&utm_medium=erid:2Vtzqw4xuPH&utm_campaign=1
Реклама. ИП Ветчинкин К.Е. ИНН: 773376451099 Erid: 2Vtzqw4xuPH | 1 251 |
| 15 | День 2774. #Книги
«Предметно-ориентированное проектирование. Модернизация легаси-систем и снижение рисков с помощью DDD.» (Швентнер Х., Лилиенталь К. — Астана: «Спринт Бук», 2027).
3 года назад мы в нашей компании завершили многолетнее обновление нашей легаси системы, переписав всё на .NET (правда, .NET Framework, но это другая история и тому были причины). И, читая эту книгу, я дико сожалел, что у меня не было её под рукой, когда мы это делали. Уверен, что процесс прошёл бы гораздо быстрее, эффективнее, и привёл бы к более качественным результатам.
Эта книга только отчасти про DDD. Знаю, многие его не любят, отчасти потому, что для полного понимания его сути и всех преимуществ, надо довольно глубоко погрузиться. Уже писал про книгу Эванса и излишнюю академичность её русскоязычного перевода, поэтому не удивляюсь, что у нас она не получила большого распространения. Ведь мы, разработчики, живём по принципу «show me the code и побыстрее», а там 300+ страниц мелким шрифтом и довольно заумным языком.
Сразу говорю, здесь всё не так! Перевод гораздо более простым языком, а примеры гораздо понятнее. Хотя, эта книга вряд ли подойдёт или принесёт много пользы разработчикам до уровня сеньора или тем, кто просто хочет писать код. А вот всем опытным разработчикам, кто хочет повысить свой уровень разработки и моделирования систем, а может и попробовать себя в роли архитектора, эта книга просто обязательна к прочтению. Авторы – между прочим, практикующие консультанты по модернизации и проектированию информационных систем – собрали весь практический опыт сбора требований, моделирования, разработки, организации команд и последних новинок DDD, и относительно кратно, но доступно, изложили его в этой книге.
Как правильно собрать требования и описания бизнес-процессов, абстрагируясь от существующей системы и «теневого ИТ»? Чем отличаются Domain Storytelling, Event Storming и Scenario Casting, и когда какой метод использовать? Как создать понятную всем документацию и диаграммы бизнес-процессов, что поможет любому быстро разобраться в системе на любом уровне детализации? Как из этих описаний смоделировать информационную систему и правильно выделить ограниченные контексты, сохраняя их связность и слабую связанность между ними? Как сравнить эту модель с существующей системой и понять, как её модернизировать? Как наиболее эффективно организовать команды разработки? Как разобрать процесс модернизации на стратегические и тактические этапы и распределить задачи между командами? Как правильно рефакторить сильно связанный код? Как избавиться от анемичных объектов, максимально приблизить их к бизнес-процессам и снизить вероятность ошибок? В общем, как разобрать «большой комок грязи» и превратить его в систему, с которой легко работать.
Об этом и о многом другом на понятных примерах в этой книге. | 1 376 |
| 16 | День 2773. #BestPractices
Лучшие Практики Безопасности REST API в ASP.NET Core. Окончание
1-3
4-7
8-12
13. Настройка заголовков безопасности
Заголовки, такие как Content-Security-Policy, X-Content-Type-Options и Referrer-Policy, указывают браузеру, как обрабатывать ваши ответы — какие скрипты могут выполняться, следует ли угадывать тип контента, какой объём информации об источнике следует раскрывать. Добавьте их с помощью небольшого промежуточного ПО, которое запускается при каждом ответе:
app.Use(async (context, next) =>
{
var hdrs = context.Response.Headers;
hdrs["X-Content-Type-Options"] = "nosniff";
hdrs["Referrer-Policy"] = "no-referrer";
hdrs["Content-Security-Policy"] = "default-src 'self'";
hdrs["X-Frame-Options"] = "DENY";
await next();
});
- X-Content-Type-Options: nosniff предотвращает попытки браузера угадывать (и неправильно обрабатывать) тип контента;
- Content-Security-Policy ограничивает источники загрузки скриптов, стилей и других ресурсов;
- Referrer-Policy контролирует, какая часть вашего URL-адреса отправляется на другие сайты;
- X-Frame-Options: DENY предотвращает встраивание ваших ответов во фрейм (кликджекинг).
Для чистого JSON API эти параметры больше имеют смысл при отображении ответов в браузере, но они являются недорогой страховкой для любого API.
Примечание: в продакшене рекомендуется использовать поддерживаемую библиотеку или обратный прокси для управления этими заголовками и более строгую Content-Security-Policy, адаптированную под ваше приложение.
14. Безопасное хранение секретов
Ключи, строки подключения и токены никогда не должны храниться в системе контроля версий. Секрет, добавленный в файл appsettings.json, является утечкой — он навсегда остаётся в истории Git и виден всем, кто имеет доступ к репозиторию. Кроме того, ИИ-агенты могут напрямую считывать ваши секреты из конфигурации, если вы не запретите им доступ.
В процессе разработки используйте менеджер секретов .NET. В производственной среде используйте переменные среды или управляемое хранилище секретов, например Azure Key Vault:
builder.Configuration.AddAzureKeyVault(
new Uri("https://myapi.vault.azure.net/"),
new DefaultAzureCredential());
var connectionString = builder
.Configuration
.GetConnectionString("Myapi");
Ваш код затем считывает конфигурацию одинаково, независимо от того, откуда взялось значение. Приложению не важен источник — важно лишь то, чтобы значение не находилось в файле репозитория.
15. Защита от CSRF, где это необходимо
Защита от межсайтовой подделки запросов (CSRF) важна, когда вы аутентифицируете с помощью cookie. CSRF обманывает браузер авторизованного пользователя, заставляя его отправлять нежелательный запрос, используя cookie, который браузер автоматически добавляет. Если ваш API аутентифицирует с помощью cookie, вам необходимы токены защиты от подделки запросов:
builder.Services.AddAntiforgery(opts =>
{
opts.HeaderName = "X-CSRF-TOKEN";
});
var app = builder.Build();
app.UseAntiforgery();
Затем требуйте валидный токен в конечных точках, изменяющих состояние:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(CreateOrderRequest request)
{ /* … */ }
Замечание: API, основанные исключительно на токенах, в значительной степени защищены от CSRF-атак. Если ваш клиент отправляет JWT в заголовке Authorization (а не cookie), браузер не добавляет его автоматически, поэтому поддельный межсайтовый запрос не содержит учётных данных. Защита от подделки запросов в основном необходима для конечных точек, аутентифицируемых с помощью cookie — приложений, отрисовываемых сервером, и API, использующих аутентификацию на основе cookie. Согласуйте защиту с вашей моделью аутентификации: cookie нуждаются в защите от CSRF, а bearer-токены, как правило, нет.
16. Версионирование и удаление устаревших небезопасных конечных точек
Версионирование позволяет отказаться от неудачного дизайна, не нарушая работу клиентов. Когда конечная точка оказывается небезопасной или плохо структурированной, нужен способ ввести исправленную версию и поэтапно вывести из эксплуатации старую.
Добавьте пакет Asp.Versioning.Mvc и отметьте старую версию как устаревшую:
builder.Services.AddApiVersioning(opts =>
{
opts.DefaultApiVersion = new ApiVersion(2, 0);
opts.ReportApiVersions = true;
});
…
[ApiController]
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/orders")]
public class OrdersController : ControllerBase
{
…
}
ReportApiVersions добавляет заголовки api-supported-versions и api-deprecated-versions к вашим ответам, чтобы клиенты видели предстоящее устаревание и могли перейти на новую версию до того, как вы удалите v1. Это превращает ситуацию «мы не можем это исправить, потому что клиенты зависят от этого» в управляемую миграцию.
17. Журналирование и аудит событий безопасности
События безопасности — неудачные попытки входа в систему, отказы в авторизации, подозрительные шаблоны запросов — необходимо логировать с достаточным контекстом для восстановления произошедшего. Записывайте их в виде структурированных журналов:
[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest request)
{
var result = await _authService.AuthenticateAsync(request);
if (!result.Succeeded)
{
_logger.LogWarning(
"Неудачный вход для {Email}, IP: {IpAddress}",
request.Email, HttpContext.Connection.RemoteIpAddress);
return Unauthorized();
}
return Ok(result.Token);
}
Регистрируйте сбои аутентификации, отказы в авторизации и всё, что выглядит как попытка взлома. Записывайте достаточно контекста для проведения настоящего криминалистического расследования, что может включать конфиденциальные данные, если этого требует ваша модель угроз: учётная запись, исходный IP и предпринятое действие.
Т.к. эти журналы могут содержать конфиденциальные данные, относитесь к ним как к конфиденциальным: храните их там, где их могут читать только авторизованные лица, и применяйте правила хранения и контроля доступа.
18. Поддерживайте актуальность зависимостей
Ваш API зависит от десятков NuGet-пакетов и среды выполнения .NET, и каждый из них является потенциальной точкой взлома, когда обнаруживается уязвимость. Поддержание актуальности — одна из самых важных вещей. .NET может предоставить вам список уязвимых пакетов:
dotnet list package --vulnerable --include-transitive
dotnet list package --outdated
Выполняйте это регулярно и привяжите сканирование в ваш конвейер CI, чтобы обнаружение уязвимости зависимости роняло сборку.
Обновляйте NuGet-пакеты и среду выполнения .NET по расписанию, а не только при возникновении проблем. В сочетании с такими инструментами, как Dependabot или оповещениями безопасности GitHub, это устраняет уязвимость, на которую чаще всего полагаются злоумышленники.
Источник: https://antondevtips.com/blog/rest-api-security-best-practices-in-aspnetcore | 1 252 |
| 17 | День 2772. #BestPractices
Лучшие Практики Безопасности REST API в ASP.NET Core. Продолжение
1-3
4-7
8. Типы контента и размер запроса
Конечная точка, принимающая JSON, должна отклонять другие типы контента, и ни одна конечная точка не должна принимать неограниченное тело запроса. Загрузка нескольких гигабайтных файлов — простой способ исчерпать память сервера и вывести из строя ваш API. Ограничьте тип контента с помощью [Consumes] и размер тела запроса с помощью [RequestSizeLimit]:
[HttpPost]
[Consumes("application/json")]
[RequestSizeLimit(1_000_000)] // 1 MB
public IActionResult Create(CreateProductRequest request)
{
// …
}
Также можно задать лимит глобально в Kestrel:
builder.WebHost.ConfigureKestrel(opts =>
{
opts.Limits.MaxRequestBodySize = 1_000_000;
});
То же относится и к объёму данных, которые клиент может получить. Всегда используйте пагинацию с ограничением максимального размера страницы в конечных точках коллекций, чтобы клиент не мог запросить неограниченный набор результатов:
[HttpGet]
public IActionResult GetProducts(
int page = 1, int pageSize = 20)
{
// Принудительно ограничиваем размер страницы
pageSize = Math.Min(pageSize, 100);
// …
}
9. Параметризованные запросы и EF Core
Никогда не создавайте SQL-запросы путем конкатенации строк из пользовательского ввода — это способ внедрения SQL-инъекций. Параметризованные запросы сохраняют пользовательский ввод как данные, а не как исполняемый SQL. EF Core параметризует всё по умолчанию, поэтому LINQ-запрос всегда безопасен:
// EF Core параметризует 'search'
var products = await _dbContext.Products
.Where(p => p.Name.Contains(search))
.ToListAsync();
Когда вам нужен чистый SQL, используйте FromSql или FromSqlInterpolated, которые преобразуют интерполированные значения в параметры, а не в литеральный текст:
// 'category' становится параметром SQL
var products = await _dbContext.Products
.FromSqlInterpolated(
$"SELECT * FROM Products WHERE Category = {category}")
.ToListAsync();
// Никогда не делайте так
var sql = "SELECT * FROM Products WHERE Category = '" + category + "'";
// Не используйте FromSqlRaw с интерполяцией
var products = await _dbContext.Products
.FromSqlRaw(
$"SELECT * FROM Products WHERE Category = {category}")
.ToListAsync();
10. Лимитирование трафика
Без ограничений один клиент — или злоумышленник — может атаковать точку входа методом перебора паролей или завалить API запросами до тех пор, пока он не выйдет из строя. ASP.NET Core имеет встроенное лимитирование запросов, которое настраивается в Program.cs:
builder.Services.AddRateLimiter(opts =>
{
opts.AddFixedWindowLimiter("api", lim =>
{
lim.PermitLimit = 10;
lim.Window = TimeSpan.FromMinutes(1);
lim.QueueLimit = 0;
});
opts.RejectionStatusCode =
StatusCodes.Status429TooManyRequests;
});
var app = builder.Build();
app.UseRateLimiter();
Затем можно применить политику к методу действия контроллера:
[HttpPost("login")]
[EnableRateLimiting("api")]
public IActionResult Login(LoginRequest request)
{ /* … */ }
или конечной точке минимальных API:
app.MapPost("/login", (LoginRequest request) =>
{ /* … */ })
.RequireRateLimiting("api");
Когда клиент превышает лимит, он получает ошибку 429 Too Many Requests. Это снижает вероятность атак методом перебора паролей и атак типа «отказ в обслуживании».
11. CORS
CORS (Cross-Origin Resource Sharing) контролирует, какие веб-источники могут отправлять запросы к вашему API. Опасно использовать AllowAnyOrigin(), что позволяет любому веб-сайту в интернете вызывать ваш API от имени авторизованного пользователя. Определите именованную политику, которая точно перечисляет разрешённые источники, методы и заголовки:
builder.Services.AddCors(opts =>
{
opts.AddPolicy("Web", p =>
p.WithOrigins("https://mywebsite.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Authorization", "Content-Type"));
});
var app = builder.Build();
app.UseCors("Web");
Так только https://mywebsite.com может вызвать ваш API, только с этими методами и с такими заголовками. Всё остальное отклоняется. Оставьте AllowAnyOrigin для действительно общедоступных, неаутентифицированных API — и никогда не используйте его в сочетании с учётными данными.
12. Минимальные сведения об ошибке
Ответ об ошибке должен помогать вызывающей стороне, а не злоумышленнику. Обычная страница исключения раскрывает трассировку стека, версии фреймворков, пути к файлам и SQL-запросы — карту ваших внутренних механизмов. Вместо этого возвращайте чистое, стандартное сообщение об ошибке, используя сведения о проблеме:
builder.Services.AddProblemDetails();
var app = builder.Build();
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();
else
app.UseExceptionHandler();
В производственной среде необработанное исключение возвращает структурированное тело ProblemDetails с кодом состояния и стандартным сообщением — и ничего о ваших внутренних процессах.
Окончание следует…
Источник: https://antondevtips.com/blog/rest-api-security-best-practices-in-aspnetcore | 1 306 |
| 18 | День 2771. #BestPractices
Лучшие Практики Безопасности REST API в ASP.NET Core. Продолжение
Начало
4. Авторизация с политиками
Аутентификация определяет, кто является пользователем. Авторизация определяет, что ему разрешено делать. Простая проверка через [Authorize] проверяет только то, что пользователь авторизован. Для реальных правил — «только менеджеры могут удалять заказы», «пользователи могут видеть только свои данные» — необходима авторизация на основе политик и ресурсов. Определите политику:
builder.Services.AddAuthorization(opts =>
{
opts.AddPolicy("orders:write", p =>
p.RequireRole("Manager")
.RequireClaim("permission", "orders:write"));
});
Примените её к методу действия контроллера:
public class OrdersController : ControllerBase
{
// …
[HttpDelete("{id:int}")]
[Authorize(Policy = "orders:write")]
public IActionResult Delete(int id)
{
_orderService.Delete(id);
return NoContent();
}
}
Или к конечной точке минимальных API:
app.MapDelete("/api/orders/{id:int}",
(int id, IOrderService orders) =>
{
orders.Delete(id);
return Results.NoContent();
})
.RequireAuthorization("orders:write");
Когда правило зависит от конкретного ресурса — например, «только владелец может редактировать этот заказ» — используйте авторизацию на основе ресурсов с помощью IAuthorizationService.AuthorizeAsync(user, order, "OrderOwner"), которая оценивает политику на основе фактической сущности. Политики позволяют хранить логику авторизации в одном месте, вне тела конечных точек.
5. Принцип наименьших привилегий
Предоставьте каждому клиенту минимальный необходимый доступ и ничего больше. Токен, набор утверждений или ключ API должны предоставлять только те разрешения, которые необходимы для выполнения его задачи. Мобильное приложение, которое только читает каталог товаров, не должно иметь токен, позволяющий удалять заказы. Ограничьте это на уровне токена. Когда ваш поставщик идентификации выдаёт токен, он должен включать только те области действия, которые были предоставлены клиенту, а ваши политики - проверять их наличие:
builder.Services.AddAuthorization(opts =>
{
opts.AddPolicy("catalog:read", p =>
p.RequireClaim("scope", "catalog:read"));
opts.AddPolicy("orders:write", p =>
p.RequireClaim("scope", "orders:write"));
});
Тот же принцип применим к ключам API и аккаунтам баз данных — ограничьте их область действия. Если учётные данные утекут, принцип наименьших привилегий ограничит радиус атаки только тем, что могут сделать эти аккаунты.
6. Проверка и очистка входных данных
Некорректные или вредоносные входные данные должны отклоняться на границе, до того, как они достигнут бизнес-логики или базы данных. ASP.NET Core помогает в этом: атрибут [ApiController] автоматически возвращает ошибку 400 с подробным описанием проблемы на запрос, не прошедший валидацию модели. Добавьте аннотации данных или, для более сложных правил, FluentValidation:
public class CreateProductRequest
{
[Required]
[StringLength(200, MinimumLength = 1)]
public string Name { get; set; } = string.Empty;
[Range(0.01, 1_000_000)]
public decimal Price { get; set; }
}
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpPost]
public IActionResult
Create(CreateProductRequest req)
{
// Если мы здесь, модель прошла валидацию
// …
}
}
В минимальных API проверка выполняется с помощью фильтра или явно:
app.MapPost("/api/products",
(CreateProductRequest req,
IValidator<CreateProductRequest> validator) =>
{
var result = validator.Validate(request);
if (!result.IsValid)
return Results
.ValidationProblem(result.ToDictionary());
// …
});
Ранняя проверка предотвращает целый класс атак — слишком длинные строки, числа, выходящие за пределы допустимого диапазона, отсутствующие поля и т.п.
7. Овер-постинг
Никогда не привязывайте входящие JSON-данные напрямую к сущности вашей БД. Так клиенты смогут устанавливать поля, которые они никогда не должны контролировать — IsAdmin, Balance, Status или ID другого пользователя. Это называется овер-постингом или массовым присваиванием.
Используйте отдельные DTO запроса, который отображает только те поля, которые клиенту разрешено устанавливать, а затем самостоятельно сопоставляйте его с сущностью:
// DTO запроса – то, что клиент может изменять
public record UpdateProductRequest(string Name, decimal Price);
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(
int id, UpdateProductRequest request)
{
var prod = await _dbContext.Products.FindAsync(id);
if (prod is null)
return NotFound();
// Задаём только разрешённые поля
prod.Name = request.Name;
prod.Price = request.Price;
await _dbContext.SaveChangesAsync();
return NoContent();
}
Сущность Product также может содержать поля CreatedAt, OwnerId или IsFeatured, но, т.к. клиент может отправлять только Name и Price, эти поля недоступны извне. Отдельные DTO запросов требуют небольшого количества дополнительного кода, но закрывают целую категорию ошибок, приводящих к повышению привилегий.
Продолжение следует…
Источник: https://antondevtips.com/blog/rest-api-security-best-practices-in-aspnetcore | 1 333 |
| 19 | День 2770. #BestPractices
Лучшие Практики Безопасности REST API в ASP.NET Core. Начало
Неважно, насколько чиста ваша архитектура или насколько быстро выполняются запросы — если злоумышленник может прочитать заказы другого пользователя или подделать токен, всё это не имеет значения. Большинство проблем с безопасностью API возникают из-за игнорирования основ:
- устаревший NuGet-пакет с уязвимостью,
- отсутствие валидации,
- слабая настройка аутентификации,
- слишком либеральная политика CORS,
- секрет, внесённый в систему контроля версий.
Хорошая новость в том, что ASP.NET Core из коробки предоставляет почти всё необходимое для устранения проблем.
1. HTTPS везде
Каждый запрос к API должен передаваться по зашифрованному соединению. Без HTTPS токены, пароли и личные данные передаются в открытом виде, и любой, кто находится на пути следования по сети, может их прочитать. Обычный HTTP также открывает двери для атак с понижением уровня безопасности, когда злоумышленник заставляет клиента использовать небезопасное соединение. ASP.NET Core предоставляет два инструмента. UseHttpsRedirection перенаправляет HTTP-запросы на HTTPS, а HSTS (HTTP Strict Transport Security) указывает браузерам подключаться только по HTTPS. Совместимые браузеры вообще отказываются взаимодействовать с вашим API по HTTP, что блокирует атаки с понижением уровня безопасности:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHsts(opts =>
{
opts.MaxAge = TimeSpan.FromDays(365);
opts.IncludeSubDomains = true;
opts.Preload = true;
});
var app = builder.Build();
if (!app.Environment.IsDevelopment())
app.UseHsts();
app.UseHttpsRedirection();
// …
HSTS пропускается в среде разработки, поскольку там часто используется http://localhost.
2. Аутентификация с помощью токенов, а не сессий
Предпочтительнее использовать аутентификацию с помощью токенов без сохранения состояния, чем серверные сессии. Сессия хранит состояние аутентификации в памяти сервера или в общем хранилище, что привязывает каждого пользователя к серверу и затрудняет горизонтальное масштабирование. Токен содержит подтверждение личности, поэтому любой экземпляр вашего API может проверить его без поиска пользователя в базе. Для большинства API подойдёт токены JWT bearer, часто выдаваемые поставщиком идентификации через OAuth 2.0 или OpenID Connect:
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opts =>
{
opts.Authority = "https://my-idp.com";
opts.Audience = "my-api";
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
Клиент отправляет токен в заголовке Authorization: Bearer <token> в каждом запросе. Ваш API проверяет его и считывает идентификационные данные пользователя из утверждений токена — никакого хранилища сессий, никаких «липких» сессий, никакого масштабируемого состояния на стороне сервера. Это делает ваш API «не сохраняющим состояние» и упрощает горизонтальное масштабирование.
См. также про референтные токены и отзыв токенов.
3. Проверка подписи JWT, издателя, аудитории и срока действия
Токен безопасен только после проверки того, кто его выпустил, для кого он предназначен, что он не истёк и что его подпись действительна. Настройте эти проверки явно через TokenValidationParameters, а не доверяйте значениям по умолчанию фреймворка:
.AddJwtBearer(opts =>
{
opts.TokenValidationParameters =
new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://my-idp.com",
ValidateAudience = true,
ValidAudience = "my-api",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(30)
};
});
Каждый флаг закрывает лазейку:
- ValidateIssuerSigningKey подтверждает подпись — никогда не отключайте этот параметр, иначе кто угодно сможет подделать токен;
- ValidateIssuer и ValidateAudience гарантируют, что токен получен от вашего поставщика идентификации и предназначен для вашего API, а не для какого-то другого сервиса;
- ValidateLifetime отклоняет просроченные токены.
- ClockSkew - существует для того, чтобы допускать небольшие расхождения во времени между серверами, но его значение по умолчанию составляет целых 5 минут, поэтому просроченный токен может приниматься ещё до 5 минут. Сокращение параметра до 30 секунд или даже до 0 ужесточает контроль за истечением срока действия токенов, при условии синхронизации часов ваших серверов (с помощью NTP).
Продолжение следует…
Источник: https://antondevtips.com/blog/rest-api-security-best-practices-in-aspnetcore | 1 440 |
| 20 | День 2769. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
44. Распределённая трассировка
«Как бы вы реализовали распределённую трассировку для мониторинга и отладки взаимодействия микросервисов? Приведите примеры инструментов и методологий, которые вы бы использовали».
Хороший ответ
Реализация распределённой трассировки включает в себя интеграцию инструментов, которые могут отслеживать ход выполнения и производительность запросов по мере их прохождения через микросервисы. Вот как можно её настроить.
1. Выбрать систему распределённой трассировки, совместимую с .NET, например, OpenTelemetry, которая стала стандартом для обеспечения наблюдаемости в облачных приложениях. Она поддерживает трассировку, метрики и журналы.
2. Добавить необходимые пакеты OpenTelemetry в проект.
3. Настроить трассировку в Program.cs, убедившись, что она захватывает как входящие, так и исходящие запросы для всестороннего отслеживания взаимодействия между микросервисами:
var builder = WebApplication.CreateBuilder(args);
// Добавляем OpenTelemetry
builder.Services.AddOpenTelemetryTracing(trc =>
{
trc.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter(opts => {
opts.Endpoint =
new Uri("http://your-tracing-backend:4317");
});
});
var app = builder.Build();
app.MapGet("/api/data", async (HttpClient client) =>
{
// Пример исходящего запроса
var response = await client.GetStringAsync(
"http://another-service/api/data");
return Results.Ok(response);
});
app.Run();
4. Убедиться, что все части приложения, включая внешние библиотеки и зависимости, настроены на отправку трассировок. Это может включать настройку дополнительных инструментов для баз данных или очередей сообщений, если они не охватываются автоматически.
5. Использовать соответствующие заголовки для распространения контекстов трассировки по HTTP-запросам для поддержки распределённой трассировки через границы. Обычно это обрабатывается библиотеками OpenTelemetry, но следует проверить полноту их работы.
6. Использовать инструменты, вроде Jaeger, Zipkin или коммерческие решения, такие как Dynatrace или DataDog, которые могут визуализировать данные трассировки и помогать в отладке и мониторинге производительности.
7. Убедиться, что бэкенд трассировки настроен на обработку объёма данных трассировки, генерируемых нашими сервисами, и может масштабироваться по мере необходимости.
Преимущества
- Улучшенная отладка: Быстрое определение, какая часть взаимодействия сервисов завершилась с ошибкой или вызывала задержки.
- Оптимизация производительности: Анализ данных трассировки для оптимизации производительности отдельных микросервисов и системы в целом.
- Улучшенная наблюдаемость: представление о поведении приложений и о том, как сервисы взаимодействуют в производственной среде.
Часто встречающийся плохой ответ
«Я бы просто логировал запросы в каждом сервисе. Затем можно сопоставить эти логи и отследить, как обрабатываются запросы в сервисах.»
Почему это неправильно
- Отсутствие масштабируемости и эффективности: Использование журналов для ручной трассировки запросов не масштабируемо и может быть крайне неэффективным, особенно в сложных или высоконагруженных средах.
- Отсутствие полноты: Журналы не предоставляют структурированной, коррелированной информации, которую предлагают специализированные инструменты трассировки. Они могут упускать важные детали взаимодействия или не содержать контекста, необходимого для эффективной отладки.
- Ресурсоёмкость: Зависимость исключительно от журналов при трассировке может привести к значительным накладным расходам как на хранение, так и на обработку, влияя на производительность системы.
Эта ошибка обычно возникает из-за недостаточного понимания специализированных инструментов и методов распределённой трассировки или из-за недооценки сложности, связанной с ручной трассировкой распределённых транзакций. Это подчёркивает необходимость интегрированных автоматизированных решений для трассировки в современных архитектурах приложений.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md | 1 452 |
