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

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 1755. #ЧтоНовенького
Упрощаем Облачную Разработку с .NET Aspire

.NET Aspire — это оркестратор для создания отказоустойчивых, наблюдаемых и настраиваемых облачных приложений в .NET. Он включает набор компонентов для обнаружения сервисов, телеметрии, поддержки устойчивости и проверки работоспособности.

Новый шаблон .NET Aspire Starter доступен в Visual Studio 2022 (17.9 Preview 1), либо из командной строки. Приложение шаблона состоит из двух проектов (веб-интерфейс Blazor и API) и кэша Redis. Также включены два новых проекта:
1. <appname>.AppHost запускает .NET-проекты, контейнеры или исполняемые файлы, необходимые для вашего распределённого приложения.
2. <appname>.ServiceDefaults содержит общую сервисную логику, которая применяется к каждому из проектов в приложении: обнаружение сервисов, телеметрия и конечные точки проверки работоспособности.

Панель мониторинга
Запуск приложения .NET Aspire отображает панель мониторинга. Она даёт единое представление о ваших сервисах с их журналами, метриками, трассировками и ошибками. На картинке 1 ниже показано изображение проекта с ошибкой, обозначенной красной точкой. Также можно смотреть журналы всех проектов и распределённую трассировку, показывающую запрос на страницу погоды (см. картинку 2 ниже). На панели собираются все диагностические данные во время разработки. Используются открытые стандарты, такие как Grafana+Prometheus, Application Insights и т. д.

Компоненты
В веб-проекте приложения есть NuGet-пакет Aspire.StackExchange.Redis.OutputCaching. Такие пакеты называются компонентами .NET Aspire. Это связующие библиотеки, которые настраивают SDK для работы в облачной среде. Каждый компонент должен:
- Предоставлять схему JSON для конфигурации.
- Использовать настраиваемые шаблоны устойчивости, такие как повторные попытки, тайм-ауты и автоматические выключатели, чтобы максимизировать доступность.
- Предоставлять проверки работоспособности.
- Предлагать интегрированное ведение журнала, метрики и трассировку с использованием современных абстракций .NET (ILogger, Meter, Activity).
- Предлагать методы расширения, которые «склеивают» сервисы из SDK в DI-контейнер с правильным временем жизни для регистрируемых типов.

То есть компоненты .NET Aspire настраивают зависимости для соблюдения ряда требований для успешной работы в облаке. Они не оборачивают и не скрывают фактический SDK/библиотеку, а действуют как связующее звено, гарантируя, что библиотека настроена с хорошим набором значений по умолчанию и правильно зарегистрирована в DI.

Код
В Program.cs веб-проекта вы можете увидеть код:
builder.Services
.AddHttpClient<WeatherApiClient>(
client => client
.BaseAddress = new("https://apiservice"));

Это настройка веб-интерфейса для вызова API погоды. Название apiservice взято из проекта AppHost:
var builder = 
DistributedApplication.CreateBuilder(args);

var cache =
builder.AddRedisContainer("cache");

var apiservice = builder
.AddProject<Projects.AspireApp_ApiService>(
"apiservice");

Builder
.AddProject<Projects.AspireApp_Web>("webfrontend")
.WithReference(cache)
.WithReference(apiservice);

builder.Build().Run();

AppHost —стартовый проект. Он запускает все проекты, их зависимости и настраивает их, позволяя им взаимодействовать. Механизм обнаружения сервисов позволяет использовать логические имена вместо IP-адресов и портов при выполнении HTTP-вызовов. Здесь API-сервис назван "apiservice", и это имя можно использовать при выполнении HTTP-вызовов через IHttpClientFactory. Аналогично кэш Redis назван "cache". Вызовы, выполненные с использованием этого метода, также будут автоматически повторяться и обрабатывать временные сбои благодаря интеграции с Polly.

Подробнее о .NET Aspire смотрите в:
- докладе с .NET Conf
- видео от Ника Чапсаса
- деплой в Azure от Ника Чапсаса

Источник: https://devblogs.microsoft.com/dotnet/introducing-dotnet-aspire-simplifying-cloud-native-development-with-dotnet-8/
👍12
День 1756. #Testing
Тестируем Валидацию Модели

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

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

Определим простую модель:
public class User
{
[Required]
[MinLength(3)]
public string FirstName { get; set; }

[Required]
[MinLength(3)]
public string LastName { get; set; }

[Range(18, 100)]
public int Age { get; set; }
}

У нас есть два варианта: мы можем написать интеграционные тесты для отправки запросов в систему, на которой работает сервер, и проверить полученный ответ. Или мы можем использовать внутренний класс Validator, который используется ASP.NET для проверки моделей ввода, и создать быстрые юнит-тесты. Вот вспомогательный метод, который мы можем использовать в тестах:
public static IList<ValidationResult> 
ValidateModel(object model)
{
var results = new List<ValidationResult>();
var context =
new ValidationContext(model, null, null);

Validator.TryValidateObject(
model, context, results, true);

if (model is IValidatableObject vm)
results.AddRange(vm.Validate(context));

return results;
}

Мы создаём контекст проверки без какой-либо внешней зависимости, ориентированный только на модель ввода. Затем мы проверяем все свойства, вызывая TryValidateObject, и сохраняем результаты проверки в списке results. Наконец, если модель реализует интерфейс IValidatableObject, который предоставляет метод Validate, мы вызываем его и добавляем возвращённые ошибки в тот же список results. Таким образом мы можем обрабатывать как атрибуты полей, такие как [Required], так и пользовательскую проверку в методе Validate() класса модели.

Используем этот метод в тестах:
[Test]
public void Pass_WhenModelValid()
{
var model = new User {
FirstName = "Jon",
LastName = "Smith",
Age = 42
};

var result = ValidateModel(model);

Assert.That(result, Is.Empty);
}

[Test]
public void Fail_WhenAgeLessThan18()
{
var model = new User {
FirstName = "Jane",
LastName = "Smith",
Age = 17
};

var result = ValidateModel(model);

Assert.That(result, Is.Not.Empty);
}

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

Источник: https://www.code4it.dev/csharptips/unit-test-model-validation/
👍12
День 1757. #ЗаметкиНаПолях
Версионирование API. Начало

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

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

Начиная с .NET 5, есть 5 вариантов версионирования API.

1. По URI (Пути)
Версия включается непосредственно в URL (/api/v1/products).
+ Наиболее простой и понятный метод.
- Может привести к дублированию URL и потребует от клиентов изменять URL для доступа к различным версиям.
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/products")]
public class ProductsV1Controller : ApiController
{
public IHttpActionResult Get()
{
// Реализация для версии 1
}
}

[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/products")]
public class ProductsV2Controller : ApiController
{
public IHttpActionResult Get()
{
// Реализация для версии 2
}
}


2. По строке запроса
Версия указывается в качестве параметра запроса в URL (/api/products?version=1).
+ Позволяет легко перейти на новую версию.
- Может быть не так очевидно, как управление через путь, и пользователи могут не заметить параметра.
public class ProductsController : ApiController
{
const string DefaultVersion = "1.0";

[HttpGet]
public IHttpActionResult Get()
{
var version = HttpContext.Current
.Request.QueryString["version"]
?? DefaultVersion;

if (version == "1.0")
{
// Реализация для версии 1
return Ok(…);
}
if (version == "2.0")
{
// Реализация для версии 2
return Ok(…);
}

return BadRequest("Версия не поддерживается");
}
}


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

Источник:
https://stefandjokic.tech/blog
👍17
День 1758. #ЗаметкиНаПолях
Версионирование API. Окончание
Начало

3. По HTTP-заголовку
Информация о версии отправляется в виде пользовательского заголовка в HTTP-запросе (X-API-Version: 1).
+ URL остаётся неизменным для разных версий, что может быть полезно для SEO и кэширования.
- Версию труднее обнаружить и сложнее тестировать, поскольку требуется установка HTTP-заголовков.
public class ProductsController : ApiController
{
private const string HeaderName = "X-API-Version";

[HttpGet]
public IHttpActionResult Get()
{
var version = HttpContext.Current
.Request.Headers[HeaderName];

if (version == "1.0")
{
// Реализация для версии 1
return Ok(…);
}
if (version == "2.0")
{
// Реализация для версии 2
return Ok(…);
}

return BadRequest("Требуется версия API");
}
}


4. По Media Type (заголовок Accept)
Информация о версии включается в заголовок Accept HTTP-запроса, часто с использованием пользовательского типа (application/vnd.myapi.v1+json).
+ Метод точно соответствует спецификации HTTP.
- Более сложно, а некоторым клиентам API может быть сложно управлять версиями.
[Produces("application/json")]
public class ProductsController : ApiController
{
[HttpGet]
public IHttpActionResult Get()
{
var version = GetVersionFromAcceptHeader();
// реализация аналогична методу 3
}

private string GetVersionFromAcceptHeader()
{
var header = Request.Headers.
Accept.FirstOrDefault();
if (header != null)
{
var version =
header.Parameters.FirstOrDefault(
p => p.Name.Equals("version",
StringComparison.OrdinalIgnoreCase));
return version?.Value;
}
return null;
}
}


5. По имени хоста
Разные версии API размещаются на разных доменных именах (api-v1.example.com). Может осуществляться через маршрутизацию или перезапись URL. Обычно не обрабатывается непосредственно в контроллере, а скорее в настройках IIS или обратного прокси.
+ Наиболее интуитивно понятен для конечных пользователей.
- Требует дополнительной настройки инфраструктуры и может усложнить управление сертификатами SSL.

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

Источник: https://stefandjokic.tech/blog
👍11
День 1759. #Оффтоп #Безопасность
Осторожно: URL – Это Указатели на Изменяемые Сущности

Люди часто оценивают URL как: «Безопасные или вредоносные». В частности, поставщики продуктов безопасности ставят URL в один ряд с такими индикаторами опасности, как хэш известного вредоносного файла, вредоносный/скомпрометированный цифровой сертификат или заведомо вредоносный IP.

К сожалению, это разные вещи. Хэш файла никогда не меняется, содержимое файла не может измениться без изменения его хэша, и «безвредный» файл не может стать «вредоносным»*.
*Мы говорим о самом файле, а не о пути к файлу.

Однако IP или URL, может легко измениться с «безобидного» на «опасный» и наоборот. IP – просто указатель, а URL — указатель на указатель. И даже после того, как клиент подключается к целевому серверу, этот сервер решает, как интерпретировать запрос, и может каждый раз возвращать разное содержимое.

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

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

Как это возможно? Злоумышленники просто воспользовались тем фактом, что URL являются указателями на изменяемые объекты. Скорее всего злоумышленник разместил заказ на рекламу и указал ссылку на редиректор, который перенаправлял на какую-то страницу на YouTube. Система проверки рекламы Google проверила целевой URL, чтобы убедиться, что он действительно указывает на YouTube, и начала показывать рекламу. Затем злоумышленник обновил редиректор на свой мошеннический сайт — классическая уязвимость «время проверки, время использования».

Цепочка перенаправлений браузера при нажатии ссылки
Из-за того, как работает модель безопасности веб-платформы, способность Google обнаруживать такого рода уловки ограничена: после того, как браузер пользователя покидает сервер googleadservices.com, рекламная система Google не знает, где в итоге окажется пользователь, и не может знать, что следующий редиректор теперь отправляет пользователя на сайт атаки.

Более того, вы можете подсмотреть на подсказку статуса браузера в нижнем углу браузера, чтобы увидеть, куда ведёт ссылка, прежде чем нажать на неё. Но на самом деле перед этим есть ещё один уровень косвенности: ссылка (элемент <a>) сама по себе является указателем-на-указатель-на-указатель. Благодаря манипуляциям с JavaScript URL, на который указывает ссылка на странице, может измениться при нажатии на него.

В частности, страница результатов поиска Google помещает URL в тэг <a>, но, когда пользователь нажимает на него, URL изменяется на «настоящий»: googleadservices.com/... и только потом осуществляется перенаправление на целевой URL.

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

Источник: https://textslashplain.com/2023/10/13/beware-urls-are-pointers-to-mutable-entities/
👍10
День 1760. #ЗаметкиНаПолях
Меняем Поведение Конструкторов Копирования Записей
Когда вы используете записи, вы можете создать новый экземпляр, используя ключевое слово new или скопировать экземпляр с некоторыми изменениями, используя выражение with. Выражение with копирует все поля из исходного экземпляра, а затем применяет изменения.
var john = new Person("John", "Doe");
var Jane = john with { FirstName = "Jane" };

record Person(string FirstName, string LastName);

Если запись содержит изменяемый объект, например List<T>, экземпляр будет совместно использоваться оригиналом и копией. Это связано с тем, что выражение with не клонирует изменяемый объект:
var john = new Person("John", "Doe") { 
Phones = ["1234567890"]
};
var jane = john with { FirstName = "Jane" };
jane.Phones.Add("123");

// Both list are modified
Console.WriteLine(john.Phones.Count); // 2
Console.WriteLine(jane.Phones.Count); // 2

public record Person(string FirstName, string LastName)
{
public List<string>? Phones { get; init; }
}

При использовании выражения with вызывается созданный компилятором метод <Clone>$. Он вызывает конструктор копирования. Если вы не реализуете конструктор копирования, компилятор сгенерирует конструктор, копирующий все поля. Вот сгенерированный код для записи Person:
internal class Person
{
// ...

[CompilerGenerated]
public virtual Person <Clone>$()
{
return new Person(this);
}

[CompilerGenerated]
protected Person(Person original)
{
<FirstName>k__BackingField = original.<FirstName>k__BackingField;
<LastName>k__BackingField = original.<LastName>k__BackingField;
<Phones>k__BackingField = original.<Phones>k__BackingField;
}
}

Если вы хотите клонировать свойство Phones при использовании выражения with, вы можете реализовать конструктор копирования вручную:
public record Person(string FirstName, string LastName)
{
public List<string>? Phones { get; init; }

protected Person(Person other)
{
FirstName = other.FirstName;
LastName = other.LastName;
if(other.Phones != null)
Phones = new List<string>(other.Phones);
}
}

var john = new Person("John", "Doe") {
Phones = ["1234567890"];
var jane = john with { FirstName = "Jane" };
jane.Phones.Add("0987654321");

// Изменится только оригинал, но не копия
Console.WriteLine(john.Phones.Count); // 1
Console.WriteLine(jane.Phones.Count); // 2


Источник: https://www.meziantou.net/customizing-the-behavior-of-the-record-copy-constructor.htm
👍23👎2
День 1761. #Docs
Пишем Документацию, Которую Будут Читать и Использовать
Вы завершили работу и выпустили классный продукт. Но никто, кроме вас, не знает, как им пользоваться. Вы начинаете это описывать… но не помните, как решили ту проблему полгода назад. Многое из того, что вы пишете, предполагает, что читатель знает столько же, сколько вы. И вы даже не помните, как некоторые детали соединены или как должны работать вместе. Это очень распространённый сценарий. Документация часто пишется после того, как всё сделано, и с учётом времени до дедлайна. Мы забываем важные шаги и предполагаем, что у читателей есть опыт и знания. Это делает документацию трудной для чтения и разочаровывает читателей.

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

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

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

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

1. Пишите для своей аудитории
Переоценка или недооценка аудитории, скорее всего, затруднит использование документации для ваших реальных читателей. Предположения о знаниях людей могут оказаться исключающими, если вы не будете осторожны.
Например, вы документируете пакет для тестирования сайта. Если вы предполагаете, что ваша аудитория знает систему, можно начать с «Шаг 1. Запустите веб-сайт локально». Это предположение исключает из аудитории любого, кто не знает, как это сделать. Остальное написанное может быть для него бесполезным.
Другая крайность — допущение, что аудитория ничего не знает. Если это опытные тестировщики, а документация читается так, будто написана для нетехнических людей, читатель может почувствовать, что вы разговариваете с ним свысока, заскучать и бросить чтение. Важен поиск золотой середины.
Пакет тестирования скорее всего используют тестировщики и разработчики. Если он хорош, им могут пользоваться долгие годы. Т.е. у кого-то может быть опыт, а кому-то нужно будет повышение квалификации. Как учесть и тех, и других?

2. Используйте модульную документацию
Возвращаясь к «Шаг 1. Запустите веб-сайт локально». Опытным читателям больше объяснять не надо. А для новичков… напишите больше документации! Точнее, посмотрите, существует ли уже документация для этого. Если да, и она актуальна, оставьте ссылку на неё. Если нет, напишите новую страницу документации об этом.
Модульность документации позволит обеим аудиториям продолжить работу на своём уровне. Экспертам - пройти этот шаг быстро, новичкам - копнуть глубже и получить новый опыт.

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

Источник
👍7👎2
День 1762. #ЗаметкиНаПолях
Повышаем Производительность EF Core с Помощью Скомпилированных Запросов
Скомпилированные запросы в .NET появились как ответ на проблемы с производительностью, возникающие при операциях извлечения данных, особенно в приложениях, использующих ORM, такие как Entity Framework.

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

Допустим, у нас есть класс модели данных:
public class User
{
public int Id { get; init; }
public string Name { get; init; }
public int Age { get; init; }
}

Стандартный код для извлечения пользователя по Id будет примерно таким:
using var dbCtx = new AppDbContext();

var user =
dbCtx.Users.FirstOrDefault(x => x.Id == x);

Создадим скомпилированный запрос:
public class AppDbContext
{
private static
Func<AppDbContext, int, User> getUser =
EF.CompileQuery(
(ctx, id) => ctx.User
.FirstOrDefault(n => n.Id == id));

public User GetUser(int id)
{
return getUser(this, id);
}
}

Здесь:
- Статическое поле getUser типа Func<AppDbContext, int, User> - это скомпилированный запрос с использованием метода EF.CompileQuery.
- Метод GetUser – принимает id в качестве параметра и использует статическое поле getUser для получения объекта User из БД.

Мы также можем использовать асинхронный скомпилированный запрос с помощью EF.CompileAsyncQuery. Тогда поле должно быть типа Func<AppDbContext, int, Task<User>>, а метод GetUser будет асинхронным:
public async Task<User> GetUserAsync(int id)
{
return await getUser(this, id);
}


Теперь можно вызвать метод, созданный в нашем контексте:
using var dbCtx = new AppDbContext();
var user = dbCtx.GetUser(id);

Зачем использовать скомпилированные запросы?
1. Производительность
Преобразование запроса из LINQ в SQL выполняется один раз и используется повторно, что особенно полезно для часто выполняемых запросов.
2. Эффективное использование ресурсов
Поскольку запрос компилируется один раз и кэшируется*, это снижает загрузку ЦП и потребление ресурсов, связанных с процессом компиляции запроса.
*Замечание: EF Core автоматически компилирует и кэширует наиболее распространённые запросы, уменьшая необходимость делать это вручную.

Когда не использовать
1. Высокодинамичные запросы
Если запросы часто меняются или динамически конструируются на основе различных условий.
2. Редко выполняемые запросы
Выигрыш в производительности может быть незначительным.
3. Среды с ограниченными ресурсами
Если использование памяти ограничено, будьте осторожны с количеством скомпилированных запросов.

Итого
Скомпилированные запросы в EF Core — мощная функция для оптимизации доступа к БД. Они наиболее полезны для повышения производительности в сценариях с часто повторяющимися запросами. Однако их использование должно быть сбалансировано с учётом потенциальных недостатков, таких как повышенное использование памяти и сложность, особенно в приложениях с динамическими шаблонами запросов или ограниченными ресурсами.

Источник: https://stefandjokic.tech/blog
👍23
День 1763. #ЗаметкиНаПолях
Оптимизируем Запросы EF Core.
Продолжаем разговор о быстродействии запросов в EF Core. Существует множество правил оптимизации запросов, сегодня рассмотрим основные.

1. Выбирайте только нужные поля
Рассмотрим пример:
var employees = context.Employees
.Select(e => new EmployeeDto
{
Name = e.Name,
Email = e.Email
}).ToList();

В БД скорее всего будет больше 2 полей. Но нам не нужны остальные.

Преимущества

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

2. Избегайте N+1 запросов
Это происходит, когда ваше приложение выполняет один запрос для получения набора объектов, а затем дополнительные запросы для каждого объекта для получения связанных данных:
// Получаем все блоги - 1 запрос
var blogs = context.Blogs.ToList();
foreach (var b in blogs)
{
// Для каждого блога получаем посты
// N запросов
var posts = b.Posts;
foreach (var p in posts)
Console.WriteLine(p.Title);
}

Вместо этого используем «жадную загрузку»:
var blogs = context.Blogs
.Include(b => b.Posts).ToList();
foreach (var b in blogs)
{
foreach (var p in b.Posts)
Console.WriteLine(post.Title);
}

«Жадная» загрузка с помощью Include(b => b.Posts) сообщает EF Core загрузить посты, связанные с каждым блогом, как часть изначального запроса. Таким образом выполняется один запрос, который извлекает все блоги со связанными с ними постами. Поэтому во время итерации в цикле foreach не выполняются никакие дополнительные запросы.

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

3. Используйте AsNoTracking()
Этот метод особенно полезен, когда вы только считываете данные из базы. В следующем примере список продуктов извлекается без затрат на отслеживание изменений:
var products = context.Products
.AsNoTracking().ToList();
// Используем products только для чтения

Преимущества
- Особенно заметно в крупных приложениях или при работе с большими наборами данных. Запросы выполняются быстрее и потребляется меньше памяти.
- Сокращение накладных расходов, т.к. не требуется отслеживать изменения или сохранять информацию о состоянии объектов.

4. Используйте разделённые запросы с помощью AsSplitQuery().
Подробности здесь.

Источник: https://stefandjokic.tech/blog
👍14
День 1764. #ЗаметкиНаПолях #Debugging
Правила отладки: Меняйте За Раз Что-то Одно

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

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

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

Изолируйте ключевой фактор
Суть эффективной отладки в выявлении критического фактора путем сужения фокуса до определённого раздела кода, в котором может возникнуть потенциальная проблема. Так процесс отладки становится более точным и эффективным. Чтобы сделать это систематически исключайте части программы и наблюдайте, сохраняется ли ошибка. Нет ли при этом знакомой закономерности? Признание того, что «я это видел раньше», часто означает начало понимания, если не полное решение.

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

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

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

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

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

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

Источник: https://dev.to/rajasegar/debugging-rules-change-one-thing-at-a-time-3kc6
👍6
День 1765. #ВопросыНаСобеседовании #Многопоточность
Самые часто задаваемые вопросы на собеседовании по C#


27. Опишите концепцию конфликта блокировок в многопоточности и объясните его влияние на производительность приложения. Как можно решить и смягчить проблемы, связанные с конфликтами блокировок?

Конфликт блокировок — это состояние, когда один поток ожидает другого, пытаясь получить блокировку. Какое бы время ни было потрачено на ожидание блокировки, это «потерянное» время, потраченное впустую. Очевидно, что это может вызвать серьезные проблемы с производительностью. Конфликты блокировок могут быть вызваны любым типом механизма синхронизации потоков. Это может быть из-за оператора блокировки, AutoResetEvent/ManualResetEvent, ReaderWriterLockSlim, Mutex или Semaphore и всех остальных. Всё, что заставляет один поток приостанавливать выполнение до тех пор, пока не поступит сигнал другого потока, может вызвать конфликт блокировок.

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

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

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

3. Используйте структуры данных и алгоритмы без блокировки
Если возможно, используйте неблокирующие алгоритмы и структуры данных, которые не полагаются на блокировки, такие как ConcurrentQueue, ConcurrentDictionary или ConcurrentBag.

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

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

6. Избегайте вложенных блокировок
Уменьшите риск взаимоблокировок и конфликтов, избегая вложенных блокировок или иерархий блокировок.

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

Источник: https://dev.to/bytehide/c-multithreading-interview-questions-and-answers-4opj
👍24
This media is not supported in your browser
VIEW IN TELEGRAM
День 1766. #ЧтоНовенького
Пишем Сообщения Коммитов с Помощью GitHub Copilot

В последней превью версии Visual Studio добавлена новая функция –автоматическое описание изменений в сообщении коммита.

Чтобы опробовать новую функцию, загрузите последнюю превью версию Visual Studio и обновите расширение GitHub Copilot Chat Extension. Вам также понадобится активная подписка GitHub Copilot.

Новая функция Generated Commit Message (Сгенерированное Сообщение Коммита) использует GitHub Copilot для описания изменений вашего кода. Благодаря этому написание описательных и полезных сообщений о коммитах становится таким же простым, как нажатие кнопки и последующее добавление пояснений.

Используйте новый значок ручки в окне Git Changes, чтобы сгенерировать предложение. GitHub Copilot проверит изменения файлов в вашем коммите, обобщит их, а затем опишет каждое изменение. См. видео. Затем вы можете использовать предложение или отменить его, а также добавить свои комментарии.

Источник
👍9
День 1767. #Оффтоп
А вы знали, что даже в таком, казалось бы, простом алгоритме, как бинарный поиск, может быть баг? Причём, не где-нибудь, а в реализации в Java. И просуществовал он там почти 10 лет!

Об этом в новом видео на канале Computerphile.

А если интересно узнать, либо вспомнить, собственно алгоритм, то объяснение здесь.
👍6
День 1768. #Карьера
Пишем Код, как Сеньор

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

1. Завершите работу, прежде чем двигаться дальше.
Часто вы можете оказаться в ситуации, когда время поджимает, работа на 90% готова, и возникает желание отложить оставшееся на потом. Но так вы накапливаете для себя технический долг. Возможно, только вы об этом знаете, но рано или поздно другие тоже узнают. Лучше честно сказать, что вы ещё не закончили. Если руководство разочаровано, пусть лучше они знают реальное состояние проекта вместо того, чтобы ваша ложь выяснилась на более поздних этапах. Кто-то другой будет просматривать ваш код в будущем. И вас будут оценивать, глядя на ваш код. Лучше убедиться, что вы действительно закончили работу – это одна из главных отличительных черт сеньора.

2. Соблюдайте стандарты кодирования.
Сегодня большинство IDE могут форматировать код (например, editorconfig для Visual Studio). Если в вашей IDE этого нет, найдите утилиту или плагин. Ничто не разочаровывает больше в кодовой базе, чем обнаружить разные стили написания кода в разных местах. Имеет смысл создать стандарт для команды, либо использовать какой-то готовый, и договориться его придерживаться.

3. Дисциплинированно документируйте шаблоны.
Многие команды, начиная проект, договариваются о наборе шаблонов/библиотек и приступают к работе. Не полагайтесь на самодокументирующийся код, а потратьте время на фактическую документацию шаблонов и решений, которые вы приняли в проекте. Тогда, если вы при обзоре кода обнаружите, что разработчик использует другой шаблон, у вас не будет дебатов на тему, нужен он или нет, - вы сможете сослаться на документ. Также, если вы используете шаблон, который уже является частью фреймворка или языка, добавьте ссылку на документацию, где объясняется, как его использовать, чтобы остальные не гуглили, а имели именно тот пример, который использовали вы. Если вы собираетесь добавить новый шаблон, сначала обсудите это с командой. Возможно, кто-то раньше его уже рассматривал, и отказался от него по каким-то причинам. Не останавливайтесь на устном обсуждении. Задокументируйте цель шаблона, когда его использовать, а когда нет, и несколько примеров кода его применения в распространённых сценариях.

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

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

Источник:
https://youtu.be/oJbfMBROEO0
👍15👎1
День 1769. #ЗаметкиНаПолях
Используем Перехватчики в EF Core

Перехватчики в EF Core позволяют перехватывать, изменять или подавлять операции EF Core.

Перехватчики регистрируются для каждого экземпляра DbContext при настройке контекста. Каждый перехватчик реализует интерфейс IInterceptor. Несколько распространённых производных интерфейсов включают
- IDbCommandInterceptor,
- IDbConnectionInterceptor,
- IDbTransactionInterceptor,
- ISaveChangesInterceptor.

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

Добавление записей аудита
Записи аудита изменений сущностей - ценная функция в некоторых приложениях. Вы записываете дополнительную информацию аудита каждый раз, когда объект создаётся или изменяется. Также это могут быть значения «до» и «после», в зависимости от ваших требований.
Например, создадим интерфейс IAuditable с датами создания и изменения объекта:
public interface IAuditable
{
DateTime Created { get; }
DateTime? Modified { get; }
}

Добавим UpdateInterceptor для записи значений аудита. Он использует ChangeTracker для поиска всех экземпляров IAuditable и устанавливает соответствующее значение свойства. Здесь мы используем метод SavingChangesAsync, который запускается до того, как изменения будут сохранены в БД.
internal sealed class UpdateInterceptor 
: SaveChangesInterceptor
{
public override
ValueTask<InterceptionResult<int>>
SavingChangesAsync(
DbContextEventData e,
InterceptionResult<int> result,
CancellationToken ct = default)
{
if (e.Context is not null)
UpdateEntities(e.Context);

return base
.SavingChangesAsync(e, result, ct);
}

private static void
UpdateEntities(DbContext ctx)
{
var now = DateTime.UtcNow;
var entities = ctx
.ChangeTracker
.Entries<IAuditable>()
.ToList();

foreach (var e in entities)
{
if (e.State == EntityState.Added)
e.Property(
nameof(IAuditable.Created)) = now;

if (e.State == EntityState.Modified)
e.Property(
nameof(IAuditable.Modified)) = now;
}
}
}

Эту реализацию можно легко расширить, включив в неё, например, информацию о текущем пользователе.

Зарегистрировать перехватчик можно следующим образом:
services.AddSingleton<UpdateInterceptor>();
services.AddDbContext<
IApplicationDbContext,
AppDbContext>(
(sp, opts) => opts
.UseSqlServer(connString)
.AddInterceptors(
sp.GetRequiredService<UpdateInterceptor>());


Источник: https://www.milanjovanovic.tech/blog/how-to-use-ef-core-interceptors
👍18
День 1770. #ЗаметкиНаПолях #AsyncAwaitFAQ
ConfigureAwait в .NET 8. Начало

Мы привыкли использовать ConfigureAwait(false), чтобы предотвратить захват контекста. Изначально его рекомендовали использовать везде, но со временем рекомендация поменялась на «используйте его в коде библиотек, но не в коде приложения». Это довольно простое правило. Недавно (в частности, из-за того, что SynchronizationContext был удалён из ASP.NET Core), многие решили вовсе отказаться от ConfigurationAwait(false), даже в коде библиотек.

Есть несколько распространенных заблуждений относительно ConfigureAwait(false):
1. Это не лучший способ избежать взаимоблокировок. Это не его цель. Чтобы избежать взаимоблокировок, необходимо убедиться, что весь асинхронный код использует ConfigureAwait(false), включая код в библиотеках и среде выполнения. Да и просто это не очень удобное в обслуживании решение.

2. ConfigureAwait настраивает ожидание, а не задачу. Например, функция ConfigureAwait(false) в
SomethingAsync().ConfigureAwait(false).GetAwaiter().GetResult() 

ничего не делает. Аналогично, ожидание в
var task = SomethingAsync();
task.ConfigureAwait(false);
await task;

по-прежнему продолжает использовать захваченный контекст, полностью игнорируя ConfigureAwait(false).

3. ConfigureAwait(false) не означает «запустить остальную часть этого метода в потоке из пула потоков» или «запустить остальную часть этого метода в другом потоке». Он работает только в том случае, если await передаёт управление, а затем возобновляет асинхронный метод. В частности, await не передаст управление, когда задача уже завершена, ConfigureAwait не будет иметь никакого эффекта, поскольку ожидание продолжится синхронно.

Теперь посмотрим, что изменилось в ConfigureAwait в .NET 8.
Ничто из существующего поведения не изменилось. Поведение по умолчанию то же, и ConfigurationAwait(false) имеет то же поведение. Добавлена новая перегрузка.

ConfigureAwait(ConfigureAwaitOptions)
ConfigurationAwaitOptions — это новый тип, который предоставляет все возможные способы настройки ожидаемых объектов:
[Flags]
public enum ConfigureAwaitOptions
{
None = 0x0,
ContinueOnCapturedContext = 0x1,
SuppressThrowing = 0x2,
ForceYielding = 0x4,
}


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

Во-вторых, пока это доступно только в Task и Task<T>, по крайней мере, для .NET 8. В ValueTask/ValueTask<T> это пока не добавлено.

Далее рассмотрим каждую из опций.

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

Источник:
https://blog.stephencleary.com/2023/11/configureawait-in-net-8.html
👍12👎2
День 1771. #ЗаметкиНаПолях #AsyncAwaitFAQ
ConfigureAwait в .NET 8. Окончание

Начало

None и ContinueOnCapturedContext
Эти опции очевидны, с небольшим замечанием.
ContinueOnCapturedContext — то же самое, что true, т.е. ожидание захватит контекст и возобновит выполнение асинхронного метода в этом контексте.
None — то же самое, что false, т.е. ожидание не будет захватывать контекст.
Замечание. По умолчанию контекст НЕ захватывается. Если явно не добавить ContinueOnCapturedContext, контекст не будет захвачен.
Поведение по умолчанию не изменилось: отсутствие ConfigureAwait аналогично true или ContinueOnCapturedContext. Но при использовании других опций ConfigureAwaitOptions, для захвата контекста нужно будет добавлять флаг ContinueOnCapturedContext (см. ниже).

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

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

Замечание: эта семантика не работает для Task<T>, поскольку в этом случае ожидание должно возвращать значение типа T. Неясно, какое значение будет уместно вернуть в случае игнорируемого исключения, поэтому текущее поведение заключается в выдаче исключения ArgumentOutOfRangeException во время выполнения. Было добавлено новое предупреждение:
CA2261: The ConfigureAwaitOptions.SuppressThrowing is only supported with the non-generic Task (ConfigureAwaitOptions.SuppressThrowing поддерживается только с необобщённым Task).
Это предупреждение лучше сделать ошибкой, поскольку во время выполнения оно всегда будет давать сбой.

ForceYielding
ForceYielding – флаг, который может быть полезен в очень редких случаях, но в этих случаях он будет очень удобен. Он похож на Task.Yield. Yield возвращает специальную ожидаемую задачу, которая всегда утверждает, что она не завершена, но немедленно планирует продолжение. Обычно await проверяет, завершена ли ожидаемая задача и, если да, продолжает синхронно. ForceYielding предотвращает такое синхронное поведение, заставляя ожидание вести себя асинхронно. Это может быть полезно, например, при модульном тестировании.

Обратите внимание, что сам по себе флаг ForceYielding также подразумевает отказ от продолжения работы с захваченным контекстом, поэтому это то же самое, что сказать «запланировать остальную часть этого метода для выполнения в пуле потоков» или «переключиться на поток из пула потоков».
Task.Yield возобновит работу в захваченном контексте, поэтому это не то же самое, что ForceYielding. Task.Yield аналогично комбинации флагов ForceYielding | ContinueOnCapturedContext.

Конечно, реальная ценность ForceYielding в том, что его можно применить к любой задаче. Раньше приходилось либо добавлять отдельный await Task.Yield(); либо создавать собственный ожидаемый объект. Теперь в этом больше нет необходимости, поскольку ForceYielding можно применять к любой задаче.

Источник: https://blog.stephencleary.com/2023/11/configureawait-in-net-8.html
👍9
День 1772. #ЗаметкиНаПолях #PatternMatching
Шпаргалка по Сопоставлению по Шаблону в C#

Сопоставление по шаблону — это функция, используемая для проверки выражений на соответствие некоторым условиям с одновременной проверкой их типов. Исторически оно было отличительной чертой функционального программирования, и уже существует в других популярных языках, таких как Scala, Rust, Python, Haskell, Prolog и многих других. Сопоставление по шаблону было представлено в C# 7, и с тех пор получало множество обновлений в последующих версиях.

Помните, что его можно применять только в выражениях is или switch.

С# 7
Шаблон типа

Проверка типа выражения
public bool IsFood(object product)
=> product is Food;


Шаблон объявления
Проверка типа + присвоение переменной при успехе.
public bool IsFridgeFood(object product)
=> product is Food food &&
RequiresFridge(food.StorageTemp);


Шаблон константы
Проверка на константное значение: int, float, char, string, bool, enum, const, null.
public bool IsFresh(Food food)
=> food?.Category?.ID is (int)Category.Fresh;


Шаблон null
Проверка ссылочного или обнуляемого типа на null
public bool DoesNotExist(Food food)
=> food is null;


Шаблон var
Подобно шаблону типа, шаблон var сопоставляет шаблон, проверяет на null и присваивает значение переменной. Тип var объявляется на основе типа времени компиляции соответствующего выражения.
public bool RequiresFridge(Food food)
=> GetStorageRequirement(food) is var req &&
req is StorageRequirement.Freezer;


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

Источник:
https://codingsonata.com/your-quick-guide-to-pattern-matching-in-c/
👍11