C# Portal | Программирование – Telegram
C# Portal | Программирование
14.9K subscribers
965 photos
117 videos
24 files
807 links
Присоединяйтесь к нашему каналу и погрузитесь в мир для C#-разработчика

Связь: @devmangx

РКН: https://clck.ru/3FocB6
Download Telegram
Тестирование текущего времени с помощью TimeProvider и FakeTimeProvider

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

В этом посте разберем класс TimeProvider, как его использовать и как подменять в тестах.

Раньше: ручной интерфейс

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

public interface IDateTimeWrapper
{
DateTime GetCurrentDate();
}


И его стандартную реализацию, которая возвращает время в UTC:

public class DateTimeWrapper : IDateTimeWrapper
{
public DateTime GetCurrentDate()
=> DateTime.UtcNow;
}


Либо похожий вариант через абстрактный класс:

public abstract class DateTimeWrapper
{
public virtual DateTime GetCurrentDate() => DateTime.UctNow;
}


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

Сейчас: класс TimeProvider

Начиная с .NET 8, команда .NET добавила абстрактный класс TimeProvider. Он не только даёт абстракцию над локальным временем, но и предоставляет методы для работы с высокоточными временными метками и часовыми поясами.

Важно: даты возвращаются в виде DateTimeOffset, а не DateTime.

TimeProvider доступен из коробки в консольных приложениях .NET:

DateTimeOffset utc = TimeProvider.System.GetUtcNow();
Console.WriteLine(utc);

DateTimeOffset local = TimeProvider.System.GetLocalNow();
Console.WriteLine(local);


Если вы используете dependency injection, его достаточно зарегистрировать как синглтон:

builder.Services.AddSingleton(TimeProvider.System);


Пример использования:

public class Vacation(TimeProvider _time)
{
public bool IsVacation
=> _time.GetLocalNow().Month == 8;
}


Тестирование TimeProvider

Для тестов можно использовать NuGet-пакет Microsoft.Extensions.TimeProvider.Testing, в котором есть класс FakeTimeProvider. Это заглушка для абстрактного TimeProvider.

С помощью FakeTimeProvider можно вручную задавать текущее UTC-время, локальное время и другие параметры, которые предоставляет TimeProvider:

[Fact]
public void WhenItsAugust_ShouldReturnTrue()
{
// Arrange
var fakeTime = new FakeTimeProvider();
fakeTime.SetUtcNow(
new DateTimeOffset(2025, 8, 14,
22, 24, 12, TimeSpan.Zero));
var sut = new Vacation(fakeTime);

Assert.True(sut.IsVacation);
}


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

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍93
Настоятельно рекомендую фри PDF-книгу:

Algorithms от Jeff Erickson — с кучей наглядных иллюстраций и упражнений.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8😁1
Создание параметризованных тестов в xUnit

xUnit использует атрибуты для объявления тестовых методов.
Атрибут Fact описывает обычный тест, а Theory — параметризованный.

Допустим, у нас есть тест, который проверяет, что email-парсер корректно извлекает домен:

public void Should_Return_Domain(
string email, string expectedDomain)
{
var parser = new EmailParser();
var domain = parser.GetDomain(email);
Assert.Equal(domain, expectedDomain);
}


Рассмотрим четыре способа написания параметризованных тестов.

1. InlineData

Самый простой вариант — передать тестовые данные напрямую через атрибут:

[Theory]
[InlineData("test@test.com", "test.com")]
[InlineData("user@github.io", "github.io")]
public void Should_Return_Domain(
string email, string expectedDomain)
{ … }


В InlineData мы передаём значения аргументов тестируемого метода и ожидаемый результат. Атрибут можно указывать сколько угодно раз — по одному на каждый тестовый кейс.

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

2. MemberData

MemberData позволяет программно задавать тестовые данные через статическое свойство или метод:

[Theory]
[MemberData(nameof(EmailTestData))]
public void Should_Return_Domain(
string email, string expectedDomain)
{ … }

public static IEnumerable<object[]>
EmailTestData => new List<object>
{
new object[] { "test@test.com", "test.com" },
new object[] { "user@github.io", "github.io" }
};


В атрибуте MemberData указывается имя члена. Ограничение здесь одно — свойство или метод должны возвращать IEnumerable<object[]>, из-за чего строгая типизация отсутствует.

3. ClassData

ClassData выносит тестовые данные в отдельный класс. Это удобно для структурирования данных и повторного использования.

Тестовые данные загружаются из класса, который реализует IEnumerable<object[]>:

[Theory]
[ClassData(typeof(EmailTestData))]
public void Should_Return_Domain(
string email, string expectedDomain)
{ … }

public class EmailTestData : IEnumerable<object[]>
{
public IEnumerable<object[]> GetEnumerator()
{
yield return new object[] {
"test@test.com", "test.com" };
yield return new object[] {
"user@github.io", "github.io" };
}

IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
}


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

4. TheoryData

TheoryData позволяет хранить тестовые данные с сохранением строгой типизации.

Пример с использованием ClassData (аналогично можно применять и с MemberData, возвращая TheoryData из свойства или метода):

[Theory]
[ClassData(typeof(EmailTestData))]
public void Should_Return_Domain(
string email, string expectedDomain)
{ … }

public class EmailTestData :
TheoryData<string, string>
{
public EmailTestData()
{
Add("test@test.com", "test.com");
Add("user@github.io", "github.io");
}
}


TheoryData — это обобщённый класс, в котором явно задаются типы параметров теста.
Для добавления одного тестового кейса вызывается Add, а новые сценарии просто добавляются дополнительными вызовами этого метода.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥31👍1
This media is not supported in your browser
VIEW IN TELEGRAM
Это аккуратный и чистый способ работать с Git из терминала

Froggit — минималистичный терминальный UI для Git. Он помогает стейджить файлы, делать коммиты, управлять ветками и смотреть логи без необходимости заучивать сложные команды.

Всё управляется с клавиатуры, работает быстро и без лишних отвлекающих элементов, поэтому проще оставаться в потоке при работе с Git.

Тестим и оцениваем 😁

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4👏2
Разбираем генераторы исходного кода.

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

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

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

1. Инкрементные генераторы

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

2. Анализ зависимостей исходного кода

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

3. Улучшенная диагностика

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

4. Улучшения API Roslyn

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

Простой пример генератора кода

1. Настройка проекта

Создадим проект библиотеки и установим необходимые NuGet-пакеты:

dotnet new classlib -n MySourceGenerator
cd MySourceGenerator
dotnet add package Microsoft.CodeAnalysis.CSharp


2. Генератор кода

Нам понадобится класс, реализующий интерфейс IIncrementalGenerator:

using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

[Generator]
public class HelloWorldGenerator : IIncrementalGenerator
{
public void Initialize(
IncrementalGeneratorInitializationContext context)
{
var src =
"""
using System;
namespace HelloGenerated;
public static class HelloWorld
{
public static void SayHello()
=> Console.WriteLine("Hello from the generated code!");
}
""";

context.RegisterPostInitializationOutput(
ctx => ctx.AddSource(
"HelloWorldGenerated",
SourceText.From(src, Encoding.UTF8)));
}
}


Этот генератор добавляет в проект класс HelloWorld с методом SayHello. Сгенерированный код компилируется вместе с основным проектом, поэтому метод HelloGenerated.HelloWorld.SayHello() можно вызывать напрямую из вашего кода.

3. Интеграция с проектом

Создадим консольное приложение:

dotnet new console -n UseSourceGenerator


Добавим ссылку на проект генератора:

<ProjectReference
Include="..\MySourceGenerator\MySourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />


Обратите внимание на два нестандартных атрибута:

OutputItemType="Analyzer" — указывает, что проект должен использоваться как анализатор;

ReferenceOutputAssembly="false" — гарантирует, что целевой проект не будет ссылаться на DLL генератора во время компиляции.

После этого сгенерированный класс HelloWorld станет доступен в консольном проекте.

4. Сборка и запуск

Соберём проект и запустим его. В консоли появится вывод из сгенерированного метода:

// Program.cs
HelloGenerated.HelloWorld.SayHello();


В след. посте про чтение кода из файла 😌

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
8👍1🤔1
This media is not supported in your browser
VIEW IN TELEGRAM
Это рай для программистов: репозиторий с сотнями бесплатных API.

Отлично подходит для практики и прокачки навыков программирования

Забираем здесь 🥶

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
Знаешь, что в .NET 10 сильно снизили накладные расходы при работе с любыми встроенными коллекциями через интерфейсы (да, не только с массивами)?

Итерация по списку через IList<T> или по словарю через IDictionary<K, V> и т.п. теперь почти не даёт абстрактного оверхеда.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11🤯11🔥42
Держи инструмент для запросов к данным, если уже надоели классические клиенты для БД: agx

Внутри есть умный SQL-редактор с подсветкой синтаксиса и помощью LLM при написании запросов, браузер схем для быстрого просмотра структуры данных, а также импорт данных из файлов через drag-and-drop.

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

🛌

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
Разбираем Server-Sent Events в ASP.NET Core и .NET 10.

Обновления UI в реальном времени больше не считаются просто желательной фичей. Большинство современных приложений ожидают поток данных в реальном времени от сервера. Долгие годы основным решением в экосистеме .NET был SignalR. Он действительно очень мощный, но для более простых сценариев приятно иметь альтернативы.

В ASP.NET Core 10 появился собственный высокоуровневый API для Server-Sent Events (SSE). Он закрывает разрыв между примитивным HTTP-поллингом и полнодуплексными WebSockets через SignalR.

Зачем?

SignalR — инструмент, который автоматически работает с WebSockets, Long Polling и SSE, предоставляя полнодуплексный (двусторонний) канал связи. Но за это приходится платить: свой протокол, обязательная клиентская библиотека и необходимость в «липких сессиях» или отдельном бэкенде (например, Redis) для масштабирования.

В отличие от SignalR, SSE:

- Однонаправленные - разработаны специально для потоковой передачи данных с сервера на клиент.
- Нативны для HTTP - это стандартный HTTP-запрос с типом содержимого text/event-stream. Никаких пользовательских протоколов.
- Поддерживают автоматическое переподключение - браузеры обрабатывают переподключения напрямую через API EventSource.
- Легковесны - нет тяжёлых клиентских библиотек или сложной логики рукопожатия.

Простейшая SSE-конечная точка

Мы можем использовать новый объект Results.ServerSentEvents, чтобы вернуть поток событий из любого IAsyncEnumerable<T>. Поскольку IAsyncEnumerable представляет поток данных, который может приходить со временем, сервер понимает, что HTTP-соединение нужно держать открытым, а не закрывать его после первого куска данных.

Минимальный пример SSE-эндпоинта, который в реальном времени передаёт события о размещении заказов:

app.MapGet("orders/realtime", (
ChannelReader<OrderPlacement> reader,
CancellationToken ct) =>
{
// ReadAllAsync возвращает IAsyncEnumerable
// Results.ServerSentEvents заставляет браузер держать соединение открытым
// Новые данные отправляются клиенту по мере поступления из канала
return Results.ServerSentEvents(
reader.ReadAllAsync(ct),
eventType: "orders");
});


Когда клиент обращается к этой конечной точке:

Сервер отправляет заголовок Content-Type: text/event-stream.

Соединение остаётся открытым и ждёт данные.

Как только приложение пишет новый заказ в канал, IAsyncEnumerable возвращает элемент, и .NET сразу отправляет его в браузер по открытому HTTP-соединению.

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

В примере используется Channel. В реальном приложении это может быть фоновый сервис, который слушает очередь сообщений (например, RabbitMQ или Azure Service Bus) или поток изменений БД и прокидывает новые события в канал для доставки подключённым клиентам.

В след. посте поговорим про обработку пропущенных событий 👍

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥15👍6👏21
📱 Держите 6 хороших каналов по искусственному интеллекту и программированию для любого уровня!

Выбирай направление:

📱 Нейросети@neuro_prompt

🤖 AI-инструменты @ai_prompt

📱 Python@python_prompt

🤔 InfoSec & Хакинг @infosec_prompt

👩‍💻 IT Новости @it_news

😄 IT Мемы@it_memes

Промпты, обучение, шпаргалки и полезные ресурсы на каждую тему!
Please open Telegram to view this post
VIEW IN TELEGRAM
2👎1🥴1
EF Core наконец поправил проблему, вокруг которой мы делали костыли годами.

Фикс завезли в EF 10

В EF Core 10 глобальные query filters переделали в named query filters.

Global query filters в Entity Framework Core — это мощная фича для управления паттернами доступа к данным.

Глобальные query filters — это предикаты LINQ, применяемые к entity-моделям EF Core.

Фильтры автоматически накладываются на все запросы, где участвуют соответствующие сущности. Особенно полезно для multi-tenant приложений или soft delete сценариев.

Апдейт в EF 10 закрыл одно серьёзное ограничение EF Core: теперь можно вешать несколько global query filters на одну сущность.

В реальном приложении нам часто нужно иметь несколько фильтров на одну и ту же сущность, например:
• один для soft delete
• второй для multi-tenancy

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍63
Топ-20 NuGet пакетов для .NET разработчиков в 2026

Не надо изобретать колесо

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

1. Entity Framework Core

2. Dapper

3. Serilog

4. FluentValidation

5. Polly

6. Refit

7. Wolverine

8. Dapr

9. Quartz .NET

10. Swagger (Swashbuckle)

11. xUnit

12. Shouldly

13. TestContainers

14. Bogus

15. Moq

16. FluentEmail

17. FastEndpoints

18. HotChocolate GraphQL

19. SignalR

20. UnitsNet


👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍107😐2
Обработка пропущенных событий

У простой конечной точки, которую мы делали ранее , есть один минус: у неё нет отказоустойчивости.

Одна из самых неприятных проблем с потоками в реальном времени — обрывы соединения. Пока браузер автоматически переподключится, сервер уже может отправить несколько событий, и они потеряются. Для этого в SSE предусмотрен встроенный механизм — заголовок Last-Event-ID. Когда браузер переподключается, он отправляет этот ID обратно на сервер.

В .NET 10 можно использовать тип SseItem<T> для добавления метаданных к данным (ID и retry-интервалы).

Можно завести простой OrderEventBuffer в памяти, который содержит объекты SseItem<OrderPlacement> и умеет отдавать все элементы после Last-Event-ID, который пришлёт браузер. Так мы можем «переиграть» пропущенные сообщения при переподключении:

app.MapGet("orders/realtime/with-replays", (
ChannelReader<OrderPlacement> reader,
OrderEventBuffer buffer,
[FromHeader(Name = "Last-Event-ID")]
string? lastEventId,
CancellationToken ct) =>
{
async IAsyncEnumerable<SseItem<OrderPlacement>>
StreamEvents()
{
// «Доливаем» пропущенные события
if (!string.IsNullOrWhiteSpace(lastEventId))
{
var missed = buffer.GetEventsAfter(lastEventId);
foreach (var m in missed)
yield return m;
}

// Отдаём новые события по мере появления в канале
await foreach (var order in reader.ReadAllAsync(ct))
{
// Буфер назначает уникальный ID
var sseItem = buffer.Add(order);
yield return sseItem;
}
}

return TypedResults.ServerSentEvents(
StreamEvents(), "orders");
});


Далее фильтрация SSE по пользователю - ❤️

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥8
Худший вид бага тот, который взрывается через 3 дня после деплоя.

Это скрытая проблема стандартного Options Pattern в .NET.

Ты биндишь appsettings.json к классу, инжектишь его, всё запускается, ошибок нет. Жизнь вроде хороша.

Но если, например, отсутствует обязательный API Key? Узнаешь об этом только когда пользователь дёрнет нужный сценарий, и приложение упадёт.

И вот тут важно правило — конфиг должен валиться сразу. Fail Fast.

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

Исправляется это расширением Options Pattern через IValidateOptions.

Вместо простого биндинга ты навешиваешь валидацию:

Определяешь правила: ApiKey не null, RetryCount > 0 и т.п.

1. Регистрируешь валидатор
2. Если конфиг не проходит правила, DI выбрасывает исключение на старте
3. Автор пошёл дальше и прикрутил FluentValidation, чтобы правила выглядели чище и понятнее.

Полная реализация тут

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👍42🤯2🤔1
Toolbox что это вообще такое?

Для начала, сам Toolbox лежит тут: [https://toolbox.bitspire.ch/]

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

• JSON → C# (да, это уже тысячу раз видели, но инструмент обязателен, и тут он ещё и настраиваемый)
• JSON → TypeScript (и обратно)
• JWT decoder
• Middleware Designer — генератор ASP.NET middleware (особенно полезен новичкам)
• Package centralizer — генерирует Directory.Packages.props на основе csproj-файлов
• C# Mindmap — обзор всех фич в одном месте

Хочу отдельно выделить две фичи, которые я пока нигде больше не встречал.

1. Middleware Designer

Middleware Designer это простой старт для новичков, который помогает понять какие middleware обычно используются в .NET и как их конфигурировать. Инструмент также пытается подсвечивать потенциальные проблемы. В примере на фото 1 видно, что Authorization вызывается раньше Authentication, что вряд ли то, что нужно.

Есть ещё режим "симуляции" запросов, чтобы посмотреть вживую как работают всякие штуки вроде rate-limiting. В идеале это даёт более понятную картинку типичных пайплайнов в ASP.NET и то, как их настраивать и использовать.

2. Package Centralizer

Ещё одна штука, которую я не встречал в открытом виде — генерация Directory.Packages.props из набора csproj-файлов. По сути вы подаёте пачку csproj-ов и получаете централизованную конфигурацию зависимостей (с опциональным разрешением конфликтов) + обновлённые csproj-файлы.

Если интересно покопаться:

GitHub tracker для фичреквестов и багов

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
3🥴2
Числовая сортировка строк в .NET

После десятилетия споров .NET наконец получил нормальную сортировку строк с числами. Кейс file1, file10, file2 теперь сортируется так, как ожидают люди, а не как ожидает лексикографический порядок.

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

Идея обсуждалась ещё в 2015 году в issue#13979, зависла на годы и только недавно доехала нативно в стек. Сейчас это уже в .NET 10.

Протестить:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

var files = new List<string> { "file10", "file2", "file1", "file20" };

// Обычная сортировка
var sortedLex = files.OrderBy(f => f).ToList();
Console.WriteLine("Лексикографическая: " + string.Join(", ", sortedLex));
// file1, file10, file20, file2

// Числовая сортировка
var comparer = StringComparer.Create(CultureInfo.CurrentCulture, CompareOptions.NumericOrdering);
var sortedNumeric = files.OrderBy(f => f, comparer).ToList();
Console.WriteLine("Числовая: " + string.Join(", ", sortedNumeric));
// file1, file2, file10, file20


Работает через:

StringComparer.Create(CultureInfo.CurrentCulture, CompareOptions.NumericOrdering)


👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
11🔥8👍2
Форматировщик C# в VSCode

1. Установите расширение C#

2. Перезапустите VS Code, чтобы расширение применилось.

3. Создайте .editorconfig в текущей директории по умолчанию или в родительской.
Я создал его в C:\Users\user, чтобы можно было использовать отовсюду.

Пример .editorconfig:

root = true

[*.cs]
indent_size = 2
indent_style = space
csharp_new_line_before_open_brace = none # не переносить фигурные скобки


Добавьте в settings.json:

"[csharp]": {
"editor.defaultFormatter": "ms-dotnettools.csharp",
}


Теперь форматтер для C# должен быть настроен.

Предупреждение:
На версии 2.100.5 и выше .editorconfig не применялся.
На 2.97.38 всё работало, так что пришлось откатиться.
Откройте расширение, нажмите стрелку рядом с Uninstall, там будет выбор конкретной версии.

Список проверки:

2.93.22    работает
2.94.41 работает
2.96.3 работает
2.97.38 работает
2.100.5 не работает
2.110.4 не работает


До версии 2.96 при нажатии Enter форматирование срабатывает автоматически,
потому что расширение включает editor.formatOnType.
Поэтому нужно переопределить его в settings.json:

"[csharp]": {
"editor.defaultFormatter": "ms-dotnettools.csharp",
"editor.formatOnType": false
}


PR, который исправляет editor.formatOnType

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
3