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

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 2483. #ЗаметкиНаПолях
Сжатие Запросов HttpClient с Помощью GZIP
Если вам нужно отправлять большие файлы во внешний API, вы можете решить, что необходимо сжимать их перед отправкой. Однако дело в том, что очевидное решение может привести к проблемам с памятью…

Рассмотрим такой код:
internal async ValueTask<HttpRequestMessage>
CreateRequest(Uri uri, Stream source)
{
var outStream = new MemoryStream();
await using (var zip =
new GZipStream(outStream,
CompressionMode.Compress,
leaveOpen: true))
{
await source.CopyToAsync(zip);
zip.Close();
}
outStream.Position = 0;

var request = new HttpRequestMessage(
HttpMethod.Post, uri)
{
Content = new StreamContent(outStream)
};
request.Content.Headers.ContentEncoding.Add("gzip");
return request;
}


Использовать его можно примерно так:
var uri = new Uri("https://my.api/endpoint");
var file = File.OpenRead("largefile.txt");
var request = await CreateRequest(uri, file);
await httpClient.SendAsync(request);

Обычно этот код просто работает. Однако каждый раз при отправке запроса сжатый gzip-контент создаётся в памяти (благодаря MemoryStream) перед отправкой. И если требуется отправлять много таких запросов, приложение начнёт испытывать нехватку памяти.

Решение в gzip-сжатии «на лету»:
public class GzipContent : HttpContent
{
private readonly Stream _source;

public GzipContent(Stream source)
{
_source = source;
Headers.ContentEncoding.Add("gzip");
}

protected override async Task
SerializeToStreamAsync(
Stream stream,
TransportContext? context)
{
await using var zip =
new GZipStream(stream,
CompressionMode.Compress,
leaveOpen: true);
await _source.CopyToAsync(zip);
}

protected override bool
TryComputeLength(out long length)
{
length = -1;
return false;
}
}

Также переделаем CreateRequest:
internal ValueTask<HttpRequestMessage>
CreateRequest(Uri uri, Stream source)
{
return ValueTask.FromResult(
new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = new GzipContent(source)
});
}

Использование будет точно таким же.

Производительность
Вот мои тесты в .NET10. По времени особой разницы нет, зато версия GzipContent выделяет гораздо меньше памяти. Чем больше файл, тем заметнее разница.
| File | Method| Mean   | Ratio| Allocated| Alc Rat |
|-----:|-------|-------:|-----:|---------:|--------:|
| 7KB| Memory| 633.3us| 1.00| 6.35KB| 1.00|
| 7KB| Gzip | 603.5us| 0.96| 4.77KB| 0.75|
| 200KB| Memory| 8.025ms| 1.00| 508.52KB| 1.000|
| 200KB| Gzip | 7.260ms| 0.90| 4.74KB| 0.009|
| 3MB| Memory| 94.97ms| 1.00| 8189.99KB| 1.000|
| 3MB| Gzip | 86.02ms| 0.91| 4.82KB| 0.001|


PS: .NET API может автоматически распаковывать запросы. Достаточно добавить соответствующее промежуточное ПО:
builder.Services.AddRequestDecompression();

// …

app.UseRequestDecompression();


Источник: https://josef.codes/compress-httpclient-requests-with-gzip-dotnet-core/
👍16
День 2484. #ЗаметкиНаПолях
Разбираем Однофайловые Приложения в .NET 10. Начало
C# всегда был немного перегружен церемониями. Даже простейший «Hello World» традиционно требовал файла решения, файла проекта и достаточного количества шаблонного кода, чтобы заставить вас задуматься, а не лучше ли использовать скриптовый язык. В .NET 10 представлены однофайловые приложения.

Что это?
Идея проста: пишете код C# в одном CS-файле и запускаете его напрямую. Не нужно настраивать структуру целого проекта для быстрого служебного скрипта.

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

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

Начало работы
Предположим, нужно быстро проверить, на какой день недели приходится определённая дата. Создадим файл с date-checker.cs:
var targetDate = new DateTime(2025, 12, 31);
Console.WriteLine(
$"Новый год в 2025 - это {targetDate.DayOfWeek}");
Console.WriteLine(
$"Через {(targetDate - DateTime.Today).Days} дней");

Выполним это с помощью:
dotnet run date-checker.cs

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

Пример из реальной жизни
Допустим, нужно быстро обработать JSON и сформировать отчёт. Используем System.Text.Json и CsvHelper:
#:package [email protected]
using System.Text.Json;
using CsvHelper;
using System.Globalization;
using System.Text.Json.Serialization.Metadata;

var json = await
File.ReadAllTextAsync("sales_data.json");
var sales =
JsonSerializer.Deserialize<List<SaleRecord>>(
json,
new JsonSerializerOptions() {
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
});

var topProducts = sales
.GroupBy(s => s.Product)
.Select(g => new {
Product = g.Key,
TotalRevenue = g.Sum(s => s.Amount),
UnitsSold = g.Count()
})
.OrderByDescending(p => p.TotalRevenue)
.Take(10);

using var writer =
new StreamWriter("top_products.csv");
using var csv =
new CsvWriter(writer, CultureInfo.InvariantCulture);
csv.WriteRecords(topProducts);

Console.WriteLine(
"Отчёт сохранён в top_products.csv");

record SaleRecord(string Product, decimal Amount, DateTime Date);

Заметьте, как мы используем и ссылки на NuGet-пакеты, и асинхронные операции, и LINQ, и записи — всё в одном файле, который читается как связный скрипт. Обычно для таких задач используется Python, но теперь можно остаться в C#.

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

Источник:
https://www.milanjovanovic.tech/blog/exploring-csharp-file-based-apps-in-dotnet-10
👍36
День 2485. #ЗаметкиНаПолях
Разбираем Однофайловые Приложения в .NET 10. Окончание

Начало

Создаём что-то более амбициозное с Aspire
Даже AppHost для Aspire можно создать в одном файле:
#:sdk [email protected]
#:package [email protected]

var builder =
DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache")
.WithDataVolume();

var postgres = builder.AddPostgres("postgres")
.WithDataVolume()
.AddDatabase("tododb");

var todoApi =
builder.AddProject<Projects.TodoApi>("api")
.WithReference(cache)
.WithReference(postgres);

builder.AddNpmApp("frontend", "../TodoApp")
.WithReference(todoApi)
.WithReference("api")
.WithHttpEndpoint(env: "PORT")
.WithExternalHttpEndpoints();

builder.Build().Run();


С помощью директивы #:sdk [email protected] ваш отдельный файл становится полноценным оркестратором для распределённого приложения. Вы определяете инфраструктуру, подключаете зависимости и настраиваете полноценную среду разработки — и всё это без создания файла проекта. Это особенно полезно при создании прототипов архитектур или для быстрого развёртывания тестовой среды.

Переход на полноценный проект
Со временем некоторые скрипты перерастают рамки одного файла. Возможно, требуется разделить код на несколько файлов или, может быть, нужна полноценная поддержка IDE для отладки. Переход проходит без проблем:
dotnet project convert MyUtility.cs

Это создаст правильную структуру проекта, сохраняя все ссылки на пакеты и выбранные SDK. Ваш код переместится в Program.cs, и вы получите файл .csproj, отражающий все использованные вами директивы #:.

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

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

Но с той задачей, для которой он предназначен (обеспечение доступности C# для сценариев написания скриптов), он работает на удивление хорошо. Его можно использовать для скриптов сборки, одноразовой миграции данных, быстрых тестов API или даже для обучения C#, не перегружая новичков структурой проекта.

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

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

Источник: https://www.milanjovanovic.tech/blog/exploring-csharp-file-based-apps-in-dotnet-10
👍9
День 2486. #ЗаметкиНаПолях
Удаляем Старые Версии .NET
Со временем на вашем компьютере и серверах, где вы работаете, может накопиться несколько версий среды выполнения и SDK .NET. Хотя наличие нескольких версий часто необходимо для обеспечения совместимости, старые версии, которые больше не нужны, могут занимать ценное дисковое пространство и загромождать систему. Рассмотрим процесс безопасного определения и удаления старых версий .NET.

Зачем?
- Место на диске: каждая версия SDK занимает несколько сотен мегабайт.
- Прозрачность: меньшее количество версий упрощает управление средой разработки.
- Безопасность: старые версии могут содержать известные уязвимости.
- Обслуживание: сохранение только необходимых версий упрощает обновления и устранение неполадок.

Проверка установленных версий
Откройте терминал или командную строку и выполните:
dotnet --list-sdks

Это выдаст все установленные версии SDK. Что-то вроде:
5.0.101 [C:\Program Files\dotnet\sdk]
8.0.100-preview.5.23303.2 [C:\Program Files\dotnet\sdk]
9.0.100-preview.6.24328.19 [C:\Program Files\dotnet\sdk]
10.0.100 [C:\Program Files\dotnet\sdk]

Чтобы проверить версии среды выполнения:
dotnet --list-runtimes


Важно перед удалением
Не удаляйте всё! Перед удалением версий учтите:
- Активные проекты: проверьте, на какие версии SDK ориентированы текущие проекты.
- Требования команды: убедитесь, что не удаляете версии, необходимые для командных проектов.
- Конвейеры CI/CD: убедитесь, что конвейеры сборки не зависят от определённых версий.
- Статус поддержки: сохраните хотя бы одну версию, которая в настоящее время поддерживается.
Вы можете проверить жизненный цикл поддержки .NET на официальной странице Microsoft.

Использование инструмента удаления .NET
Microsoft предоставляет официальный инструмент удаления, который делает процесс безопасным и простым. Скачайте msi-файл с официальной страницы релизов GitHub и запустите установку. После завершения установки инструмент dotnet-core-uninstall станет доступен и в командной строке.

Сначала попробуем команду list, чтобы увидеть, какие версии она может найти и удалить:
dotnet-core-uninstall list

Можно использовать команду пробного запуска с фильтром, чтобы увидеть, на что повлияет выполнение удаления:
dotnet-core-uninstall dry-run --all-below 8.0.0 --hosting-bundle

Если вы готовы, удалите версии:
dotnet-core-uninstall remove --all-below 8.0.0 --hosting-bundle

То же можно сделать для сред выполнения и SDK:
dotnet-core-uninstall remove --all-below 8.0.0 --aspnet-runtime
dotnet-core-uninstall remove --all-below 8.0.0 --runtime
dotnet-core-uninstall remove --all-below 8.0.0 --sdk


Полезные опции
--all-below <version>: Удалить все версии ниже указанной;
--all-but-latest: Оставить только последнюю версию;
--all-previews: Удалить все превью-версии;
--sdk: Точная версия SDK;
--runtime: Точная версия среды выполнения;
--force: Пропустить проверку зависимостей (используйте осторожно!).

Рекомендации
Дополнительные советы по управлению версиями .NET Core:
- Использование global.json: прикрепите определённые проекты к определённым версиям SDK.
- Регулярная очистка: запланируйте ежеквартальный просмотр установленных версий.
- Сохраните одну версию LTS: всегда поддерживайте как минимум одну версию с долгосрочной поддержкой.
- Документируйте зависимости: отслеживайте, какие версии нужны проектам.
- Использование Docker: для старых проектов рассмотрите возможность использования контейнеров Docker вместо локальной установки старых SDK.

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

Источник: https://bartwullems.blogspot.com/2025/11/how-to-uninstall-older-net-core-versions.html
1👍27👎1
День 2487. #ВопросыНаСобеседовании #Архитектура
Оптимизируем Генерацию Отчёта

«Пользователь нажимает кнопку в интерфейсе, чтобы создать отчёт в Excel или PDF. Создание отчёта занимает около пяти минут (время может быть произвольным). Пользователю приходится ждать завершения. Как бы вы оптимизировали этот процесс?»

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

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

Мы часто застреваем на мысли «сделать код быстрее». Это не что-то плохое, оптимизация производительности — ценный навык. Но мы не видим более серьёзной проблемы. Приложение выполняет всю работу синхронно, держа пользователя в заложниках до завершения.

Правильнее было бы спросить не «Как это ускорить?», а «Почему пользователь вообще ждёт?»
Если что-то выполняется минуты (или часы, дни), это не должно блокировать пользователя. Это должно происходить в фоновом режиме, вне основного потока запросов, пока пользователь занимается своими делами.

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

Как решить проблему долгого ожидания?
Оставим ту же кнопку «Сгенерировать отчёт», но при этом бэкенд примет запрос, сохранит его где-то (например, как запись о задании в БД) и сразу же вернёт управление. В этом суть создания асинхронных API. Затем задание принимается фоновым обработчиком.

В роли обработчика может выступать фоновый сервис, задание Quartz или даже функция AWS Lambda, активируемая сообщением из очереди. Он берёт на себя всю основную работу: извлечение данных, создание файла и его загрузку в хранилище, например, S3 или Azure Blob.

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

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

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

Это интересный вопрос, т.к. он показывает, как люди думают.

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

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

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

Источник: https://www.milanjovanovic.tech/blog/the-interview-question-that-changed-how-i-think-about-system-design
👍23
День 2488. #SystemDesign101
Как Разработать Хороший API
Хорошо спроектированный API кажется интуитивно понятным, он просто работает. Но за этой простотой кроется набор последовательных принципов проектирования, которые делают API предсказуемым, безопасным и масштабируемым.

Вот что отличает хорошие API от плохих.

1. Идемпотентность
GET, HEAD, PUT и DELETE должны быть идемпотентными. Отправка одного и того же запроса несколько раз даёт один и тот же результат и не содержит непреднамеренных побочных эффектов.

POST и PATCH не являются идемпотентными. Каждый вызов создаёт новый ресурс или каким-либо образом изменяет состояние. Используйте ключи идемпотентности, хранящиеся в Redis или в базе данных. Клиент отправляет один и тот же ключ с повторными попытками, сервер распознает его и возвращает исходный ответ, не обрабатывая его повторно.

2. Версионирование
При изменении контракта API меняйте его версию. Указывать версию можно в URL, строке запроса или в заголовках запроса.

3. Имена ресурсов, основанные на существительных
Ресурсы должны быть существительными, а не глаголами. "/api/products", а не "/api/getProducts".

4. Безопасность
Обеспечьте безопасность каждой конечной точки с помощью надлежащей аутентификации. Bearer-токены (например, JWT) включают заголовок, полезную нагрузку и подпись для проверки запросов. Всегда используйте HTTPS и проверяйте токены при каждом вызове.

5. Пагинация
При возврате больших наборов данных используйте параметры пагинации, например "?limit=10&offset=20", чтобы обеспечить эффективность и согласованность ответов.

См. также «REST vs RESTful. В чём Разница?»

Источник: https://blog.bytebytego.com
👍12
День 2489. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.

9. Асинхронное Программирование с async и await
«Можете объяснить, как ключевые слова async и await улучшают приложения .NET? Включите в свой ответ краткое объяснение работы этих ключевых слов и приведите пример сценария, в котором асинхронное программирование особенно полезно».

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

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

Практический пример того, где async и await полезны, — это приложения с пользовательским интерфейсом, где критически важно поддерживать отзывчивость. Например, сетевой запрос на получение данных из веб-сервиса. Используя async и await, можно вызвать веб-сервис, не замораживая пользовательский интерфейс в ожидании ответа. Это достигается за счёт ожидания сетевой операции, что позволяет потоку пользовательского интерфейса продолжать обработку других событий, таких как пользовательский ввод, до тех пор, пока данные не будут готовы, как показано в следующем коде:
public async Task LoadDataAsync()
{
try
{
// Асинхронно получаем данные
var data = await GetDataAsync();
DisplayData(data);
}
catch (Exception ex)
{
// Обрабатываем ошибки получения данных
ShowError(ex.Message);
}
}

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

Часто встречающийся неудачный ответ:
«async добавляется просто, чтобы ускорить выполнение метода, и await для каждого вызова метода внутри него. Это приводит к одновременному выполнению всех методов.»

В этом ответе неверно истолковываются назначение и функции async и await.

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

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

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

Подобные ошибки обычно возникают из-за поверхностного понимания концепций асинхронного программирования, что приводит к неверным предположениям о том, как async и await влияют на выполнение программы.

Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👍8👎4
День 2490. #ЧтоНовенького
Увеличиваем Тестовое Покрытие с GitHub Copilot Testing
Написание хороших тестов важно для надёжности ПО, но зачастую отнимает много времени и требует рутинных действий. Теперь GitHub Copilot Testing доступно в виде превью в Visual Studio. Эта новая возможность позволяет создавать модульные тесты на основе ИИ непосредственно в процессе разработки.

Copilot понимает структуру вашего кода, настройки вашего проекта и то, как должны выглядеть хорошие тесты. Нужны ли вам тесты для отдельного элемента, файла, всего проекта или даже всего решения, Copilot автоматически генерирует, собирает и запускает тесты в выбранной области. Результат? Быстрая обратная связь, меньше ошибок и больше уверенности в вашем коде.

Ключевые особенности
- Модульные тесты, генерируемые ИИ: автоматическое создание, сборка и запуск тестов для отдельных элементов, файлов, проектов или целых решений.
- Детерминированные, типобезопасные результаты: тесты основаны на семантике компилятора и языка, что обеспечивает согласованность и предсказуемость.
- Поддержка MSTest, xUnit и NUnit: работает с вашими существующими фреймворками и рабочими процессами.
- Интеллектуальная интеграция с Visual Studio: GitHub Copilot Testing интегрировано с Visual Studio, использует анализаторы Roslyn, MSBuild, интеграцию с системами проектов и Test Explorer для обеспечения бесперебойной настройки, выполнения и обнаружения тестов прямо из IDE.
- Автоматическое восстановление тестов: иногда не все тесты проходят сразу — и это нормально. В случае сбоя теста Copilot пытается исправить ошибку, перегенерировать тест и повторно запустить его, чтобы убедиться в его корректной работе

Начало работы
Необходимые условия: последняя сборка Visual Studio 2026 Insiders, код C# и лицензия GitHub Copilot.

Убедитесь, что функция GitHub Copilot Testing (Тестирование GitHub Copilot) включена в разделе Tools > Options > GitHub > Copilot > Testing (Инструменты > Параметры > GitHub > Copilot > Тестирование).

Откройте проект или решение и убедитесь, что сборка проходит без ошибок, чтобы упростить процесс. В окне чата Copilot используйте следующий запрос, чтобы запустить тестирование GitHub Copilot и начать генерацию тестов:
@Test #target

Где #target может быть именем члена, класса, файла, проекта, решения или #changes для git diff.

GitHub Copilot Testing запустит итеративный процесс. Copilot проанализирует ваш код и создаст тестовый проект, если его нет. Затем автоматически cгенерирует тесты, соберёт проект и запустит их.

В обозревателе тестов будут отображаться результаты по мере генерации тестов. После завершения генерации тестов GitHub Copilot Testing предоставит сводку в чате Copilot, которая включает в себя:
- Статистику о количестве созданных или изменённых тестов, файлов и проектов.
- Обзор покрытия до и после, чтобы вы могли легко оценить изменения в покрытии.
- Индикаторы качества отобразят состояние «успех/неудача», а также любые пропущенные или нестабильные случаи для отслеживания.
- Если есть пробелы в тестируемости, они будут перечислены вместе с практическими советами.
- Прямые ссылки на тестовые проекты и файлы.

Источник: https://devblogs.microsoft.com/dotnet/github-copilot-testing-for-dotnet/
День 2491. #ЗаметкиНаПолях
Паттерн Идемпотентный Потребитель. Начало

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

Что может пойти не так при публикации
Предположим, сервис публикует событие при создании новой заметки:
await publisher.PublishAsync(
new NoteCreated(note.Id, note.Title, note.Content));

Конкретная реализация издателя или брокера сообщений не важна.

Теперь представьте:
- Издатель отправляет сообщение брокеру.
- Брокер сохраняет его и отправляет ACK.
- Сбой в сети: ACK не доходит до производителя.
- Производитель по тайм-ауту повторяет попытку публикации.
- Теперь у брокера два события NoteCreated.
С точки зрения производителя, он действовал правильно. Но потребитель получил два события о создании одной и той же заметки.

И это только один из путей возникновения сбоя. Вы также можете получить дубликаты из-за:
- Повторных доставок брокером.
- Сбоев потребителя + повторных попыток.
Т.е. даже если вы всё сделали «правильно» на издателе, потребителю всё равно придётся защищаться.

Идемпотентность на стороне издателя (управляет брокер)
Многие брокеры сообщений поддерживают идемпотентную публикацию посредством дедупликации сообщений, если вы укажете уникальный идентификатор сообщения. Например, Azure Service Bus может обнаруживать дубликаты и игнорировать повторные публикации сообщений с тем же идентификатором в течение заданного периода. Amazon SQS и другие брокеры также предлагают аналогичные гарантии.

Не нужно заново изобретать эту логику в вашем приложении. Ключ к успеху — назначить каждому сообщению стабильный идентификатор, уникально отражающий отправляемое логическое событие. Например, при публикации события NoteCreated:
var message = new NoteCreated(
note.Id, note.Title, note.Content)
{
MessageId = Guid.NewGuid() // либо note.Id
};

await publisher.PublishAsync(message);

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

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

Она не обрабатывает повторные попытки потребителя, которые происходят при повторной доставке сообщений или сбое вашего сервиса во время обработки.

С этим поможет шаблон «Идемпотентный потребитель».

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

Источник:
https://www.milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it
👍12
День 2492. #ЗаметкиНаПолях
Паттерн Идемпотентный Потребитель. Продолжение

Начало

Реализация «Идемпотентного потребителя»
Вот пример идемпотентного потребителя события создания заметки NoteCreated:
internal class NoteCreatedConsumer(
DbContext dbContext,
// … кэш, логгер и т.п.
)
: IConsumer<NoteCreated>
{
public async Task
ConsumeAsync(ConsumeContext<NoteCreated> ctx)
{
// Проверяем, получали ли мы это сообщение
if (await dbCtx
.MessageConsumers.AnyAsync(c =>
c.MessageId == ctx.MessageId &&
c.ConsumerName == nameof(NoteCreatedConsumer)))
return;

using var transaction = await
dbCtx.Database.BeginTransactionAsync();

// … сохраняем заметку в базе

// Записываем, что сообщение обработано
dbCtx.MessageConsumers
.Add(new MessageConsumer
{
MessageId = ctx.MessageId,
ConsumerName = nameof(NoteCreatedConsumer),
ConsumedAtUtc = DateTime.UtcNow
});

await dbContext.SaveChangesAsync();
await transaction.CommitAsync();

// … обновляем кэш, пишем в лог
}
}


Важные детали
1. Ключ Идемпотентности
if (await dbCtx
.MessageConsumers.AnyAsync(c =>
c.MessageId == ctx.MessageId &&
c.ConsumerName == nameof(NoteCreatedConsumer)))
return;

Используем:
- MessageId из контекста передачи сообщений (ctx.MessageId);
- ConsumerName (чтобы несколько получателей могли безопасно обрабатывать одно и то же сообщение).
При поступлении дублирующего сообщения выполняется быстрый выход, и ничего не происходит.
Также важно иметь ограничение уникальности (MessageId, ConsumerName) в таблице MessageConsumers для предотвращения гонок. Таким образом, даже при параллельной обработке одного и того же сообщения только одна сможет вставить запись.

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

3. Обработка доставки по принципу «как минимум один раз»
Большинство реалистичных конфигураций выполняются по принципу «как минимум один раз»:
- Потребитель обрабатывает сообщение;
- Сбой подтверждения / тайм-аут;
- Брокер повторно доставляет;
- Ваш код выполняется снова.
При использовании этого шаблона второй запуск обращается к таблице MessageConsumers и завершается раньше времени.

Нет дублирования побочных эффектов.

Это работает, за исключением одного нюанса…

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

Источник:
https://www.milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it
👍10
День 2493. #ЗаметкиНаПолях
Паттерн Идемпотентный Потребитель. Окончание

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

Детерминированные и недетерминированные обработчики
Что произойдёт, когда ваш обработчик вызывает что-то вне базы данных: API, отправка email, платёжный шлюз или очередь фоновых заданий? Всё это распространённые побочные эффекты, которые также должны быть идемпотентными.

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

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

1. Использовать ключ идемпотентности во внешнем вызове
Если внешний сервис это поддерживает, передавайте стабильный идентификатор, например, MessageId сообщения, с каждым запросом. Многие API, включая платёжные системы и платформы email, позволяют указывать заголовок c ключом идемпотентности. Сервис гарантирует, что идентичные запросы с одним и тем же ключом будут выполнены только один раз:
await emailSvc.SendAsync(new EmailRequest
{
To = user.Email,
Subject = "Привет!",
Body = "Спасибо за подписку на канал.",
IdempotencyKey = ctx.MessageId
});

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

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

Компромисс сводится к оценке последствий. Если последствия повторения действия существенны, явно добавьте идемпотентность. В противном случае повтор операции может быть приемлемым.

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

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

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

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

Во всём остальном — чем проще, тем лучше.

Источник:
https://www.milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it
👍13
День 2494. #ЧтоНовенького
Модернизируем .NET-приложения с Copilot
Модернизация приложений — не просто поддержание актуальности версии. Старые фреймворки могут создавать риски безопасности и снижать производительность. Обновление старого приложения .NET не обязательно означает поиск неисправных сборок и непонятных ошибок. Однако для многих разработчиков простое обновление версии оборачивается часами ручных исправлений и борьбы с конфликтами зависимостей.

GitHub Copilot - помощник по модернизации, который проведёт вас через каждый этап, автоматизирует сложную работу и поможет перейти от «это может занять несколько недель» к «сделано за несколько часов».

Предварительные требования
- Visual Studio 2026 (или VS 2022 версии 17.14.17 или новее);
- Лицензия GitHub Copilot (Pro, Pro+, Business или Enterprise);
- Рабочая нагрузка .NET Desktop с включёнными компонентами: GitHub Copilot и GitHub Copilot app modernization.

Обновление приложения .NET
1. Откройте проект или решение
Начните с запуска Visual Studio и откройте свой проект или решение .NET.

2. Начните сеанс агента
- Щёлкните правой кнопкой мыши по проекту или решению в обозревателе решений и выберите Modernize (Модернизировать).
- Либо откройте чат GitHub Copilot и введите @modernize, а затем ваш запрос.

3. Выберите путь
Выберите опцию Upgrade to a newer version of .NET (Обновиться до более новой версии .NET).

4. Оценка и планирование
Copilot оценит ваш код и зависимости, затем:
- Задаст несколько вопросов о ваших целях, чтобы адаптировать план.
- Создаст план обновления в формате Markdown для прозрачности (см. картинку 1).
- Позволит просмотреть и отредактировать план перед дальнейшими действиями.
Вы можете отредактировать план, добавив контекст, изменив порядок этапов или исключив определённые проекты перед утверждением.

5. Применение изменений и устранение ошибок
После утверждения плана Copilot:
- Автоматически обновит файлы, скорректирует импорт и исправит синтаксические ошибки;
- Обработает ошибки сборки в цикле исправления и тестирования для обеспечения стабильности;
- Будет отслеживать ход выполнения в документе «Подробности обновления» для наглядности;
- Будет фиксировать каждый важный шаг в Git для лёгкого отката при необходимости;
- Если Copilot обнаружит проблему, которую не может исправить автоматически, он приостановит работу и запросит действия от вас, сохраняя контроль над процессом.

6. Проверка результатов
После завершения Copilot предоставит:
- Подробный отчёт с хешами коммитов Git для отслеживания;
- Раздел «Следующие шаги» для действий после обновления, таких как обновление конвейеров CI/CD или запуск интеграционных тестов.

Источник: https://devblogs.microsoft.com/dotnet/modernizing-dotnet-with-github-copilot-agent-mode/
👎21👍7
День 2495. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.

10. Управление памятью и сборка мусора
«Объясните роль сборки мусора в .NET и расскажите, как она помогает управлять памятью? С какими потенциальные проблемами могут столкнуться разработчики при сборке мусора, и какие есть способы их решения?»

Хороший ответ
«Сборка мусора (GC) в .NET — это форма автоматического управления памятью. Сборщик мусора управляет освобождением памяти для приложений. При создании экземпляров объектов в .NET среда CLR (Common Language Runtime) выделяет для них память в управляемой куче. По мере создания новых объектов куча заполняется, и память необходимо освобождать. Сборщик мусора автоматизирует этот процесс, периодически определяя объекты, которые больше не используются приложением, и освобождая занимаемую ими память.

Сборщик мусора работает по модели поколений для повышения производительности, разделяя кучу на три поколения:
- 0 - короткоживущие объекты. GC очищает это поколение чаще всего.
- 1 - служит буфером между короткоживущими и долгоживущими объектами.
- 2 - содержит долгоживущие объекты.
Объект изначально помещается в поколение 0. Если он «выжил» при первой сборке мусора, он перемещается в поколение 1, потом – в поколение 2. Идея в том, что объекты, которые используются долгое время, скорее всего продолжат использоваться и далее, поэтому не имеет смысла часто их проверять.

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

Из потенциальных проблем, связанных со сборкой мусора, можно выделить:
1. Задержки
Поскольку сборка мусора недетерминирована, она может происходить в неподходящее время, увеличивая задержку отклика приложения. Разработчики могут снизить задержки, оптимизируя создание и управление объектами, например, повторно используя объекты, где это возможно, или избегая выделений в куче для больших объектов (LOH), сборка которых требует больших затрат.

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

Часто встречающийся некорректный ответ
«Сборка мусора в .NET автоматически очищает все неиспользуемые объекты, поэтому .NET разработчикам не нужно беспокоиться об управлении памятью. GC всё делает за нас, гарантируя, что приложение использует минимум памяти».

Этот ответ демонстрирует ошибочное представление о сборке мусора.

- Чрезмерная уверенность в GC: Хотя GC действительно помогает управлять памятью, разработчикам всё равно следует осознавать, сколько и каких объектов они создают и как управляют памятью, чтобы предотвратить такие проблемы, как утечки памяти и чрезмерное использование памяти.

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

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

Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👍10
День 2496. #SystemDesign101
8 Структур Данных, Использующихся в БД

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

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

1. Skiplist
Вероятностная структура данных, позволяющая в среднем за O(log(n)) времени выполнять операции добавления, удаления и поиска элементов. Распространённый тип индекса в памяти. Используется в Redis.

2. Хэш-индекс
Структура данных, используемая для быстрого поиска точных совпадений в БД, основанная на хэш-таблицах. Работает с помощью хэш-функции, которая преобразует значение ключа в хэш-код (число), а затем используется для определения "бакета" (корзины), где хранится ссылка на нужную запись.

3. SS-таблица
Формат хранения данных в виде неизменяемого файла на диске, содержащего отсортированные по ключам пары "ключ-значение". Данные из временной памяти (Memtable) сбрасываются на диск в виде SS-таблицы, что делает их постоянными и отсортированными для быстрого доступа.

4. LSM-дерево
Skiplist + SSTable. Cтруктура данных, используемая в БД для эффективного хранения и обработки большого количества записей, особенно при частых вставках и удалениях. Новые данные помещаются в отсортированный буфер в оперативной памяти (Memtable), затем периодически сбрасываются на диск в виде SS-таблиц, которые затем объединяются в фоновом режиме.

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

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

7. Суффиксное дерево
Cжатое дерево, представляющее все суффиксы заданной строки. Позволяет быстро решать задачи, связанные с поиском подстрок, такие как поиск вхождений, поиск самых длинных общих подстрок и т.п.

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

Источник: https://blog.bytebytego.com
👍16
День 2497. #ЗаметкиНаПолях
Используем Выражения Коллекций для Пользовательских Типов

В C#12 появились выражения коллекций — новый упрощённый синтаксис для инициализации коллекций:
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];

Этот синтаксис отлично работает со встроенными типами коллекций, но как насчёт наших типов коллекций? Здесь на помощь приходит атрибут CollectionBuilderAttribute, позволяющий расширить этот современный синтаксис на пользовательские типы.

Пользовательские коллекции
Представьте, что вы создали свой тип коллекции:
public class MyCollection<T> 
{
private readonly List<T> _items;

public MyCollection(ReadOnlySpan<T> items) =>
_items = [.. items];

public IEnumerator<T> GetEnumerator()
=> _items.GetEnumerator();

// другие члены…
}

Раньше вы не могли использовать синтаксис выражений коллекций для этого типа. Вам приходилось использовать старый способ:
var myCol = 
new MyCollection<int>(new[] { 1, 2, 3, 4, 5 });

Либо (что не сильно меняет дело):
var myCol = 
new MyCollection<int>([1, 2, 3, 4, 5]);

Не слишком элегантно.

Атрибут CollectionBuilderAttribute устраняет этот разрыв, сообщая компилятору, как конструировать вашу коллекцию из выражения коллекции:
[CollectionBuilder(typeof(MyCollectionBuilder),
nameof(MyCollectionBuilder.Create))]
public class MyCollection<T>
{
//…
}

public static class MyCollectionBuilder
{
public static MyCollection<T>
Create<T>(ReadOnlySpan<T> items)
=> new([..items]);
}

Теперь мы можем использовать:
MyCollection<int> myCol = [1, 2, 3, 4, 5];

Компилятор автоматически вызовет метод Create, передав ему элементы.

Как это работает
Атрибут принимает два параметра:
1. Тип построителя – тип, содержащий фабричный метод (typeof(MyCollectionBuilder));
2. Имя метода – имя статического метода, создающего экземпляр ("Create").

Метод построителя должен:
- быть статическим;
- принимать либо ReadOnlySpan<T> (предпочтительно), либо T[];
- возвращать экземпляр типа коллекции;
- иметь параметры типа, соответствующие вашей коллекции.

Замечание: также необходимо, чтобы коллекция имела «тип итерации», то есть имела метод GetEnumerator(), возвращающий IEnumerator (или IEnumerator<T>). Можно либо реализовать интерфейс IEnumerable или IEnumerable<T>, либо просто добавить метод GetEnumerator().

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

Источник: https://bartwullems.blogspot.com/2025/11/how-to-uninstall-older-net-core-versions.html
👍19
День 2498. #Architecture
Архитектура Вертикальных Срезов. Где Живёт Общая Логика? Начало

Архитектура Вертикальных Срезов (Vertical Slice Architecture) кажется очень удобной, когда вы с ней впервые сталкиваетесь. Не нужно прыгать между семью уровнями, чтобы добавить одно поле, не нужны десятки проектов в решении. Но при реализации более сложных функций начинают проявляться недостатки.

Допустим, у нас есть срезы CreateOrder, UpdateOrder и GetOrder. Внезапно возникает повторение: логика проверки адреса заказа в 3х местах. А подсчёт подытога, налогов и итога нужен как для корзины, так и для оформления заказа. Хочется создать общий проект или папку SharedServices. Это самый критический момент во внедрении VSA. Выберете неправильный вариант, и снова создадите связанность, от которой пытались избавиться. Выберите правильный - сохраните независимость, которой отличается VSA.

Чистая архитектура устанавливает строгие ограничения. Она точно определяет, где находится код:
- сущности - в домене (Domain),
- интерфейсы — в приложении (Application),
- реализации — в инфраструктуре (Infrastructure).
Это безопасно, предотвращает ошибки, но также препятствует использованию обходных путей, даже когда они уместны.

VSA устраняет ограничения. Она гласит: «Организуйте код по функциям, а не по техническим особенностям». Это обеспечивает скорость и гибкость, но перекладывает бремя дисциплины на вас. Что делать?

Ловушка: «Общий» мусорный ящик
Путь наименьшего сопротивления — создать проект (или папку) с именем Shared, Common или Utils. Это почти всегда ошибка.

Представьте проект Common.Services с классом OrderCalculationService:
- метод для итогов корзины (используется Cart),
- для истории доходов (используется Reporting),
- метод для форматирования счетов (используется Invoices).
3 несвязанные задачи. 3 разные причины изменений. Класс, объединяющий всё.

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

Схема принятия решений
В потенциальной ситуации совместного использования кода нужно задать 3 вопроса:

1. Это инфраструктурный или доменный код?
Инфраструктура (контексты БД, ведение журнала, HTTP-клиенты) почти всегда используются из нескольких мест. Концепции домена требуют более тщательного изучения (об этом позже).

2. Насколько стабильна концепция?
Если меняется раз в год – можно кидать в общий код. Если при каждом запросе на новую функцию, оставьте её локальной.

3. Нарушается ли «правило трёх»?
Дублирование кода 1 раз допустимо. Создание 3х копий должно насторожить. Не абстрагируйтесь, пока не достигнете трёх.

Мы решаем эту проблему рефакторингом кода. Далее рассмотрим несколько примеров.

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

Источник:
https://www.milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live
👍12
День 2499. #Architecture
Архитектура Вертикальных Срезов. Где Живёт Общая Логика? Продолжение

Начало

Три уровня общего кода
Вместо бинарного разделения «общий/локальный код» мыслите тремя уровнями.

Уровень 1. Техническая инфраструктура (спокойно используйте общий код)
Чистая инфраструктура, одинаково влияющая на все срезы: логеры, фабрики подключений к БД, промежуточное ПО аутентификации, шаблон Result, конвейеры валидации и т.п. Централизуйте всё это в проекте Shared.Kernel или Infrastructure. Обратите внимание, что это также может быть папка внутри проекта. Такой код редко меняется в связи с бизнес-требованиями.
// Техническое ядро
public readonly record struct Result
{
public bool IsSuccess { get; }
public string Error { get; }

private Result(bool isSuccess, string error)
{
IsSuccess = isSuccess;
Error = error;
}

public static Result Success()
=> new(true, string.Empty);
public static Result Failure(string error)
=> new(false, error);
}


Уровень 2. Концепции домена (используйте общий код)
Вместо того, чтобы разбрасывать бизнес-правила по срезам, передавайте их в сущности и объекты-значения:
// Сущность с бизнес-логикой 
public class Order
{
public Guid Id { get; set; }
public OrderStatus Status { get; set; }
public List<OrderLine> Lines { get; set; }

public bool CanBeCancelled() =>
Status == OrderStatus.Pending;

public Result Cancel()
{
if (!CanBeCancelled())
return Result.Failure("Нельзя отменить подтверждённый заказ.");

Status = OrderStatus.Cancelled;
return Result.Success();
}
}

CancelOrder, GetOrder и UpdateOrder все используют одни бизнес-правила. Логика живёт в одном месте. Т.е. разные вертикальные срезы могут использовать одну и ту же модель домена.

Уровень 3: Логика, специфичная для конкретного объекта (сохраняйте её локально)
Логика, общая для связанных срезов, таких как CreateOrder и UpdateOrder, не обязательно должна быть глобальной. Создайте общую папку (из каждого правила есть исключения) внутри функции:
📂 Features
📂 Orders
📂 CreateOrder
📂 UpdateOrder
📂 GetOrder
📂 Shared
📄 OrderValidator.cs
📄 OrderPricingService.cs

Это также имеет скрытое преимущество. Если вы удалите функцию Orders, общая логика будет удалена вместе с ней. Никакого зомби-кода.

Наконец рассмотрим несколько сложных сценариев, которые большинство упускают из виду.

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

Источник:
https://www.milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live
👍9
День 2500. #Architecture
Архитектура Вертикальных Срезов. Где Живёт Общая Логика? Окончание

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

Совместное использование между функциями
Что насчёт совместного использования кода между несвязанными функциями в VSA? CreateOrder должен проверять наличие клиента. GenerateInvoice должен рассчитывать налог. И Orders и Customers должны форматировать уведомления. Это не совсем вписывается в папку Shared определённой функции. Куда деть эту логику?

1. Действительно ли нужен общий код?
Большинство случаев «совместного использования» между функциями — это просто замаскированный доступ к данным. Если CreateOrder нужны данные о клиентах, он напрямую обращается к БД. Он не обращается к функции Customers. Каждый срез владеет своим доступом к данным. Сущность Customer является общей (находится в домене), но Orders и Customers не имеют общего сервиса.

2. Когда действительно нужна общая логика
- Логика домена (бизнес-правила, калькуляции) → Domain/Services
- Инфраструктура (внешние API, форматирование) → Infrastructure/Services
// Domain/Services/TaxCalculator.cs
public class TaxCalculator
{
public decimal CalculateTax(
Address address, decimal subtotal)
{
var rate = GetTaxRate(address.State, address.Country);
return subtotal * rate;
}
}

CreateOrder и GenerateInvoice могут использовать этот код без привязки друг к другу.

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

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

3. Когда дублирование — правильный выбор
Иногда «общий» код только кажется общим:
// Features/Orders/GetOrder
public record GetOrderResponse(Guid Id, decimal Total, string Status);

// Features/Orders/CreateOrder
public record CreateOrderResponse(Guid Id, decimal Total, string Status);

Они идентичны. Возникает соблазн создать SharedOrderDto. Не поддавайтесь ему.

На следующей неделе в GetOrder понадобится URL трекинга. Но CreateOrder выполняется до отправки, поэтому не имеет URL. Если бы используете общий класс DTO, у вас появится свойство, допускающее NULL, которое в половине случаев остаётся пустым, что сбивает с толку. Дублирование дешевле, чем неправильная абстракция.

Структура Проекта
Вот как может выглядеть структура проекта VSA:
📂 src
📂 Features
│ ├📂 Orders
│ │ ├📂 CreateOrder
│ │ ├📂 UpdateOrder
│ │ └📂 Shared
│ ├📂 Customers
│ │ ├📂 GetCustomer
│ │ └📂 Shared
│ └📂 Invoices
│ └📂 GenerateInvoice
📂 Domain
│ ├📂 Entities
│ ├📂 ValueObjects
│ └📂 Services
📂 Infrastructure
│ ├📂 Persistence
│ └📂 Services
📂 Shared
📂 Behaviors

- Features — Изолированные срезы. Каждый со своими моделями запроса/ответа.
- Features/[Name]/Shared — Локальный общий код между связанными срезами.
- Domain — Сущности, объекты-значения и доменные сервисы. Общая бизнес-логика живёт здесь.
- Infrastructure — Технические вопросы.
- Shared — Только общее поведение.

Правила
1. Функции владеют своими моделями запросов/ответов. Исключений нет.
2. Перемещайте бизнес-логику в домен. Сущности и объекты-значения — лучшее место для совместного использования бизнес-правил.
3. Сохраняйте совместное использование семейств функций локальным. Если это нужно только срезам Order, храните его в Features/Orders/Shared (не стесняйтесь найти более подходящее название, чем Shared).
4. Инфраструктура по умолчанию является общей. Контексты БД, HTTP-клиенты, ведение журнала. Это технические вопросы.
5. Применяйте правило трёх. Не извлекайте данные, пока не найдёте 3 реальных использования идентичной, стабильной логики.

Источник: https://www.milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live
👍16
День 2501.
Сегодня порекомендую видео от моего любимого докладчика Дилана Бити. Недавно на конференции NDC в Копенгагене он выступал с докладом “Algorithms Demystified”.

Бывало ли у вас, что вы застревали на какой-то проблеме с кодом? Возможно, реализовывали какую-то функцию в одном из своих проектов, или решали задачи на LeetCode или Advent of Code, и застряли. Вы не можете понять, как получить нужный результат. Поэтому спрашиваете у коллег, на Stack Overflow или Reddit, и получаете ответ вроде: «Да тут просто надо использовать алгоритм Дейкстры»… и ваш мозг зависает. Использовать что? Вы гуглите и обнаруживаете, что это «поиск кратчайших путей между узлами во взвешенном графе», и теперь нужно узнать, что такое «узел» и что такое «взвешенный граф»… а затем понять, как превратить всё это в работающий код.

Алгоритмы — ключ ко всем видам функций и систем, от сетей до автокоррекции, и понимание того, как они работают, поможет вам создавать более качественное программное обеспечение, исправлять едва заметные ошибки и решать задачи на Advent of Code. В этом докладе Дилан расскажет о некоторых из его любимых алгоритмов, объяснит их важность и поможет вам понять, что они делают и как.
👍11