أحدث المنشورات من .NET Разработчик (@netdeveloperdiary) على Telegram

منشورات .NET Разработчик على Telegram

.NET Разработчик
Дневник сертифицированного .NET разработчика.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
6,233 مشترك
395 صورة
2 فيديو
آخر تحديث 11.03.2025 03:38

قنوات مشابهة

Ozon Tech
21,871 مشترك
C#/.Net Job Offers
3,459 مشترك
📚 .NET Книги
1,876 مشترك

أحدث المحتوى الذي تم مشاركته بواسطة .NET Разработчик على Telegram

.NET Разработчик

28 Feb, 05:01

1,772

День 2221. #ЗаметкиНаПолях
Как Компилятор Выводит Тип из default?

Ключевое слово default — мощный инструмент в C#. Для ссылочных типов он возвращает null, а для типов значений (структур) — обнулённое значение. Интересно, что default(T) и new T() могут давать разные результаты для структур. Изначально C# требовал default(T), но теперь вы можете просто использовать default, когда тип может быть выведен компилятором. Но как компилятор выводит тип? И всегда ли вы можете доверять, что он сделает это правильно?

Давайте рассмотрим два простых случая:
// Выведение типа из левой части 
int foo = default;

// Выведение типа из типа параметра
Foo(default);
void Foo(int bar) => throw null;


Предыдущие случаи довольно очевидны, но что будет с выражением switch?
var sample = new byte[0];
ReadOnlyMemory<byte> foo = sample switch
{
byte[] value => value,
_ => default
};

Вы можете ожидать, что default будет default(ReadOnlyMemory<byte>), так как это видимый тип в левой части. Однако на самом деле это будет default(byte[]). Компилятор выводит тип из ветвей выражения switch. В этом случае компилятор определит наилучший общий тип для всех случаев, которым в данном случае является byte[]. Следовательно, типом default будет byte[].

В предыдущем примере результат foo не поменяется, если вы используете default(byte[]) или default(ReadOnlyMemory<byte>). Но это может иметь некоторые последствия. Давайте изменим тип с ReadOnlyMemory<byte> на ReadOnlyMemory<byte>? (обнуляемый).
var sample = new object();
ReadOnlyMemory<byte>? foo = sample switch
{
byte[] value => value,
_ => default
};

В этом примере типом default всё ещё будет default(byte[]). Поэтому выражение switch вернёт default(byte[]), что является null. Однако, затем значение будет преобразовано в ReadOnlyMemory<byte>?. А у ReadOnlyMemory<T> есть оператор неявного преобразования из массива:
// из исходного кода .NET 9
public static implicit operator
ReadOnlyMemory<T>(T[]? array)
=> new ReadOnlyMemory<T>(array);

В свою очередь, конструктор ReadOnlyMemory<T>, принимающий обнуляемый массив, создаёт пустой ReadOnlyMemory<T>, если получает null:
// из исходного кода .NET 9
public ReadOnlyMemory(T[]? array)
{
if (array == null)
{
this = default;
return; // returns default
}

}

Таким образом, на выходе мы получим пустой ReadOnlyMemory<byte>, и foo.HasValue будет true!

А если мы явно укажем обнуляемый тип в default:
object sample = new();

ReadOnlyMemory<byte>? foo = sample switch
{
byte[] value => value,
_ => default(ReadOnlyMemory<byte>?)
};

Тогда закономерно получим null в выражении switch, и foo.HasValue будет false.

Источник: https://www.meziantou.net/how-does-the-compiler-infer-the-type-of-default.htm
.NET Разработчик

27 Feb, 05:04

2,012

День 2220. #ЗаметкиНаПолях
Правильно Имитируем Состояние Гонки в C#. Окончание

Начало

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

2. Добавление искусственных задержек, таких как Thread.Sleep(1) внутри метода Withdraw, увеличивает вероятность возникновения состояния гонки каждый раз (см. полный пример в предыдущем посте https://t.me/NetDeveloperDiary/2676):
public void Withdraw(int amount)
{
if (_balance > 0)
{
Thread.Sleep(1);
_balance -= amount;
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} – Баланс после: {_balance}");
}
}

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

Thread.Sleep(1) заставляет операционную систему приостановить выполнение текущего потока и разрешить другим потокам работать. Это прерывание происходит после проверки баланса, но до его обновления, создавая окно, в котором другой поток может войти в метод, прочитать тот же (устаревший) баланс и продолжить своё выполнение. Т.к. оба потока видят один и тот же начальный баланс до того, как какой-либо из них его обновит, они выполняют вычисления на основе устаревших данных, что приводит к неверным конечным значениям.
Без Thread.Sleep состояние гонки может возникнуть непредсказуемо из-за загрузки системы и планирования потоков, но после его добавления проблема последовательно воспроизводится, что упрощает наблюдение и отладку.

3. Также мы можем выстроить потоки в точке гонки и использовать семафор, чтобы освободить их все одновременно. Этот метод гарантирует, что несколько потоков войдут в критическую секцию вместе, естественным образом увеличивая конкуренцию:
var acc = new BankAccount(1000);
int threadCount = 15;
var threads = new Thread[threadCount];
// изначально семафор закрыт
var semaphore = new SemaphoreSlim(0);

for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(() =>
{
semaphore.Wait();
acc.Withdraw(100);
});
threads[i].Start();
}

// ждём старта всех потоков
Thread.Sleep(1000);

// открываем семафор
semaphore.Release(threadCount);

foreach (var thread in threads)
thread.Join();

Console.WriteLine($"Итого: {acc.GetBalance()} (Ожидается: 0 или <0 при состоянии гонки)");


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

В этом конкретном примере операция уменьшения (_balance -= amount;) выполняется так быстро, что вероятность чередования потоков крайне мала. Поэтому без искусственных задержек (Thread.Sleep) или дополнительных шагов чтения-изменения-записи перед вычитанием, большинство ЦП будут выполнять каждое изъятие последовательно, а не параллельно.

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

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

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

Источник: https://dev.to/thecodewrapper/how-to-properly-simulate-a-race-condition-in-c-37o8
.NET Разработчик

26 Feb, 05:00

2,109

День 2219. #ЗаметкиНаПолях
Правильно Имитируем Состояние Гонки в C#
Состояния гонки являются одной из самых сложных и неуловимых ошибок в многопоточном программировании. Они возникают, когда несколько потоков одновременно получают доступ к общим данным и изменяют их, что приводит к непредсказуемым результатам. Рассмотрим, как намеренно создать состояние гонки и проанализируем, почему это происходит.

Вот упрощённый пример транзакций по банковскому счёту. В этом примере несколько пользователей одновременно пытаются снять деньги. Если возникает состояние гонки, мы можем увидеть неправильный конечный баланс, например отрицательное значение, указывающее на то, что несколько потоков сняли деньги одновременно без надлежащей синхронизации. Если состояния гонки не возникнет, мы ожидаем, что конечный баланс будет равен 0 после завершения всех снятий:
var acc = new BankAccount(1000);
int threadCount = 15;
var threads = new Thread[threadCount];

for (int i = 0; i < threadCount; i++)
{
threads[i] = new Thread(() => acc.Withdraw(100));
threads[i].Start();
}

foreach (var thread in threads)
thread.Join();

Console.WriteLine($"Итого: {acc.GetBalance()} (Ожидается: 0 или <0 при состоянии гонки)");


class BankAccount(int initial)
{
private int _balance = initial;

public int GetBalance() => _balance;

public void Withdraw(int amount)
{
if (_balance > 0)
_balance -= amount;
}
}

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

Гарантировано ли в этом случае возникновение состояния гонки? Нет, из-за планирования потоков, выделения ресурсов ЦП и оптимизации памяти.

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

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

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

Очевидно, это означает, что хотя состояния гонки могут возникать, они не гарантируются каждый раз при запуске программы.

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

Источник:
https://dev.to/thecodewrapper/how-to-properly-simulate-a-race-condition-in-c-37o8
.NET Разработчик

25 Feb, 05:02

2,184

День 2218. #ЗаметкиНаПолях
Замечаем Забытые Миграции в Entity Framework Core
Entity Framework Core позволяет обновлять схему базы данных с помощью миграций. Миграции чаще всего создаются вручную путём запуска команды в консоли. Легко забыть создать новую миграцию при изменении модели. Чтобы убедиться, что миграции актуальны, можно написать тест, который сравнивает текущую модель с моделью снимка:
[Fact]
public async Task EnsureMigrationsAreUpToDate()
{
await using var dbCtx = new SampleDbCtx();

// Получаем нужные сервисы из контекста
var mmd = dbCtx.GetService<IMigrationsModelDiffer>();
var ma = dbCtx.GetService<IMigrationsAssembly>();
var mri = dbCtx.GetService<IModelRuntimeInitializer>();
var dtm = dbCtx.GetService<IDesignTimeModel>();

// Текущая модель
var model = dtm.Model;

// Модель снимка БД
var snapshot = ma.ModelSnapshot?.Model;
if (snapshot is IMutableModel mm)
{
// Выполняем пост-процессинг модели,
// чтобы она была готова к использованию
snapshot = mm.FinalizeModel();
}

if (snapshot is not null)
{
// Проверяем и инициализируем
// модель с зависимостями
snapshot =
mri.Initialize(snapshot);
}

// Находим различия в моделях
var diff = mmd.GetDifferences(
source: snapshot?.GetRelationalModel(),
target: model.GetRelationalModel());

// Коллекция различий должна быть пустой
Assert.Empty(diff);
}


В этом упрощённом примере мы создали контекст БД просто через new. В реальном проекте нужно будет привязать контекст к реальной базе. Как вариант, можно использовать следующий код:
var services = new ServiceCollection();
services.AddDbContext<SampleDbCtx>(
o => o.UseSqlServer(…);

using var svcProvider = services.BuildServiceProvider();
using var scope =
svcProvider
.GetRequiredService<IServiceScopeFactory>()
.CreateScope();

var dbCtx = scope
.ServiceProvider
.GetService<SampleDbCtx>();


Источник: https://www.meziantou.net/detect-missing-migrations-in-entity-framework-core.htm
.NET Разработчик

24 Feb, 05:05

2,307

День 2217.
Всё-таки размещу здесь свой доклад на конференции DotNext 2024. Его недавно выложили на Youtube. Думаю, что большинство из интересующихся конференцией его уже видели, поскольку он был в открытой части. Но для тех, кто ещё не успел, в сентябре 2024 года я рассказал о том, что готовилось к выходу в .NET 9. Подавляющее большинство из этого действительно вышло (хотя, некоторые части не сразу в ноябре, а чуть позже или в виде превью функциональности).

Ну и обо всём этом я писал здесь на канале, можете поискать по хештегу #ЧтоНовенького

https://youtu.be/t-Sp-iEkqk0
.NET Разработчик

23 Feb, 04:59

2,377

День 2216. #УрокиРазработки
Уроки 50 Лет Разработки ПО

Урок 42. Никакие инженерные или управленческие приёмы не дадут эффекта, если вы имеете дело с неразумными людьми

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

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

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

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

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

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

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

Источник: Карл Вигерс “Жемчужины Разработки”. СПб.: Питер, 2024. Глава 5.
.NET Разработчик

22 Feb, 05:04

2,170

День 2215. #ЗаметкиНаПолях
Создаём Надёжных Клиентов API с Refit. Окончание

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

Обработка HTTP-ответов
Хотя Refit по умолчанию автоматически десериализует ответы в определённые вами типы, бывают случаи, когда требуется больше контроля над HTTP-ответом. Refit предоставляет два варианта для этих сценариев: HttpResponseMessage и ApiResponse<T>. Обновим IBlogApi для использования этих типов:
public interface IBlogApi
{
[Get("/posts/{id}")]
Task<HttpResponseMessage> GetPostRawAsync(int id);

[Get("/posts/{id}")]
Task<ApiResponse<Post>>
GetPostWithMetadataAsync(int id);

[Post("/posts")]
Task<ApiResponse<Post>>
CreatePostAsync([Body] Post post);
}


Рассмотрим, как их использовать.

1. HttpResponseMessage
HttpResponseMessage resp = 
await blogApi.GetPostRawAsync(1);

if (resp.IsSuccessStatusCode)
{
var content = await resp.Content
.ReadAsStringAsync();
var post = JsonSerializer
.Deserialize<Post>(content);
// …
}
else
{
Console.WriteLine($"Error: {resp.StatusCode}");
}

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

2. ApiResponse<T>
Специфичный для Refit тип, который оборачивает десериализованный контент и метаданные ответа:
var newPost = new Post("New Post", "Content", 1);

ApiResponse<Post> resp = await
blogApi.CreatePostAsync(newPost);

if (resp.IsSuccessStatusCode)
{
var post = resp.Content;
var location = resp.Headers.Location;
Console.WriteLine($"ID: {post.Id}");
Console.WriteLine($"Location: {location}");
}
else
{
Console.WriteLine($"Error: {resp.Error.Content}");
Console.WriteLine($"Status: {resp.StatusCode}");
}

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

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

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

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

Источник: https://www.milanjovanovic.tech/blog/refit-in-dotnet-building-robust-api-clients-in-csharp
.NET Разработчик

21 Feb, 05:04

2,035

День 2214. #ЗаметкиНаПолях
Создаём Надёжных Клиентов API с Refit. Продолжение

Начало

Параметры запроса
При работе с API часто требуется отправлять данные как часть URL либо в маршруте, либо в строке запроса. Refit делает этот процесс простым и типобезопасным. Добавим в IBlogApi пару более сложных сценариев:
public interface IBlogApi
{
// …

[Get("/posts")]
Task<List<Post>> GetPostsAsync(
[Query] Filters filters);

[Get("/users/{userId}/posts")]
Task<List<Post>> GetUserPostsAsync(int userId);
}

public record Filters(int? UserId, string? Title);

Здесь:
- GetPostsAsync использует объект для представления фильтров запроса. Это подходит для конечных точек со множеством необязательных параметров. Refit автоматически преобразует этот объект в строку запроса. Использование объекта для фильтров запроса делает код типобезопасным и удобным для рефакторинга. Если нужно добавить новый фильтр запроса, просто добавьте свойство в Filters.
- GetUserPostsAsync демонстрирует передачу параметров в маршрут.

Динамические заголовки и аутентификация
Ещё одним распространённым требованием при интеграции с API является включение пользовательских заголовков или токенов аутентификации в запросы. Refit предоставляет несколько способов сделать это: от простых статических заголовков до динамической аутентификации, специфичной для запроса.
public interface IBlogApi
{
[Headers("User-Agent: MyAwesomeApp/1.0")]
[Get("/posts")]
Task<List<Post>> GetPostsAsync();

[Get("/secure-posts")]
Task<List<Post>> GetSecurePostsAsync(
[Header("Authorization")] string bearerToken);

[Get("/user-posts")]
Task<List<Post>> GetUserPostsAsync(
[Authorize(scheme: "Bearer")] string token);
}

- можно добавить статический заголовок ко всем запросам, используя атрибут метода Headers;
- с помощью атрибута параметра Header можно передавать значение заголовка в качестве параметра;
- атрибут параметра Authorize — удобный способ добавить аутентификацию через токен.

А если нужно добавить один и тот же динамический заголовок ко всем запросам, можно использовать DelegatingHandler.

Настройки JSON-сериализации
Refit дает гибкость при выборе и настройке JSON-сериализации. По умолчанию он использует System.Text.Json, но можно легко переключиться на Newtonsoft.Json, установив NuGet-пакет Refit.Newtonsoft.Json и настроив клиента:
builder.Services.AddRefitClient<IBlogApi>(
new RefitSettings {
ContentSerializer =
new NewtonsoftJsonContentSerializer(
new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
})
})
.ConfigureHttpClient(…);

Здесь мы настроили camelcase для имён свойств и игноририрование нулевых значений при сериализации.
System.Text.Json быстрее и использует меньше памяти, что делает его отличным выбором по умолчанию. Однако Newtonsoft.Json предлагает больше функций и может быть необходим для совместимости со старыми системами или для особых потребностей сериализации.

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

Источник:
https://www.milanjovanovic.tech/blog/refit-in-dotnet-building-robust-api-clients-in-csharp
.NET Разработчик

20 Feb, 05:02

2,129

День 2213. #ЗаметкиНаПолях
Создаём Надёжных Клиентов API с Refit. Начало

Работа с внешними API - важная часть современной разработки ПО, но это может быть больно. Мы все боролись с настройками HttpClient, писали повторяющийся код и надеялись, что не пропустили где-то параметр или заголовок. Refit значительно упрощает нам жизнь. Он обрабатывает всю тяжёлую логику работы с HTTP, позволяя сосредоточиться на том, что важно: логике приложения.

Что это?
Refit — это типобезопасная библиотека REST для .NET. Она позволяет определить API как интерфейс, который Refit затем реализует для вас. Такой подход сокращает шаблонный код и делает вызовы API более читаемыми и поддерживаемыми. Вы описываете конечные точки API с помощью сигнатур методов и атрибутов, а Refit заботится обо всём остальном:
1. Автоматическая сериализация и десериализация.
Вам не придётся преобразовывать объекты в JSON и обратно. Refit делает это за вас.
2. Строго типизированные определения API.
Refit помогает выявлять ошибки на ранних этапах. Если вы неправильно введёте параметр или используете неправильный тип данных, вы узнаете об этом во время компиляции, а не когда ваше приложение даст сбой при работе.
3. Поддержка различных методов HTTP: GET, POST, PUT, PATCH, DELETE.
4. Манипуляции с запросами/ответами.
Вы можете легко добавлять пользовательские заголовки или обрабатывать определённые типы контента.
Кроме того, вызовы API становятся самодокументируемыми. Любой, кто читает код, сможет быстро понять, что делает каждый метод, не углубляясь в детали реализации.

Настройка и использование
Мы реализуем полный интерфейс CRUD и продемонстрируем его использование в приложении Minimal API. Сначала установим необходимые NuGet-пакеты:
Install-Package Refit
Install-Package Refit.HttpClientFactory


Теперь создадим интерфейс Refit:
using Refit;

public interface IBlogApi
{
[Get("/posts/{id}")]
Task<Post> GetPostAsync(int id);

[Get("/posts")]
Task<List<Post>> GetPostsAsync();

[Post("/posts")]
Task<Post> CreatePostAsync([Body] Post post);

[Put("/posts/{id}")]
Task<Post> UpdatePostAsync(int id, [Body] Post post);

[Delete("/posts/{id}")]
Task DeletePostAsync(int id);
}

public record Post(int Id, string Title, string Body, int UserId);

Мы определяем интерфейс IBlogApi с методами для CRUD-операций. Запись Post представляет сообщения в блоге.

Затем зарегистрируем Refit в DI-контейнере:
using Refit;

builder.Services
.AddRefitClient<IBlogApi>()
.ConfigureHttpClient(c => c.BaseAddress =
new Uri("https://jsonplaceholder.typicode.com"));


Наконец мы можем использовать IBlogApi в конечных точках Minimal API:
app.MapGet("/posts/{id}",
async (int id, IBlogApi api) =>
await api.GetPostAsync(id));

app.MapGet("/posts",
async (IBlogApi api) =>
await api.GetPostsAsync());

app.MapPost("/posts",
async ([FromBody] Post post, IBlogApi api) =>
await api.CreatePostAsync(post));

app.MapPut("/posts/{id}",
async (int id, [FromBody] Post post, IBlogApi api) =>
await api.UpdatePostAsync(id, post));

app.MapDelete("/posts/{id}",
async (int id, IBlogApi api) =>
await api.DeletePostAsync(id));


Мы создали полностью функциональный API, который взаимодействует с внешним сервисом, всего в нескольких строках кода. Никаких ручных HTTP-запросов, никакой обработки сырого JSON — Refit позаботится обо всём этом за нас.

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

Источник:
https://www.milanjovanovic.tech/blog/refit-in-dotnet-building-robust-api-clients-in-csharp
.NET Разработчик

19 Feb, 04:59

2,145

День 2212. #ЧтоНовенького
Обновления HTTP-файлов в Visual Studio. Окончание

Начало

Переменные среды
Чтобы задать переменным разные значения в разных средах, создайте файл http-client.env.json в том же каталоге, что и http-файл, или в одном из его родительских каталогов:
{
"dev": {
"HostAddress": "https://localhost:44320"
},
"remote": {
"HostAddress": "https://contoso.com"
}
}

Это файл JSON, содержащий одну или несколько именованных сред, например dev и remote. Каждая именованная среда содержит одну или несколько переменных, например HostAddress. На переменные из файла среды можно ссылаться так же, как и на другие переменные:
GET {{HostAddress}}/api/search/tool

Среду можно выбрать в заголовке окна http-файла в Visual Studio.
Если в http-файле определена переменная с тем же именем, она переопределит значения из файла переменных среды.

Среда $shared
Вы можете объявить переменную, которая будет доступна для всех сред. Для этого и предназначена новая среда $shared. Если вы создаёте среду с именем $shared, переменные будут доступны в любой среде. Если значение, также определено в именованной среде, оно переопределит значение из среды $shared:
{
"$shared": {
"message": "Default msg",
"username": "httpfile-user",
"hosturl": "http://example.com/api/sample"
},
"dev": {
"hosturl": "http://localhost:5000/api/sample"
},
"prod": {
"message": "Msg from prod"
}
}

Здесь в среде dev значение hosturl будет переопределено на localhost, а в среде prod будет своё значение message.

Источники:
-
https://devblogs.microsoft.com/visualstudio/http-file-updates-for-request-variables-and-more/
-
https://learn.microsoft.com/en-us/aspnet/core/test/http-files?view=aspnetcore-9.0#environment-files