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

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 1293. #Оффтоп #МоиИнструменты
Notebook Editor для Visual Studio
Сегодня посоветую вам видео Скотта Хенсельмана https://youtu.be/WfozTizHMlM, в котором он рассказывает про расширение Jupiter Notebooks для Visual Studio.

Jupiter Notebooks – это смесь документа Word со средой исполнения. Если вам нужно научить кого-то языку, новому функционалу, попросить кандидата на интервью выполнить задание, либо просто описать, какие действия нужно сделать в программе. Вместо того, чтобы писать всё это в текстовом документе, откуда куски кода придётся копировать и вставлять в среду исполнения, используйте Jupiter Notebooks, где можно исполнять код прямо в документе.

Скотт рассказывает про инструмент на примере обучающих курсов от Microsoft по C# и Machine Learning.

На GitHub есть репозиторий с примерами использования https://github.com/dotnet/csharp-notebooks/

Источник: https://techcommunity.microsoft.com/t5/educator-developer-blog/using-visual-studio-notebooks-for-learning-c/ba-p/3580015
👍2
День 1405. #МоиИнструменты
Интеграционные Тесты
ASP.NET Core с Помощью Alba
Alba — это небольшая библиотека, которая обеспечивает простое интеграционное тестирование маршрутов ASP.NET Core, полностью совместимая с NUnit/xUnit.Net/MSTest. Недавно вышедшая версия 7.1 поддерживает .NET 7, улучшила обработку JSON для конечных точек Minimal API и поддерживает составной тип содержимого форм.

Допустим, есть проект Minimal API:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello, World!");
app.Run();

Мы можем написать тест маршрута, вроде такого:
[Fact]
public async Task sample_test()
{
// Здесь для примера хост создаётся внутри теста.
// В реальности это дорого,
// его лучше создать один на все тесты.
await using var host =
await AlbaHost.For<global::Program>();

var result = await host.GetAsText("/");

Assert.Equal(result, "Hello, World!");
}

Тест загружает ваше фактическое приложение, используя его конфигурацию, но используя TestServer вместо Kestrel в качестве веб-сервера.
Помимо GetAsText() есть другие полезные методы, вроде PostJson(), который использует конфигурацию сериализации JSON вашего приложения, если вдруг она у вас кастомизирована. Аналогично Receive<T>() использует сериализацию JSON вашего приложения.
Когда тест выполняется, он проходит через весь конвейер ASP.NET Core вашего приложения, включая любое зарегистрированное промежуточное ПО.

Альтернативным вариантом для теста может быть использование класса Scenario и его удобочитаемых методов:
await host.Scenario(_ =>
{
_.Get.Url("/");
_.ContentShouldBe("Hello, World!");
_.StatusCodeShouldBeOk();
});

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

Имейте в виду, чтобы ваши тесты «видели» ваш класс Program, вам нужно разрешить вашему тестовому проекту «видеть» ваш основной проект. Для этого в файл .csproj основного проекта добавьте:
<ItemGroup>
<InternalsVisibleTo Include="[ИмяПроектаТестов]" />
</ItemGroup>

Источник: https://jeremydmiller.com/2022/11/28/alba-for-effective-asp-net-core-integration-testing/
👍21
День 1442. #МоиИнструменты
Процессор JSON, Который Вам Пригодится
Обработка JSON — обычная задача в повседневной работе разработчиков. Мы привыкли работать с JSON, но иногда нам нужно что-то более динамичное и эффективное, чем System.Text.Json и Newtonsoft.Json. JMESPath — это мощный язык запросов, который позволяет вам выполнять задачи Map/Reduce декларативным и интуитивно понятным способом. Его можно использовать в .NET, см. JmesPath.Net.

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

Например, Azure CLI использует параметр –query для выполнения запроса JMESPath к результатам выполнения команд.

Допустим, у нас есть какие-то данные в JSON в строковой переменной source:
{
"balance": "$2,285.51",
"name": "Eva Sharpe",
"email": "evasharpe@zaggles.com",
"latitude": 46.325291,
"friends": [
{
"id": 0,
"name": "Nielsen Casey",
"age": 19
},
{
"id": 1,
"name": "Carlene Long",
"age": 38
}
]
}

Следующий код содержит запрос для его обработки. Он демонстрирует различные концепции, такие как проекция, фильтрация, агрегация, преобразование типов и т.д. Думаю, что синтаксис достаточно интуитивно понятен и не нуждается в объяснении:
var expressions = new (string, string)[]
{
("scalar", "balance"),
("projection", "{email: email, name: name}"),
("functions", "to_string(latitude)"),
("arrays", "friends[*].name"),
("filtering", "friends[?age > `20`].name"),
("aggregation", "{sum: sum(friends[*].age)}"),
};

var parser = new JmesPath();
foreach (var (name, expression) in expressions)
{
var result = parser.Transform(source, expression);
Console.WriteLine($"{name}: {result}");
}

Вывод
scalar: "$2,285.51"
projection: {"email":"evasharpe@zaggles.com","name":"Eva Sharpe"}
functions: "46.325291"
arrays: ["Nielsen Casey","Carlene Long"]
filtering: ["Carlene Long"]
aggregation: {"sum":57}

Итого
Как видите, JMESPath неплохо решает проблемы динамической обработки JSON на основе пользовательского ввода. Он также имеет модель расширяемости (возможность писать свои функции), которая открывает массу возможностей.

Источник: https://dev.to/nikiforovall/introduction-to-jmespath-json-processor-you-should-definitely-know-2dpb
👍20
День 1444. #МоиИнструменты
Тестовые Контейнеры в C#/.NET
Многие приложения сильно зависят от реляционной базы данных. Ваше приложение использует сложные запросы, ограничения на данные и другие возможности реляционной базы данных. Это означает, что многое в поведении ваших приложений зависит от того, как действует база данных. Поэтому важно проводить тесты на реальной базе. Сегодня это легко сделать с помощью TestContainers.

TestContainers — это библиотека, которая запускает контейнеры Docker для ваших тестов и предоставляет множество настроек для баз данных.

Попробуем настроить тестовый контейнер с Microsoft SqlServer:
// настройка БД
var dbConfig = new MsSqlTestcontainerConfiguration
{
Password = "Test1234",
Database = "TestDB"
};

// создание контейнера
var сontainer =
new TestcontainersBuilder<MsSqlTestcontainer>()
.WithDatabase(dbConfig)
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
.Build();

// и запуск
container.StartAsync().Wait();

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

Посмотрим пример теста:
record Dog (string Name, DateTime BirthDate);

using (var db = new SqlConnection(
container.ConnectionString))
{
db.Execute(@"CREATE TABLE Dogs(
BirthDate DATETIME NOT NULL,
Name VARCHAR(MAX) NOT NULL
)");

var born = new DateTime(2022, 12, 22);
db.Execute(@"
INSERT Dogs(BirthDate,Name)
VALUES(@BirthDate, @Name)",
new {
BirthDate = born,
Name = "Joe"
});

var dog = db.Query<Dog>(
@"SELECT * FROM Dogs")
.First();
Assert.AreEqual(born, dog.BirthDate);
}

// очищаем контейнер
container.DisposeAsync();

Что если убить процесс, который запускает тесты? Что произойдёт с контейнерами, которые он создал? На самом деле при запуске тестового контейнера создаётся два контейнера. Помимо запущенного вами, создаётся контейнер testcontainers/ryuk, который управляет всеми тестовыми контейнерами и выполняет очистку при сбоях.

В TestContainers встроена поддержка многих баз данных в наследниках класса TestcontainerDatabase. То же самое касается систем сообщений, которые являются наследниками TestcontainerMessageBroker. Всё это можно найти в документации.

Источник: https://www.gamlor.info/posts-output/2022-12-22-test-containers/en/
👍29
День 1445. #МоиИнструменты
Используем Новинки Языка на Старых Платформах
Сегодня порекомендую вам интересное видео от Ника Чапсаса об использовании новейших функций языка C# в старых версиях платформы, например, в .NET Framework.

На самом деле, все функции можно использовать практически «из коробки», просто изменив версию языка на 11. Да, Visual Studio не даст вам этого сделать так просто, потому что она автоматом определяет версию языка по версии фреймворка. Но можно вручную отредактировать файл .csproj, указав нужную версию языка в блоке компиляции:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>

<LangVersion>11.0</LangVersion>
</PropertyGroup>

Только одно это действие уже позволит вам писать, например, top-level statements (т.е. обойтись без класса Program).

Добавить другие функции чуть сложнее. Например, чтобы использовать init-свойства, нужно добавить пустой класс IsExternalInit:
namespace System.Runtime.CompilerServices
{
public static class IsExternalInit { }
}

Однако и эту работу можно облегчить, просто добавив NuGet-пакет PolySharp.

Все подробности в новом видео Ника https://youtu.be/RgKa-tjnUMA
👍12👎1
День 1457. #МоиИнструменты
Мониторинг Сайтов в UptimeRobot
Полезно знать, когда сайты, которые вы поддерживаете, не работают. Несмотря на то, что в ASP.NET Core есть встроенные способы проверки работоспособности, иногда нужно простое решение, которое требовало бы меньше кода, работало для произвольного количества сайтов, и просто уведомляло бы, когда сайт недоступен, и когда он снова онлайн.

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

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

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

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

У сервиса также есть приложения под Android и iOS, так что следить можно и с телефона. А если вы хотите отображать статус сайтов вашей команды на экране ТВ, даже в бесплатном варианте есть опция "TV Mode" (на картинке ниже).

Источник: https://ardalis.com/monitor-sites-with-uptime-robot-or-your-own-process/
👍11
День 1461. #МоиИнструменты
Полезные NuGet-Пакеты для Юнит-Тестирования
Помимо всем известных пакетов для юнит-тестирования, вроде Moq, NSubstitute или FluentAssertions, есть менее популярные, но оттого не менее полезные. Ниже приведу некоторые из них.

1. JustMock Lite
Это бесплатный пакет с открытым исходным кодом, который упрощает юнит-тестирование, простой в использовании, многофункциональный, мощный и гибкий. Он похож на Moq, но, как мне показалось, имеет более понятный fluent-API настройки и более богатую функциональность. JustMock Lite — бесплатная версия коммерческого продукта JustMock.

2. System.IO.Abstractions
В основе библиотеки лежат IFileSystem и FileSystem. Вместо прямого вызова таких методов, как File.ReadAllText, используйте IFileSystem.File.ReadAllText. Они имеют точно такой же API, который можно внедрять и тестировать. Библиотека также поставляется с набором тестовых помощников, чтобы избавить вас от необходимости имитировать каждый вызов для базовых сценариев. Они не являются полной копией реальной файловой системы, но помогут вам в тестах.

3. DeepEqual
Это расширяемая библиотека для глубокого сравнения. Чтобы проверить экземпляры объектов на равенство, просто вызовите метод расширения IsDeepEqual:
bool result = obj1.IsDeepEqual(obj2);
При использовании внутри теста вы можете вместо этого вызвать ShouldDeepEqual. Этот метод выдает исключение с подробным описанием различий между двумя объектами:
obj1.ShouldDeepEqual(obj2);
Также библиотека имеет fluent-API для кастомизации сравнения (например, можно игнорировать отдельные свойства).

4. ObjectDumper.Net
Это утилита, предназначенная для сериализации объектов C# в строку для целей отладки и ведения журнала. Вы можете использовать её в любом проекте .NET, совместимом с PCL. ObjectDumper предоставляет два отличных способа визуализации объектов .NET в памяти:
- DumpStyle.Console: сериализует объекты в удобочитаемые строки, часто используется для записи сложных объектов C# в файлы журналов.
- DumpStyle.CSharp: сериализует объекты в код инициализатора C#, который можно использовать для повторного создания объекта C#.

5. MockHttp
Это тестовый слой для библиотеки Microsoft HttpClient. Он позволяет настраивать заглушки для HTTP-ответов и может использоваться для тестирования сервисного уровня приложения.
MockHttp определяет замену HttpMessageHandler, механизма, управляющего HttpClient, который предоставляет гибкий API конфигурации и предоставляет готовый ответ. Вызывающий объект (например, сервисный уровень вашего приложения) остается в неведении о его наличии.

Источник: https://blog.markoliver.website/Testing-In-Dotnet
👍20👎1
День 1541. #МоиИнструменты
Обфускаторы в .NET
Распространенным подходом к защите интеллектуальной собственности является обфускация) символов и сокрытие кода IL. Веб-разработчики должны уделить особое внимание обфускации, поскольку код отправляется в браузер клиента.

На самом деле обфускаторов для .NET десятки и помимо перечисленных ниже. Многие из этих проектов так или иначе заброшены. Создание обфускатора для .NET — популярная и интересная задача, которую упростили библиотеки, такие как проект Mono.Cecil. Однако создать функциональный обфускатор, который должным образом запутывает код, не добавляя ошибок, сложно. Работающие обфускаторы .NET являются скорее исключением, чем нормой.

1. Preemptive Dotfuscator
Microsoft продвигала Dotfuscator как обфускатор для .NET, но, скорее всего, они никогда не собирались его предоставлять как утилиту. Разработчики, переходящие с C++ и VB6 на .NET, не осознавали, что их код может быть раскрыт, и это считалось недостатком новой платформы.
Несмотря на то, что Preemptive Dotfuscator является функциональным продуктом, недавно они на 50% увеличили плату за подписку, что вряд ли обосновано.

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

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

4. .NET Reactor
Понятный пользовательский интерфейс .NET Reactor, полный всплывающих подсказок и ссылок на обширную документацию, - то, что нужно для сложных инструментов, таких как обфускаторы. Полезно иметь описания функций, встроенные в UI, чтобы пользователям не приходилось снова и снова просматривать документацию или обращаться за поддержкой. Хотя поддержка у .NET Reactor быстрая и эффективная.

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

Источник: https://blog.ndepend.com/in-the-jungle-of-net-obfuscator-tools/
👍14
День 1558. #МоиИнструменты
WireMock.NET
Я как-то рассказывал про библиотеку Alba, которая позволяет проводить интеграционное тестирование API. Сегодня же расскажу вам про утилиту, позволяющую тестировать клиентов API и имитировать HTTP-запросы.

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

После установки nuget-пакета вы можете начать использовать WireMock.NET, создав экземпляр класса WireMockServer и настроив его на желаемое поведение. Это можно легко сделать, вызвав метод WireMockServer.Start или WireMockServer.StartWithAdminInterface:
using var wireMock = 
WireMockServer.Start(port: 1080, ssl: false);

Теперь можно начать определять моки для внешних HTTP-запросов, в том числе, используя Fluent API:
wireMock
.Given(
Request.Create()
.WithPath("/foo")
.UsingGet()
)
.RespondWith(
Response.Create()
.WithStatusCode(200)
.WithHeader("Content-Type",
"application/json; charset=utf-8")
.WithBodyAsJson(new
{ msg = "Hello world!" })
);

В тестах легко написать проверку результатов HTTP-запроса:
[Fact]
public async Task sample_test()
{
var response = await new HttpClient()
.GetAsync($"{_wireMock.Urls[0]}/foo");

Assert.Equal(HttpStatusCode.OK,
response.StatusCode);
Assert.Equal(
"""{"msg":"Hello world!"}""",
await response.Content.ReadAsStringAsync());
}

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

WireMock.NET предлагает множество функций, помимо базовых заглушек и имитаций HTTP-запросов:
1. Прокси-запросы к реальному сервису и захват ответов в виде маппинга. Вы можете использовать их в качестве основы для своих заглушек, устраняя необходимость в ручном определении ответов.
2. Чтение маппингов и определение заглушек из статических файлов, вместо того, чтобы определять их программно. Это может быть полезно для совместного использования заглушек в разных тестах или проектах.
3. Создание динамических шаблонов ответов, которые включают данные из запроса. Это позволяет создавать ответы, которые различаются в зависимости от данных запроса, что может быть полезно для тестирования пограничных случаев или моделирования поведения реального сервиса.
4. Моделирование поведения сервиса с помощью сценариев и состояний. Вы можете легко имитировать различные состояния сервиса и переключаться между ними. Это может быть полезно для проверки того, как ваш код обрабатывает различные типы сбоев или ответов от сервиса.

Узнать больше о возможностях WireMock.NET можно на странице WireMock Wiki.

Источник: https://cezarypiatek.github.io/post/mocking-outgoing-http-requests-p1/
👍30
День 1659. #МоиИнструменты
Веб-Инструменты для Каждого Разработчика
Сегодня поделюсь с вами подборкой веб-сайтов с инструментами, которые пригодятся каждому разработчику вне зависимости от области и языка программирования.

1. Transform.tools
Позволяет преобразовывать почти всё, например, HTML в JSX, JavaScript в JSON, CSS в объекты JS и многое другое. Действительно экономит время. К сожалению, отсутствует поддержка C#, поэтому JSON можно преобразовать разве что в объект Java, но с парочкой изменений руками (когда уже в Java человеческие свойства завезут?) потянет.

2. Convertio
Простой инструмент для конвертации файлов онлайн. Более 309 различных документов, изображений, электронных таблиц, электронных книг, архивов, презентаций, аудио и видео форматов. Например, PNG в JPEG, SVG в PNG, PNG в ICO и многие другие. Попробовал конвертнуть PDF книгу в DOCX. Очень даже неплохо. У бесплатной версии есть ограничение на общее время конвертации в день, но сколько это, я не нашёл. Думаю, для конвертации парочки файлов время от времени хватит.

3. Code Beautify
Онлайн версия программ Code Beautifier и Code Formatter, позволяющая форматировать исходный код. Но, кроме этого, также поддерживает некоторые преобразователи, такие как изображение в base64, CSV в Excel и т.п., минификаторы и обфускаторы кода для многих языков и много чего ещё.

4. Removebg
Мы программисты чаще всего плохо умеем в фотошоп, поэтому ловите инструмент, который позволяет легко удалить фон любого изображения. RemoveBG мгновенно определяет объект изображения и удаляет фон, оставляя изображение объекта на прозрачном фоне, которое вы можете легко использовать в своих проектах. Я попробовал его на паре фоток и должен отметить, что работает отлично. Потом результат можно перенести визуальный редактор canva.com и добавить фон, эффекты и даже создать видео.

5. ray.so
ray.so преобразует ваш фрагмент кода в визуально привлекательное изображение в пару кликов! Вы можете настроить иллюстрацию, выбрав из множества подсветок синтаксиса, добавить фон или выбрать тёмную или светлую тему. Это идеальный способ продемонстрировать свой код в посте или презентации по-настоящему стильно.

Источник: https://dev.to/j471n/top-10-websites-every-developer-needs-to-know-about-f5j
👍19👎1
День 1698. #МоиИнструменты
Как Освоить Клавиатуру и Стать Эффективным Разработчиком ПО
Мышь непродуктивна. Нужно переместить руку в другое место, точно навести курсор на что-то и нажать кнопку. Вы можете работать гораздо более продуктивно, используя только клавиатуру при правильном рабочем процессе, ПО, оборудовании и знаниях. Кроме того, использование мыши увеличивает нагрузку на мышцы, что может стать проблемой, если вы используете компьютер по много часов в день. В любом случае, если вы попробуете следующие советы, многие вещи вы сможете делать гораздо быстрее.

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

2. Найдите горячие клавиши в часто используемых приложениях
Вы будете удивлены, сколько их существует, например, в Visual Studio. Погуглите горячие клавиши для каждого инструмента, которым вы часто пользуетесь. Они точно вам пригодятся.

3. Горячие клавиши ОС
Начните с основных для редактирования текста, таких как Ctrl+[стрелка влево/вправо] для перемещения между словами, Ctrl+[del/backspace] для удаления слов и Ctrl+Shift+[стрелки влево/вправо] для выделения. Некоторые другие полезные — WinKey+V для стека буфера обмена и WinKey+Shift+S для создания снимка экрана.

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

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

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

7. Попробуйте механическую клавиатуру
Щелкать по механической клавиатуре просто веселее. Многие из них более эргономичны: изогнутые, разделённые, с педалями и т.п. Многие являются программируемыми. Вы можете расположить клавиши, как угодно, а также иметь несколько «слоёв» клавиатуры и даже управлять мышью.

8. Используйте ПО, подходящее для клавиатуры
Например, в Markdown (с Typora) не нужно делать что-то неприятное, например, тянуться за мышкой, чтобы отформатировать текст, как в Word. Другой пример – диаграммы. С помощью Mermaid вы можете описывать диаграммы в виде текста. Клоны Norton Commander, имеют кучу горячих клавиш и обеспечивают лучший опыт просмотра, чем проводник.

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

Источник: https://michaelscodingspot.com/keyboard-master/
👍18👎1
День 1703. #МоиИнструменты
Тестируем Конечные Точки HTTP в Visual Studio 2022
В Visual Studio 17.6 представлен новый инструмент, Обозреватель Конечных Точек (Endpoints Explorer), который упрощает процесс обнаружения и тестирования конечных точек API.

Чтобы использовать его, перейдите в View > Other Windows > Endpoint Explorer (Вид > Другие окна > Обозреватель конечных точек).

Он автоматически определит конечные точки в проекте, предоставляя обзор проектов и связанных с ними конечных точек (см. в правой панели на рисунке). Если вызвать команду Generate Request (Создать запрос), будет создан HTTP-файл с запросом для конечной точки (слева на рисунке).

Ссылки над запросами позволяют отправить запрос к запущенному приложению или запустить отладку приложения (Debug). Результат показан в панели по центру на рисунке.

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

Источник
👍27👎1
День 1883. #МоиИнструменты
Назначенные Задания с NCronJob
Hangfire/Quartz или фоновый сервис? А может что-то среднее? Если вам не нужен полноценный планировщик заданий с множеством настроек, но нужно нечто большее, чем просто BackgroundService, рассмотрите NCronJob.

Что это?
Простой и удобный планировщик заданий, работающий поверх IHostedService в .NET. Предоставляет два способа планирования заданий:
- Мгновенные задания — запуск задания прямо сейчас,
- Задания Cron — планирование задания, используя выражение cron.

Идея в том, чтобы иметь простой и лёгкий способ планирования заданий либо повторяющихся через нотацию cron, либо одноразовых (запускаемых с помощью мгновенных заданий). Библиотека построена на основе IHostedService и поэтому идеально подходит для приложений ASP.NET.

Особенности, отличающие NCronJob от BackgroundService:
- 2 вида заданий,
- передача параметров заданиям (как cron, так и мгновенным),
- (Скоро) Уведомления о завершении работы.

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

Как использовать?
Библиотека доступна в виде NuGet-пакета:
dotnet add package LinkDotNet.NCronJob

Для начала определим задание:
public class PrintHelloWorld : IJob
{
private ILogger<PrintHelloWorld> logger;

public PrintHelloWorld(
ILogger<PrintHelloWorld> logger)
{
this.logger = logger;
}

public Task RunAsync(
JobExecutionContext context,
CancellationToken ct = default)
{
logger.LogInformation(
"Parameter: {Parameter}", context.Parameter);

return Task.CompletedTask;
}
}

Как видите, поддерживаются параметры. Это справедливо как для заданий, запускаемых через cron, так и для мгновенных заданий. Теперь зарегистрируем сервис:
builder.Services.AddNCronJob();
builder.Services
.AddCronJob<PrintHelloWorld>(opt =>
{
// Каждую минуту
opt.CronExpression = "* * * * *";
// необязательный параметр
opt.Parameter = "Hello Parameter";
});

Готово!

Вы также можете запускать мгновенные задания откуда угодно:
public class MyService
{
private IInstantJobRegistry jobReg;

public MyService(IInstantJobRegistry jobReg)
=> this.jobReg = jobReg;

public void MyMethod()
=> jobReg.AddInstantJob<MyJob>(
"Необязательный параметр");
}


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

Источник: https://steven-giesel.com/blogPost/f58777b8-e10b-4023-845b-9f5ad3b7e48f/ncronjob-scheduling-made-easy
👍21
День 1928. #МоиИнструменты
Обновляем Устаревшие Пакеты в .NET

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

1. Диспетчер пакетов в IDE
В Visual Studio или Rider легко узнать, доступны ли новые версии пакетов, используемых вашим проектом, с помощью диспетчера пакетов NuGet.
В Visual Studio откройте «Tools > NuGet Package Manager > Manage NuGet Packages for Solution…» (Инструменты > Менеджер NuGet-Пакетов > Управлять NuGet-Пакетами в Решении…). На вкладке «Updates» (Обновления) выберите все обновления и нажмите «Обновить». Либо вы можете обновить их по одному. Кроме того, вкладка Consolidate (Консолидация) покажет пакеты, которые установлены в разных версиях в проектах решения и позволит привести все установленные пакеты к одной версии.
В Rider откройте «View > Tool Windows > NuGet» (Вид > Окна Инструментов > NuGet). Чтобы обновить установленные пакеты, нажмите «Upgrade Packages in Solution» (Обновить пакеты в решении) на панели инструментов окна NuGet, а затем выберите, какие пакеты следует обновить. Либо вы можете выбрать один из установленных пакетов в левой части окна NuGet, выбрать нужную версию в правой части, а затем обновить версию пакета в конкретных проектах.

2. Глобальная утилита dotnet-outdated
Это утилита командной строки с открытым исходным кодом. Для начала нужно её установить, выполнив следующую команду:
dotnet tool install --global dotnet-outdated-tool

Теперь вызовите её, выполнив команду в папке проекта или решения:
dotnet-outdated

Утилита выдаст список всех доступных обновлений, выделенных цветом:
- зеленым – патчи и исправления ошибок (обратно совместимо),
- желтым – обновления минорных версий (обратно совместимо, добавлены новые функции),
- красным – обновления мажорных версий (возможны ломающие изменения).
Чтобы обновить все устаревшие пакеты, выполните команду:
dotnet-outdated –upgrade


3. Через команду dotnet
Также можно использовать утилиты командной строки dotnet напрямую. Чтобы получить список устаревших пакетов, выполните следующую команду:
dotnet list package --outdated

Чтобы обновить устаревший пакет, нужно выполнить команду:
dotnet add package PACKAGENAME

Заметьте, что нужно выполнить эту команду по отдельности для всех пакетов, которые вы хотите обновить.

Если ваш проект размещён на GitHub, не обязательно постоянно помнить о необходимости проверки обновлений. Утилиты GitHub, вроде Dependabot помогут вам в этом, проанализировав зависимости ваших проектов и создавая пул-реквесты каждый раз, когда появляется новая версия какой-то из зависимостей. См. подробнее

Источник: https://bartwullems.blogspot.com/2024/05/net-core-view-outdated-packages.html?m=1
👍18
День 1940. #МоиИнструменты
Генерируем http-файлы из Спецификации OpenAPI

Http-файлы хороши и удобны, но их обновление также требует определенных усилий. Почему бы не сгенерировать их из cпецификации OpenAPI?

Когда вы создаете новый http-файл для веб-API в .NET 8, вы получаете файл MyProjectName.http. Он выглядит примерно так:
@MyWebApp_HostAddress = http://localhost:5059

GET {{MyWebApp_HostAddress}}/weatherforecast/
Accept: application/json

###

Этот файл используется расширением REST Client в Visual Studio Code, Visual Studio и Rider, что позволяет отправлять HTTP-запросы к вашему API прямо из редактора.

Но есть проблема: их обновление может быть хлопотным. Представьте, что мы добавляем в наш API две новые конечные точки:
app.MapPost("/weatherforecast", 
(CreateWeatherDto dto) => TypedResults.Ok())
.WithName("CreateWeatherForecast")
.WithOpenApi();
app.MapDelete("/weatherforecast/{id}",
(int id) => TypedResults.Ok())
.WithName("DeleteWeatherForecast")
.WithOpenApi();

Чтобы сгенерировать для них запросы в http-файл, используем утилиту httpgenerator.

Установка её очень проста:
dotnet tool install --global httpgenerator 

Теперь её можно использовать, передав ей путь к спецификации OpenAPI (локальный или, при запущенном API, URL):
httpgenerator http://localhost:5059/api/v1/openapi.json --output-type OneFile

--output-type OneFile объединит все конечные точки в один файл, иначе вы получите n файлов для каждой конечной точки, что не очень удобно. Файл всегда будет называться Requests.http и будет помещён в текущий каталог (если не указано иное с помощью параметра --output). Поэтому вы можете переименовать его в MyProjectName.http, чтобы соответствовать соглашению об именах. Получим примерно такой результат:
@contentType = application/json

###################################
### Request: GET /weatherforecast
###################################

GET http://localhost:5059/weatherforecast
Content-Type: {{contentType}}

####################################
### Request: POST /weatherforecast
####################################

POST http://localhost:5059/weatherforecast
Content-Type: {{contentType}}

{
"temperatureC": 0,
"summary": "summary"
}

###########################################
### Request: DELETE /weatherforecast/{id}
###########################################

### Path Parameter: DeleteWeatherForecast_id
@DeleteWeatherForecast_id = 0


DELETE http://localhost:5059/weatherforecast/{{DeleteWeatherForecast_id}}
Content-Type: {{contentType}}

Утилита также поддерживает другие параметры, в том числе передачу Bearer-токенов, о чём можно почитать на странице проекта в GitHub.

Источник: https://steven-giesel.com/blogPost/9fa236ef-67da-4113-95e7-99770dc70444/generate-http-files-from-a-swagger-definition
👍20
День 1952. #МоиИнструменты
Операции Перед Коммитом с
Husky.NET. Начало
Если вам нужно выполнить операции перед коммитом в Git, вы можете положиться на перехватчики - Git-хуки (Hooks).

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

Категории Git-хуков:
1. Клиентские на коммит – выполняются при git commit в локальном репозитории;
2. Клиентские на email - выполняются при git am — команды, позволяющей интегрировать почту и репозитории Git (если интересует, вот документация);
3. Клиентские на другие операции - запускаются в локальном репозитории при операциях вроде git rebase;
4. Серверные - запускаются после получения коммита в удалённом репозитории и могут отклонить операцию git push.

Мы сосредоточимся на клиентских хуках на коммит. Они бывают 4х видов:
1. pre-commit - вызывается первым при git commit (если вы не используете флаг -m, то перед запросом о добавлении сообщения) и может использоваться для проверки моментального снимка фиксируемого кода.
2. prepare-commit-msg – может использоваться для редактирования сообщения коммита по умолчанию, когда оно генерируется автоматическим инструментом.
3. commit-msg - может использоваться для проверки или изменения сообщения коммита после его ввода пользователем.
4. post-commit - вызывается после корректного выполнения коммита и обычно используется для запуска уведомлений.

Используем Husky.NET
Для Husky.NET необходимо добавить файл tool-manifest в корень решения:
dotnet new tool-manifest

Эта команда добавит файл .config/dotnet-tools.json со списком всех внешних инструментов, используемых dotnet. Теперь установим Husky:
dotnet tool install Husky

И добавим его в приложение .NET:
dotnet husky install

Это создаст в корне решения папку .husky, содержащую файлы, которые будут использоваться для Git-хуков. Создадим хук:
dotnet husky add pre-commit

Эта команда создаст файл pre-commit (без расширения) в папке .husky. На данный момент он ничего не делает, поэтому изменим его. Следующий текст хука будет компилировать код, форматировать текст (используя правила из файла .editorconfig) и выполнять тесты:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

echo 'Building code'
dotnet build

echo 'Formatting code'
dotnet format

echo 'Running tests'
dotnet test


Всё готово! Хотя, погодите-ка…

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

Источник:
https://www.code4it.dev/blog/husky-dotnet-precommit-hooks/
👍11
День 1953. #МоиИнструменты
Операции Перед Коммитом с
Husky.NET. Продолжение
Начало

Управляем командой dotnet format с помощью Husky.NET
В приведённом выше примере есть проблема. Т.к. dotnet format изменяет исходные файлы, и учитывая, что моментальный снимок кода уже был создан до выполнения хука, все изменённые файлы не будут частью окончательного коммита! Также dotnet format выполняет проверку каждого файла в решении, а не только тех, которые являются частью текущего снимка. Так операция может занять много времени. Есть 3 подхода к решению этой проблемы.

1. Выполнить git add
Выполнение git add . после dotnet format сделает все изменения частью коммита, но:
- dotnet format всё ещё будет затрагивать все файлы;
- git add . добавит все изменённые файлы в коммит, а не только те, которые вы хотели добавить (возможно потому что другие изменения должны были быть включены в другой коммит).

2. Пробный прогон dotnet format
Флаг --verify-no-changes команды dotnet format приведёт к возвращению ошибки, если хотя бы один файл необходимо обновить из-за правил форматирования.
Таким образом, если есть что форматировать, весь коммит будет отменён. Затем вам придётся запустить dotnet format для всего решения, исправить ошибки, добавить изменения в git и попробовать сделать коммит ещё раз. Это более длительный процесс, но он позволяет вам иметь полный контроль над отформатированными файлами.
Кроме того, вы не рискуете включить в снимок файлы, которые хотите сохранить в промежуточном состоянии, чтобы добавить их в последующий коммит.

3. dotnet format только для файлов коммита с помощью Husky.NET Task Runner
В папке .husky есть файл task-runner.json. Он позволяет создавать собственные сценарии с именем, группой, выполняемой командой и соответствующими параметрами. Изменим его так, чтобы dotnet format затрагивал только файлы, предназначенные для коммита:
{
"tasks": [
{
"name": "dotnet-format-staged-files",
"group": "pre-commit-operations",
"command": "dotnet",
"args": ["format", "--include", "${staged}"],
"include": ["**/*.cs"]
}
]
}

Здесь мы указали имя задачи (dotnet-format-staged-files), команду для запуска (dotnet с параметрами args) и фильтр списка файлов, подлежащих форматированию, используя параметр ${staged}, который заполняется Husky.NET. Мы также добавили задачу в группу pre-commit-operations, которую мы можем использовать для задач, выполняющихся вместе. Так можно запустить отдельную задачу:
dotnet husky run --name dotnet-format-staged-files 

или группу задач:
dotnet husky run --group pre-commit-operations

Теперь заменим команду dotnet format в файле pre-commit на одну из приведённых выше, а также добавим флаги --no-restore для сборки и теста, чтобы ускорить их выполнение:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

echo 'Format'
dotnet husky run --name dotnet-format-staged-files

echo 'Build'
dotnet build --no-restore

echo 'Test'
dotnet test --no-restore

echo 'Completed pre-commit changes'

Да, сборку можно не делать отдельно, т.к. она запускается перед тестами.

И последнее. Если вы захотите сделать коммит без выполнения хука, используйте флаг --no-verify:
git commit -m "my message" --no-verify


Источник: https://www.code4it.dev/blog/husky-dotnet-precommit-hooks/
👍6
День 2140. #МоиИнструменты
Создаём и Тестируем Устойчивые Приложения в .NET с Помощью Dev Proxy
При создании приложений, подключающихся к API, мы обычно фокусируемся на том, чтобы приложение работало. Но что, если API работает медленно, возвращает ошибки или становится недоступным? Сложно смоделировать, как ваше приложение будет справляться с этими сценариями, если вы не контролируете API, с которыми вы интегрируетесь.

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

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

Dev Proxy — это веб-прокси, который вы запускаете локально на своей машине разработки. Перед запуском вы настраиваете его для отслеживания запросов на определённые URL. Затем определяете, как он должен обрабатывать эти запросы: возвращать предопределённый ответ, выдавать ошибку, задерживать ответ или имитировать ограничение скорости или другие поведения? Когда вы запускаете Dev Proxy, он регистрируется как ваш системный прокси и перехватывает все запросы, которые соответствуют настроенным вами URL. Затем он применяет определённые вами поведения. Ваше приложение не знает, что оно не общается с реальным API. Оно просто получает ответы. Это отличный способ проверить, как ваше приложение обрабатывает различные поведения API.

По умолчанию Dev Proxy имитирует ошибку в ответ на запрос с вероятностью 50%. Если запрос не возвращает ошибку, Dev Proxy передаёт его реальному API.

Как улучшить устойчивость приложения для обработки сценария с ошибкой API? Во-первых, мы должны рассмотреть возможность перехвата исключения API и отображения его в удобном для пользователя виде. Это поможет обрабатывать как ошибки API, так и, например, ограничения частоты запросов (ошибка 429 Too Many Requests). Мы также должны рассмотреть возможность обработки ошибки 429 отлично от остальных ошибок, чтобы гарантировать, что приложение корректно сделает паузу и даст API время на восстановление.

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

Подробный пример работы с Dev Proxy рассмотрен в этом видео.

Источник: https://devblogs.microsoft.com/dotnet/build-test-resilient-apps-dotnet-dev-proxy/
👍10
День 2182. #МоиИнструменты
SQL-запросы к Логам и Другим Текстовым Данным
Сразу оговорюсь, что этот инструмент древний как мамонты, и в эпоху структурированных логов, Seq и прочих мощных визуализаторов, он, возможно, мало кому будет интересен, но вдруг. Я обнаружил его только недавно по необходимости.

Попросил меня коллега выбрать из логов IIS айпишники клиентов, которые обращались к определённому URL. Логи IIS – это текстовые файлы вот такого формата:
#Fields: date time s-sitename s-computername s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs-version cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status time-taken
2025-01-15 00:00:00 W3SVC7 WWW-VM-B 127.0.0.1 GET /page.html - 443 - 8.8.8.8 HTTP/1.1 Mozilla/5.0+… - 200 0 0 103

И дальше ещё много таких строчек (в моём случае было 2 файла по 1Гб+).

Так вот, у Microsoft есть утилита Log Parser которая обеспечивает универсальный доступ через SQL-запросы к текстовым данным, таким как файлы логов, XML-файлы и CSV-файлы, а также к источникам данных в операционной системе Windows, таким как журнал событий, реестр, файловая система и Active Directory.

Но это утилита для командной строки, а для любителей GUI, есть Log Parser Studio (скачать можно здесь), которая работает поверх Log Parser. На картинке выше пример результата запроса к логам IIS. Кроме того, Log Parser Studio позволяет выполнять разные запросы на нескольких вкладках, экспортировать результаты, сгенерировать скрипт для PowerShell, чтобы выполнять его на любой машине, где установлен Log Parser, а также содержит десятки шаблонов популярных запросов.

В общем, небольшая, но полезная в отдельных случаях утилитка.

Источник: https://techcommunity.microsoft.com/blog/exchange/log-parser-studio-2-0-is-now-available/593266
👍30
День 2453. #МоиИнструменты
RazorConsole
Если среди нас есть те, кто скучает по временам Norton Commander, терпеть не может мышь и кому «все эти ваши UI как кость в горле, дайте консоль», у меня для вас хорошие новости. NuGet-пакет RazorConsole стирает грань между разработкой современных веб-UI и консольными приложениями. Он позволяет создавать сложные интерфейсы в консоли с использованием компонентов Razor.

Стандартный пример в шаблоне приложения Blazor – компонент счётчика. Посмотрим, как это выглядит в RazorConsole.

Для начала создадим новый консольный проект. Добавим в него NuGet-пакет RazorConsole:
dotnet add package RazorConsole.Core

Кроме того, RazorConsole требуется SDK Microsoft.NET.Sdk.Razor для компиляции компонентов Razor. Поэтому нужно обновить файл проекта (.csproj) для его использования:
<Project Sdk="Microsoft.NET.Sdk.Razor">
<!-- … -->
</Project>


Добавим простой компонент Razor в файл Counter.razor:
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Web
@using RazorConsole.Components

<Rows>
<Columns>
<p>Current count</p>
<Markup Content="@count.ToString()"
Foreground="@Spectre.Console.Color.Green" />
</Columns>

<Columns>
<TextButton Content="+1"
OnClick="Increment"
BackgroundColor="@Spectre.Console.Color.Grey"
FocusedColor="@Spectre.Console.Color.Blue" />

<TextButton Content="-1"
OnClick="Decrement"
BackgroundColor="@Spectre.Console.Color.Grey"
FocusedColor="@Spectre.Console.Color.Blue" />
</Columns>
</Rows>

@code {
private int count = 0;
private void Increment() => count++;
private void Decrement() => count--;
}

Мы разместили текст, элемент счётчика и 2 кнопки (увеличивающую и уменьшающую счётчик). Всё это мы поместили в простую таблицу, используя готовые компоненты Rows и Columns (о них позже).

В файле Program.cs нам осталось добавить всего одну строку:
await AppHost.RunAsync<Counter>();

Пример работы показан на видео ниже. Активная кнопка выделяется голубым, перемещаться можно клавишей Tab, а нажимать на кнопку клавишей Enter.

Таким образом можно создавать UI в консоли, используя знакомые компоненты Razor с полной поддержкой привязки данных, обработки событий и методов жизненного цикла компонентов. Пакет содержит 15 готовых компонентов, охватывающих все необходимые функции:
- Разметка: Grid, Columns, Rows, Align, Padder;
- Поля ввода: TextInput, TextButton, Select;
- Отображение: Markup, Panel, Border, Figlet, SyntaxHighlighter, Table;
- Утилиты: Spinner, Newline.
Также имеется интерактивная галерея компонентов, которая поставляется как dotnet утилита RazorConsole.Gallery. Она содержит документацию по всем компонентам.

Источник: https://github.com/LittleLittleCloud/RazorConsole/
This media is not supported in your browser
VIEW IN TELEGRAM
👍12