Русский
Русский
English
Статистика
Реклама

Csharp

Как проходить собеседования на Unity разработчика

21.04.2021 14:19:32 | Автор: admin

Вступление и личные наблюдения

Собеседование на юнити-разработчика состоит в основном из трёх частей. Процесс выглядит практически один в один как и на любую другую техническую специальность в IT. Сначала собеседование с HR или рекрутером, потом техническое интервью с Team Leader команды разработки. В конце, если предыдущие этапы успешно пройдены, вас ждет финальный босс - Project Manager(или Product Owner). Эта статья будет полезна для джунов и мидлов, а также людей которые недавно познакомились с Unity. Бородатые синьоры и лиды - буду рад увидеть от вас в комментариях ваш опыт.

Благодарности

Спасибо Никите и Денису за помощь в оформлении и составлении списка вопросов.

Первая часть - собеседование с рекрутером

Как правило занимает от 10 до 30 минут. На нём задача рекрутера дать предварительную оценку по кандидату. Обычно просят рассказать о себе.

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

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

Пример ответа на Расскажите о своем опыте.:

Разрабатываю игры со старшей школы как инди разработчик, участвовал в джемах и конкурсах. На первом курсе начал работать в гипер-казуальном стартапе. Разрабатывал проекты на Unity C# и Lens Studio JavaScript. Отвечал за полный цикл разработки и гейм дизайн, общался с заказчиком и т.д. Команда состояла из.... Потом принял решение расти как программист дальше, пошел работать в большую компанию для улучшения понимания процессов разработки и технических навыков. Там делал За время работы научился делать. На последнем месте работы делаю Удалось автоматизировать Предложил варианты решений для... Хочу сменить работу потому, что...

Часть вторая - техническое интервью

Вот мы и прошли скрининг. В целом, вроде бы не сумасшедший, какие-то слова из вашей речи рекрутер смог сопоставить с требованиями в вакансии, Лондон из зэ кэпитал оф Грейт Британ смогли из себя выдавить. Супер! Идём дальше!

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

Интервью обычно делится на такие части:

  • Общие вопросы по разработке ПО (OOP, algorithms, DI, SOLID, etc.).

  • Вопросы по C# (boxing/unboxing, GC, async/await, reference types, etc.).

  • Unity и опыт в конкретном игровом жанре(match 3, slots, AAA, FPS, etc.) или направлении(mobile, PC, consoles, AR/VR, etc.).

Общие вопросы по разработке

  • Принципы ООП. Рассказать про каждый. Как это реализовано в языке C#?Как применяли на практике?

  • SOLID. В чем смысл каждого принципа и как применяли на практике?

  • Структуры данных. Какие структуры данных вы знаете? Для каких задач лучше использовать ту или иную.

  • В чем разница между array и List?

  • Что такое хеш-таблица? Что такое хеш-функция? Как обрабатываются коллизии в словарях?

  • Алгоритмы. Поиск пути в графе, сортировки коллекций, поиск элемента в коллекции. Какие подходы в обработке коллизий объектов в 2д и 3д знаете?

  • Сложность алгоритма. Big O notation.

  • Шаблоны проектирования. Архитектурные шаблоны(MVC, MVP, MVVM, компонентный подход, ECS). Шаблоны для решения типовых задач(GoF, GRASP, Game Programming Patterns).

  • Dependency Injection. Что это за подход разработки и умеете ли работать с Zenject?

  • Реактивность. Что это за подход разработки и умеете ли работать с UniRx?

  • Клиент-серверные приложения. В чем основные принципы разработки клиент-серверных игр? Какие типы вы знаете и разрабатывали?

  • CI/CD окружение. Для чего используется? Есть ли опыт работы с ним?

Вопросы по C#

  • Что такое .NET? Что такое CLR? Что такое IL?

  • Чем отличается динамическая типизация от статической?

  • Значимые и ссылочные типы. Спецификаторы аргументов функций ref, out.

  • Boxing и unboxing. Что это и почему это плохо?

  • Строки. Операции над строками, StringBuilder.

  • Что такое класс? Что такое структура? В чем отличие между структурой и классом?

  • Модификаторы доступа.

  • Что такое интерфейс? Какие члены можно описывать в интерфейсе?

  • Отличие интерфейса и абстрактного класса.

  • Upcasting, downcasting.

  • Обработка исключений. Блок try, catch, finally. Порядок выполнения.

  • Что такое делегат? Ковариантность, контрвариантность.

  • Что такое замыкание? Привести пример с замыканием.

  • Может ли структура реализовывать интерфейс?

  • Что такое атрибут? Для каких целей используются атрибуты?

  • Что такое рефлексия? Для решение каких задач приходилось использовать?

  • LINQ. Extension syntax, query syntax.

  • Как работает сборщик мусора? Что происходит с объектами которые имеют циклические зависимости?

  • Есть ли опыт написания авто-тестов и юнит-тестов?

Вопросы по Unity

  • Игровой движок. Что собой представляет и какие проблемы решает?

  • Корутины. Что это? Работают в одном потоке или в разных? Какой механизм C# используется для реализации корутин в юнити? Можно ли запустить рутину не из MonoBehaviour? Какие типы yield инструкций вы знаете? Когда они вызываются?

  • Что такое Game Object? Что такое сцена?

  • Что такое MonoBehaviour? От чего он наследуется? Можно ли создать тип наследуемый от Component?

  • Жизненный цикл MonoBehaviour.

  • Порядок вызова Event функций в runtime режиме Unity.

  • Физика. Какие компоненты позволяют работать с физикой. Что такое rigid body? Что такое рейкаст? Отличие от лайнкаста?

  • NavMesh. Поиск пути.

  • Опыт работы с UI компонентами? Что такое канвас? Что такое панель? Чем плох и хорош канвас? Как верстать адаптивный интерфейс? Что такое LayoutGroup?

  • Камера. Типы камер, параметры для настройки. Скай бокс, occlusion culling.

  • Что такое deltaTime и fixedDeltaTime? Отличия между ними.

  • Аниматор. Можно ли дописывать логику к состояниям аниматора? Что такое Timeline и опыт работы с ним?

  • Опыт написания кастомного редактора, окна для инструментов, расширения для ускорение и автоматизации рутинных задач.

  • Ассет бандлы и адрессаблы. Для чего используются и есть ли опыт разработки с их использованием?

  • Батчинг и Draw calls. Что это? Какие подходы оптимизации вызовов отрисовки вы знаете?

  • Что такое mesh? Из чего состоит 3д модель?

  • Опыт работы с шейдерами. Приходилось ли писать шейдеры?

  • Профайлинг. Какие инструменты для диагностики проблем производительности вы знаете(profiler, deep profiling, frame debugger, memory profiling, profiling on device)?

  • Unity Web Requests. Что это? Приходилось ли работать с клиент-серверным взаимодействием?

  • Есть ли опыт работы с нативным слоем? Android Studio, XCode.

  • Опыт интеграции SDK(реклама, аналитика, конфиги, БД, пуш уведомления).

  • Test Runner. Опыт работы с тестами в движке.

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

Часть третья - финальный босс

Когда по вам дали добро в плане технических навыков, вам предложат пройти последний этап. Это беседа с вашим будущим ПМом или продюсером(aka PO). Вас могут спросить, умеете ли вы эстимировать задачи. Есть ли у вас понимание как оценивать сложность задач. Не остаетесь ли вы один на один с проблемой, умеете ли вы просить помощь и в целом коммуницировать с другими членами команды. Задача вашего потенциального административного руководителя - определить уровень самостоятельности человека, насколько он подходит по типажу к людям в команде. Не пытайтесь врать или приукрашать ответы на вопросы. Это всегда видно, а вам с этим человеком потом вместе проходить сквозь спринты. Во-первых, если вы не умеете делать вещи выше - пришло время в этом начать разбираться. Во-вторых, честные ответы ценятся больше чем красивые истории.

Вопросы

  • Есть ли опыт провалившихся дедлайнов? Как справлялись с ситуацией?

  • Как решали задачи которые не могли решить самостоятельно?

  • Расскажите о самой сложной задаче.

  • Как вы оцениваете задачи по времени и сложности?

  • Есть ли опыт менторства? Как работали с джунами?

  • Приходилось ли работать в стрессовой обстановке перед релизом?

  • Как относитесь к овертаймам?

  • По какой методологии работали(agile, scrum, kanban)?

Подведение итогов

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

Как и любой другой навык, прохождение собеседований нарабатывается, как не удивительно, прохождением собеседований! Главное - покажите на максимум те навыки, которыми уже обладаете. И помните, если вы провалили собеседование или получили отказ, это может значить две вещи: или вам нужно еще поучиться, или вы банально не подходите этой компании, этому проекту, этой вакансии Это IT, слышал, тут так бывает. Удачи на собеседовании!

Подробнее..

Перевод Что же такого особенного в IAsyncEnumerable в .NET Core 3.0?

10.08.2020 18:20:12 | Автор: admin
Перевод статьи подготовлен в преддверии старта курса Разработчик C#.





Одной из наиболее важных функций .NET Core 3.0 и C# 8.0 стал новый IAsyncEnumerable<T> (он же асинхронный поток). Но что в нем такого особенного? Что же мы можем сделать теперь, что было невозможно раньше?

В этой статье мы рассмотрим, какие задачи IAsyncEnumerable<T> предназначен решать, как реализовать его в наших собственных приложениях и почему IAsyncEnumerable<T> заменит Task<IEnumerable<T>> во многих ситуациях.

Ознакомьтесь со всеми новыми функциями .NET Core 3

Жизнь до IAsyncEnumerable<T>


Возможно, лучший способ объяснить, почему IAsyncEnumerable<T> так полезен это рассмотреть проблемы, с которыми мы сталкивались до него.

Представьте, что мы создаем библиотеку для взаимодействия с данным, и нам нужен метод, который запрашивает некоторые данные из хранилища или API. Обычно этот метод возвращает Task<IEnumerable<T>>, как здесь:

public async Task<IEnumerable<Product>> GetAllProducts()

Чтобы реализовать этот метод, мы обычно запрашиваем данные асинхронно и возвращаем их, когда он завершается. Проблема с этим становится более очевидной, когда для получения данных нам нужно сделать несколько асинхронных вызовов. Например, наша база данных или API могут возвращать данные целыми страницами, как, например, эта реализация, использующая Azure Cosmos DB:

public async Task<IEnumerable<Product>> GetAllProducts(){    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);    var iterator = container.GetItemQueryIterator<Product>("SELECT * FROM c");    var products = new List<Product>();    while (iterator.HasMoreResults)    {        foreach (var product in await iterator.ReadNextAsync())        {            products.Add(product);        }    }    return products;}

Обратите внимание, что мы пролистываем все результаты в цикле while, создаем экземпляры объектов product, помещаем их в List, и, наконец, возвращаем все целиком. Это довольно неэффективно, особенно на больших наборах данных.

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

public IEnumerable<Task<IEnumerable<Product>>> GetAllProducts(){    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);    var iterator = container.GetItemQueryIterator<Product>("SELECT * FROM c");    while (iterator.HasMoreResults)    {        yield return iterator.ReadNextAsync().ContinueWith(t =>         {            return (IEnumerable<Product>)t.Result;        });    }}

Вызывающий объект будет использовать метод следующим образом:

foreach (var productsTask in productsRepository.GetAllProducts()){    foreach (var product in await productsTask)    {        Console.WriteLine(product.Name);    }}

Эта реализация более эффективна, но метод теперь возвращает IEnumerable<Task<IEnumerable<Product>>>. Как мы видим из вызывающего кода, вызов метода и обработка данных не интуитивны. Что еще более важно, подкачка страниц это деталь реализации метода доступа к данным, о которой вызывающая сторона не должна ничего знать.

IAsyncEnumerable<T> спешит на помощь


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

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

public IEnumerable<Product> GetAllProducts(){    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);    var iterator = container.GetItemQueryIterator<Product>("SELECT * FROM c");    while (iterator.HasMoreResults)    {        foreach (var product in iterator.ReadNextAsync().Result)        {            yield return product;        }    }}

Однако, НИКОГДА ТАК НЕ ДЕЛАЙТЕ! Приведенный выше код превращает асинхронный вызов базы данных в блокирующий и не масштабируется.

Если только мы могли бы использовать yield return с асинхронными методами! Это было невозможно до сих пор.

IAsyncEnumerable<T> был представлен в .NET Core 3 (.NET Standard 2.1). Он предоставляет энумератор, у которого есть метод MoveNextAsync(), который может быть ожидаемым. Это означает, что инициатор может совершать асинхронные вызовы во время (посреди) получения результатов.

Вместо возврата Task<IEnumerable<T>> наш метод теперь может возвращать IAsyncEnumerable<T> и использовать yield return для передачи данных.

public async IAsyncEnumerable<Product> GetAllProducts(){    Container container = cosmosClient.GetContainer(DatabaseId, ContainerId);    var iterator = container.GetItemQueryIterator<Product>("SELECT * FROM c");    while (iterator.HasMoreResults)    {        foreach (var product in await iterator.ReadNextAsync())        {            yield return product;        }    }}

Чтобы использовать результаты, нам нужно использовать новый синтаксис await foreach(), доступный в C# 8:

await foreach (var product in productsRepository.GetAllProducts()){    Console.WriteLine(product);}

Это намного приятнее. Метод производит данные по мере их поступления. Код вызова использует данные в своем темпе.

IAsyncEnumerable<T> и ASP.NET Core


Начиная с .NET Core 3 Preview 7, ASP.NET может возвращать IAsyncEnumerable из экшена контроллера API. Это означает, что мы можем возвращать результаты нашего метода напрямую эффективно передавая данные из базы данных в HTTP ответ.

[HttpGet]public IAsyncEnumerable<Product> Get()    => productsRepository.GetAllProducts();

Замена Task<IEnumerable<T>> на IAsyncEnumerable<T>


С течением времени по ходу освоения .NET Core 3 и .NET Standard 2.1, ожидается, что IAsyncEnumerable<T> будет использоваться в местах, где мы обычно использовали Task<IEnumerable>.

Я с нетерпением жду возможности увидеть поддержку IAsyncEnumerable<T> в библиотеках. В этой статье мы видели подобный код для запроса данных с помощью SDK Azure Cosmos DB 3.0:

var iterator = container.GetItemQueryIterator<Product>("SELECT * FROM c");while (iterator.HasMoreResults){    foreach (var product in await iterator.ReadNextAsync())    {        Console.WriteLine(product.Name);    }}

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

Чтобы посмотреть, как это могло бы выглядеть, если бы GetItemQueryIterator<Product>() вместо этого возвращал IAsyncEnumerable<T>, мы можем создать метод-расширение в FeedIterator:

public static class FeedIteratorExtensions{    public static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(this FeedIterator<T> iterator)    {        while (iterator.HasMoreResults)        {            foreach(var item in await iterator.ReadNextAsync())            {                yield return item;            }        }    }}

Теперь мы можем обрабатывать результаты наших запросов намного более приятным способом:

var products = container    .GetItemQueryIterator<Product>("SELECT * FROM c")    .ToAsyncEnumerable();await foreach (var product in products){    Console.WriteLine(product.Name);}

Резюме


IAsyncEnumerable<T> является долгожданным дополнением к .NET и во многих случаях сделает код более приятным и эффективным. Узнать об этом больше вы можете на этих ресурсах:




Шаблон проектирования Состояние (state)



Читать ещё:


Подробнее..

Генерация типизированных ссылок на элементы управления Avalonia с атрибутом xName с помощью C SourceGenerator

29.11.2020 20:05:10 | Автор: admin


В апреле 2020-го года разработчиками платформы .NET 5 был анонсирован новый способ генерации исходного кода на языке программирования C# с помощью реализации интерфейса ISourceGenerator. Данный способ позволяет разработчикам анализировать пользовательский код и создавать новые исходные файлы на этапе компиляции. При этом, API новых генераторов исходного кода схож с API анализаторов Roslyn. Генерировать код можно как с помощью Roslyn Compiler API, так и методом конкатенации обычных строк.


Постановка задачи


С помощью новых генераторов исходного кода может получиться элегантно решить широкий спектр задач, включая генерацию шаблонного кода, который было бы не очень интересно и совсем не продуктивно писать вручную. Например, в приложениях, использующих AvaloniaUI фреймворк для разработки кроссплатформенных приложений с графическим интерфейсом, о котором недавно вышла статья на Хабре нередко приходится писать следующий код для получения ссылки на элемент управления, объявленный в XAML:


TextBox PasswordTextBox => this.FindControl<TextBox>("PasswordTextBox");

Элемент типа TextBox с именем PasswordTextBox при этом объявлен в XAML следующим образом:


<TextBox x:Name="PasswordTextBox"         Watermark="Please, enter your password..."         UseFloatingWatermark="True"         PasswordChar="*" />

Получать ссылку на элемент управления в XAML может понадобиться в случае необходимости применения анимаций, программного изменения стилей и свойств элемента управления, или использования кроссплатформенных типизированных привязок данных ReactiveUI, таких, как Bind, BindCommand, BindValidation, позволяющих связывать компоненты View и ViewModel без использования синтаксиса {Binding} в XAML-разметке.


public class SignUpView : ReactiveWindow<SignUpViewModel>{    public SignUpView()    {        AvaloniaXamlLoader.Load(this);        // Привязки данных ReactiveUI и ReactiveUI.Validation.        // Можно было бы схожим образом использовать расширение разметки Binding,        // но некоторые разработчики предпочитают описывать биндинги в C#.        // Почему бы не облегчить им (и многим другим) жизнь?        //        this.Bind(ViewModel, x => x.Username, x => x.UserNameTextBox.Text);        this.Bind(ViewModel, x => x.Password, x => x.PasswordTextBox.Text);        this.BindValidation(ViewModel, x => x.CompoundValidation.Text);    }    // Шаблонный код для типизированного доступа к именованным    // элементам управления, объявленным в XAML.    private TextBox UserNameTextBox => this.FindControl<TextBox>("UserNameTextBox");    private TextBox PasswordTextBox => this.FindControl<TextBox>("PasswordTextBox");    private TextBlock CompoundValidation => this.FindControl<TextBlock>("CompoundValidation");}

Код геттеров свойств, позволяющих получить типизированный доступ к элементам управления из XAML-файла, соответствующего SignUpView, выглядит шаблонным. Было бы неплохо научиться генерировать этот код, чтобы, с одной стороны, избежать многословности, а с другой стороны чтобы избежать возможных ошибок и опечаток в имени элемента управления, объявленного в XAML, или в имени его типа.


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


Пример входных и выходных данных


Мы ожидаем, что на вход наш генератор исходного кода будет получать два файла. Для компонента представления с именем SignUpView, данными файлами будут являться XAML-разметка SignUpView.xaml, и code-behind файл SignUpView.xaml.cs, содержащий логику пользовательского интерфейса. Например, для файла разметки пользовательского интерфейса SignUpView.xaml:


<Window xmlns="http://personeltest.ru/aways/github.com/avaloniaui"        xmlns:x="http://personeltest.ru/away/schemas.microsoft.com/winfx/2006/xaml"        x:Class="Avalonia.NameGenerator.Sandbox.Views.SignUpView">    <StackPanel>        <TextBox x:Name="UserNameTextBox"                 Watermark="Please, enter user name..."                 UseFloatingWatermark="True" />        <TextBlock Name="UserNameValidation"                   Foreground="Red"                   FontSize="12" />    </StackPanel></Window>

Содержимое файла SignUpView.xaml.cs будет выглядеть следующим образом:


public partial class SignUpView : Window{    public SignUpView()    {        AvaloniaXamlLoader.Load(this);        // Мы хотим иметь доступ к типизированным элементам управления вот здесь,        // чтобы, например, писать код наподобие вот такого:        // UserNameTextBox.Text = "Violet Evergarden";        // UserNameValidation.Text = "An optional validation error message";    }}

А сгенерированное содержимое SignUpView.xaml.cs должно будет выглядеть следующим образом:


partial class SignUpView{    internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox");    internal global::Avalonia.Controls.TextBlock UserNameValidation => this.FindControl<global::Avalonia.Controls.TextBlock>("UserNameValidation");}

Префиксы global:: здесь нужны для избежания коллизий пространств имён. Дополнительно, необходимо полностью указывать имена типов также для избежания коллизий. По аналогии с WPF, мы маркируем генерируемые свойства как internal. В случае использования partial-классов базовый класс можно указывать только в одной из частей partial-класса, поэтому в сгенерированном коде мы опускаем указание базового класса таким образом пользователи нашего генератора смогут наследоваться от какого угодно наследника Window, будь то ReactiveWindow<TViewModel>, или другой тип окна.


Следует заметить, что при вызове метода FindControl обход дерева элементов производиться не будет Avalonia хранит именованные ссылки на элементы управления в словарях, называемых INameScope в терминологии Avalonia. При желании, Вы можете изучить исходный код методов FindControl и FindNameScope на GitHub.


Реализуем ISourceGenerator


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


[Generator]public class EmptyGenerator : ISourceGenerator{    public void Initialize(GeneratorInitializationContext context) { }    public void Execute(GeneratorExecutionContext context) { }}

В методе Initialize предлагается проинициализировать новый генератор исходного кода, а в методе Execute выполнить все важные вычисления, и при необходимости добавить сгенерированные файлы исходного кода в контекст выполнения с помощью вызова метода context.AddSource(fileName, sourceText). Давайте, для начала, добавим в сборку проекта, ссылающегося на генератор, некоторый атрибут, с помощью которого пользователи нашего генератора будут помечать классы, для которых необходимо генерировать типизированные ссылки на элементы управления Avalonia, объявленные в XAML. Изменим код нашего генератора следующим образом:


[Generator]public class NameReferenceGenerator : ISourceGenerator{    private const string AttributeName = "GenerateTypedNameReferencesAttribute";    private const string AttributeFile = "GenerateTypedNameReferencesAttribute";    private const string AttributeCode = @"// <auto-generated />using System;[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]internal sealed class GenerateTypedNameReferencesAttribute : Attribute { }";    public void Initialize(GeneratorInitializationContext context) { }    public void Execute(GeneratorExecutionContext context)    {        // Добавим код атрибута в файл 'GenerateTypedNameReferencesAttribute.cs' проекта        // разработчика, который решит воспользоваться нашим ISourceGenerator.        context.AddSource(AttributeFile, SourceText.From(AttributeCode, Encoding.UTF8));    }}

Пока ничего сложного мы объявили исходный код атрибута, имя файла, и имя атрибута как константы, с помощью вызова SourceText.From(code) обернули строку в исходный текст, и затем добавили новый исходный файл в проект с помощью вызова context.AddSource(fileName, sourceText). Теперь в проекте, который ссылается на наш генератор, мы можем помечать интересующие нас классы с помощью атрибута [GenerateTypedNameReferences]. Для классов, помеченных данным атрибутом, мы будем генерировать типизированные ссылки на именованные элементы управления, объявленные в XAML. В случае рассматриваемого примера с SignUpView.xaml, code-behind данного файла разметки должен будет выглядеть вот так:


[GenerateTypedNameReferences]public partial class SignUpView : Window{    public SignUpView()    {        AvaloniaXamlLoader.Load(this);        // Мы пока только собираемся генерировать именованные ссылки.        // Если раскомментировать код ниже, проект не скомпилируется (пока).        // UserNameTextBox.Text = "Violet Evergarden";        // UserNameValidation.Text = "An optional validation error message";    }}

Нам необходимо научить наш ISourceGenerator следующим вещам:


  1. Находить все классы, помеченные атрибутом [GenerateTypedNameReferences];
  2. Находить соответствующие классам XAML-файлы;
  3. Извлекать полные имена типов элементов интерфейса, объявленных в XAML-файлах;
  4. Вытаскивать из XAML-файлов имена (значения Name или x:Name) элементов управления;
  5. Генерировать partial-класс и заполнять его типизированными ссылками.

Находим классы, маркированные атрибутом


Для реализации такой функциональности API генераторов исходного кода предлагает реализовать и зарегистрировать интерфейс ISyntaxReceiver, который позволит собрать все ссылки на интересующий синтаксис в одном месте. Реализуем ISyntaxReceiver, который будет собирать все ссылки на объявления классов сборки пользователя нашего генератора:


internal class NameReferenceSyntaxReceiver : ISyntaxReceiver{    public List<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)    {        if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax &&            classDeclarationSyntax.AttributeLists.Count > 0)            CandidateClasses.Add(classDeclarationSyntax);    }}

Зарегистрируем данный класс в методе ISourceGenerator.Initialize(GeneratorInitializationContext context):


context.RegisterForSyntaxNotifications(() => new NameReferenceSyntaxReceiver());

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


// Добавим в CSharpCompilation исходник нашего атрибута.var options = (CSharpParseOptions)existingCompilation.SyntaxTrees[0].Options;var compilation = existingCompilation.AddSyntaxTrees(CSharpSyntaxTree    .ParseText(SourceText.From(AttributeCode, Encoding.UTF8), options));var attributeSymbol = compilation.GetTypeByMetadataName(AttributeName);var symbols = new List<INamedTypeSymbol>();foreach (var candidateClass in nameReferenceSyntaxReceiver.CandidateClasses){    // Извлечём INamedTypeSymbol из нашего класса-кандидата.    var model = compilation.GetSemanticModel(candidateClass.SyntaxTree);    var typeSymbol = (INamedTypeSymbol) model.GetDeclaredSymbol(candidateClass);    // Проверим, маркирован ли класс с помощью нашего атрибута.    var relevantAttribute = typeSymbol!        .GetAttributes()        .FirstOrDefault(attr => attr.AttributeClass!.Equals(            attributeSymbol, SymbolEqualityComparer.Default));    if (relevantAttribute == null) {        continue;    }    // Проверим, маркирован ли класс как 'partial'.    var isPartial = candidateClass        .Modifiers        .Any(modifier => modifier.IsKind(SyntaxKind.PartialKeyword));    // Таким образом, список 'symbols' будет содержать только те    // классы, которые маркированы с помощью ключевого слова 'partial'    // и атрибута 'GenerateTypedNameReferences'.    if (isPartial) {        symbols.Add(typeSymbol);    }}

Находим подходящие XAML-файлы


В Avalonia действуют соглашения именования XAML-файлов и code-behind файлов для них. Для файла с разметкой с именем SignUpView.xaml файл code-behind будет называться SignUpView.xaml.cs, а класс внутри него, как правило, называется SignUpView. В нашей реализации генератора типизированных ссылок будем полагаться на данную схему именования. Файлы разметки Avalonia на момент реализации генератора и написания данного материала могли иметь расширения .xaml или .axaml, поэтому код, определяющий имя XAML-файла на основании имени типа будет иметь следующий вид:


var xamlFileName = $"{typeSymbol.Name}.xaml";var aXamlFileName = $"{typeSymbol.Name}.axaml";var relevantXamlFile = context    .AdditionalFiles    .FirstOrDefault(text =>         text.Path.EndsWith(xamlFileName) ||         text.Path.EndsWith(aXamlFileName));

Здесь, typeSymbol имеет тип INamedTypeSymbol и может быть получен в результате обхода списка symbols, который мы сформировали на предыдущем этапе. А ещё здесь есть один нюанс. Чтобы файлы разметки были доступны как AdditionalFiles, пользователю генератора необходимо их дополнительно включить в проект с использованием директивы MSBuild <AdditionalFiles />. Таким образом, пользователь генератора должен отредактировать файл проекта .csproj, и добавить туда вот такой <ItemGroup />:


<ItemGroup>    <!-- Очень важная директива, без которой генераторы исходного         кода не смогут выпотрошить файлы разметки! -->    <AdditionalFiles Include="**\*.xaml" /></ItemGroup>

Подробное описание <AdditionalFiles /> можно найти в материале New C# Source Generator Samples.


Извлекаем полные имена типов из XAML


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


Хорошая новость заключается в том, что фреймворк AvaloniaUI использует новый компилятор XamlX, целиком написанный @kekekeks. Этот компилятор мало того, что не имеет рантайм-зависимостей, умеет находить ошибки в XAML на этапе компиляции, работает намного быстрее загрузчиков XAML из WPF, UWP, XF и других технологий, так ещё и предоставляет нам удобный API для парсинга XAML и разрешения типов. Таким образом, мы можем позволить себе подключить XamlX в проект исходниками (git submodule add ://repo ./path), и написать свой собственный MiniCompiler, который наш генератор исходного кода будет вызывать для компиляции XAML и получения полной информации о типах, даже если они лежат в каких-нибудь сторонних сборках. Реализация XamlX.XamlCompiler в виде нашего маленького MiniCompiler, который мы собираемся натравливать на XAML-файлы, имеет вид:


internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>{    public static MiniCompiler CreateDefault(RoslynTypeSystem typeSystem, params string[] additionalTypes)    {        var mappings = new XamlLanguageTypeMappings(typeSystem);        foreach (var additionalType in additionalTypes)            mappings.XmlnsAttributes.Add(typeSystem.GetType(additionalType));        var configuration = new TransformerConfiguration(            typeSystem,            typeSystem.Assemblies[0],            mappings);        return new MiniCompiler(configuration);    }    private MiniCompiler(TransformerConfiguration configuration)        : base(configuration, new XamlLanguageEmitMappings<object, IXamlEmitResult>(), false)    {        Transformers.Add(new NameDirectiveTransformer());        Transformers.Add(new DataTemplateTransformer());        Transformers.Add(new KnownDirectivesTransformer());        Transformers.Add(new XamlIntrinsicsTransformer());        Transformers.Add(new XArgumentsTransformer());        Transformers.Add(new TypeReferenceResolver());    }    protected override XamlEmitContext<object, IXamlEmitResult> InitCodeGen(        IFileSource file,        Func<string, IXamlType, IXamlTypeBuilder<object>> createSubType,        object codeGen, XamlRuntimeContext<object, IXamlEmitResult> context,        bool needContextLocal) =>        throw new NotSupportedException();}

В нашем MiniCompiler мы используем дефолтные трансформеры XamlX и один особенный NameDirectiveTransformer, тоже написанный @kekekeks, который умеет преобразовывать XAML-атрибут x:Name в XAML-атрибут Name для того, чтобы впоследствии обходить полученное AST и вытаскивать имена элементов управления было проще. Такой NameDirectiveTransformer выглядит следующим образом:


internal class NameDirectiveTransformer : IXamlAstTransformer{    public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)    {        // Нас интересуют только объекты.        if (node is XamlAstObjectNode objectNode)        {            for (var index = 0; index < objectNode.Children.Count; index++)            {                // Если мы встретили x:Name, заменяем его на Name и                 // продолжаем обходить потомков XamlAstObjectNode дальше.                var child = objectNode.Children[index];                if (child is XamlAstXmlDirective directive &&                    directive.Namespace == XamlNamespaces.Xaml2006 &&                    directive.Name == "Name")                    objectNode.Children[index] = new XamlAstXamlPropertyValueNode(                        directive,                        new XamlAstNamePropertyReference(                            directive, objectNode.Type, "Name", objectNode.Type),                        directive.Values);            }        }        return node;    }}

Фабрика MiniCompiler.CreateDefault принимает первым аргументом любопытный тип RoslynTypeSystem, который вы не найдёте в исходниках XamlX. Данный тип реализует интерфейс IXamlTypeSystem, а это значит, что всё самое сложное только начинается. Чтобы наш маленький компилятор заработал внутри нашего генератора исходного кода, нам необходимо реализовать систему типов XamlX поверх API семантической модели компилятора Roslyn. Для реализации новой IXamlTypeSystem пришлось реализовывать много-много интерфейсов (IXamlType для классов, IXamlAssembly для сборок, IXamlMethod для методов, IXamlProperty для свойств и др). Реализация IXamlAssembly, например, выглядит вот так:


public class RoslynAssembly : IXamlAssembly{    private readonly IAssemblySymbol _symbol;    public RoslynAssembly(IAssemblySymbol symbol) => _symbol = symbol;    public bool Equals(IXamlAssembly other) =>        other is RoslynAssembly roslynAssembly &&        SymbolEqualityComparer.Default.Equals(_symbol, roslynAssembly._symbol);    public string Name => _symbol.Name;    public IReadOnlyList<IXamlCustomAttribute> CustomAttributes =>        _symbol.GetAttributes()            .Select(data => new RoslynAttribute(data, this))            .ToList();    public IXamlType FindType(string fullName)    {        var type = _symbol.GetTypeByMetadataName(fullName);        return type is null ? null : new RoslynType(type, this);    }}

После реализации всех необходимых интерфейсов мы наконец сможем распарсить XAML инструментами XamlX, создать инстанс нашей реализации RoslynTypeSystem, передав ей в конструктор CSharpCompilation, которую мы уже извлекли из контекста генерации на предыдущем этапе, и трансформировать полученное в результате парсинга AST в AST с включённой информацией о пространствах имён и типах:


var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>());MiniCompiler.CreateDefault(    new RoslynTypeSystem(compilation), // 'compilation' имеет тип 'CSharpCompilation'    "Avalonia.Metadata.XmlnsDefinitionAttribute")    .Transform(parsed);

Готово! Осталось извлечь все именованные объекты из дерева и дело в шляпе.


Находим именованные объекты XAML


На предыдущем этапе мы уже рассмотрели трансформер AST XamlX, реализующий IXamlAstTransformer, а теперь давайте рассмотрим и напишем посетителя узлов этого AST, реализующий интерфейс IXamlAstVisitor. Наш посетитель будет выглядеть следующим образом:


internal sealed class NameReceiver : IXamlAstVisitor{    private readonly List<(string TypeName, string Name)> _items =        new List<(string TypeName, string Name)>();    public IReadOnlyList<(string TypeName, string Name)> Controls => _items;    public IXamlAstNode Visit(IXamlAstNode node)    {        if (node is XamlAstObjectNode objectNode)        {            // Извлекаем тип AST-узла. Данный тип нам вывел XamlX в            // процессе взаимодействия с нашей RoslynTypeSystem.            //            var clrType = objectNode.Type.GetClrType();            foreach (var child in objectNode.Children)            {                // Если мы в результате обхода потомков встретили свойство,                // которое называется 'Name', и при этом внутри 'Name' лежит строка,                // то добавляем в список элементов '_items' имя и CLR-тип элемента AST.                //                if (child is XamlAstXamlPropertyValueNode propertyValueNode &&                    propertyValueNode.Property is XamlAstNamePropertyReference namedProperty &&                    namedProperty.Name == "Name" &&                    propertyValueNode.Values.Count > 0 &&                    propertyValueNode.Values[0] is XamlAstTextNode text)                {                    var typeNamePair = ($@"{clrType.Namespace}.{clrType.Name}", text.Text);                    if (!_items.Contains(typeNamePair))                        _items.Add(typeNamePair);                }            }            return node;        }        return node;    }    public void Push(IXamlAstNode node) { }    public void Pop() { }}

Процесс парсинга XAML и извлечения типов и имён XAML-элементов теперь выглядит так:


var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>());MiniCompiler.CreateDefault(    new RoslynTypeSystem(compilation), // 'compilation' имеет тип 'CSharpCompilation'    "Avalonia.Metadata.XmlnsDefinitionAttribute")    .Transform(parsed);var visitor = new NameReceiver();parsed.Root.Visit(visitor);parsed.Root.VisitChildren(visitor);// Теперь у нас есть и типы, и имена элементов.var controls = visitor.Controls;

Генерируем типизированные ссылки


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


private static string GenerateSourceCode(    List<(string TypeName, string Name)> controls, // Список имён и типов с предыдущего этапа.    INamedTypeSymbol classSymbol, // Элемент из списка 'symbols', сформированного в самом начале.    AdditionalText xamlFile){    var className = classSymbol.Name;    var nameSpace = classSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat);    var namedControls = controls        .Select(info => "        " +                       $"internal global::{info.TypeName} {info.Name} => " +                       $"this.FindControl<global::{info.TypeName}>(\"{info.Name}\");");    return $@"// <auto-generated />using Avalonia.Controls;namespace {nameSpace}{{    partial class {className}    {{{string.Join("\n", namedControls)}       }}}}";}

Добавим полученный код в контекст выполнения GeneratorExecutionContext:


var sourceCode = GenerateSourceCode(controls, symbol, relevantXamlFile);context.AddSource($"{symbol.Name}.g.cs", SourceText.From(sourceCode, Encoding.UTF8));

Готово!


Результат


Инструментарий Visual Studio понимает, что при изменении XAML-файла, включённого в проект как <AdditionalFile />, необходимо вызвать генератор исходного кода ещё раз, и обновить сгенерированные исходники. Таким образом, при редактировании XAML-файлов, ссылки на новые элементы управления, добавляемые в XAML в процессе разработки, будут автоматически становиться доступными из C#-файла с расширением .xaml.cs.


ezgif-1-f52e7303c26f


Исходный код генератора доступен на GitHub.


Интеграция генераторов исходного кода с JetBrains Rider и ReSharper доступна в последних EAP, что позволяет утверждать, что реализованная технология является кроссплатформенной, и будет работать на Windows, Linux, и macOS. В дальнейшем мы собираемся заинтегрировать получившийся генератор в Avalonia, чтобы в новых версиях фреймворка генерация типизированных ссылок стала доступна из коробки.


А вот так выглядит обновлённый пример кода из самого начала статьи, с биндингами и ReactiveUI.Validation:


[GenerateTypedNameReferences]public class SignUpView : ReactiveWindow<SignUpViewModel>{    public SignUpView()    {        AvaloniaXamlLoader.Load(this);        this.Bind(ViewModel, x => x.Username, x => x.UserNameTextBox.Text);        this.Bind(ViewModel, x => x.Password, x => x.PasswordTextBox.Text);        this.BindValidation(ViewModel, x => x.CompoundValidation.Text);    }}

Ссылки


Подробнее..

Генерация типизированных ссылок на элементы управления AvaloniaUI с атрибутом xName с помощью C Source Generators API

29.11.2020 22:13:53 | Автор: admin


В апреле 2020-го года разработчиками платформы .NET 5 был анонсирован новый способ генерации исходного кода на языке программирования C# с помощью реализации интерфейса ISourceGenerator. Данный способ позволяет разработчикам анализировать пользовательский код и создавать новые исходные файлы на этапе компиляции. При этом, API новых генераторов исходного кода схож с API анализаторов Roslyn. Генерировать код можно как с помощью Roslyn Compiler API, так и методом конкатенации обычных строк.


Постановка задачи


С помощью новых генераторов исходного кода может получиться элегантно решить широкий спектр задач, включая генерацию шаблонного кода, который было бы не очень интересно и совсем не продуктивно писать вручную. Например, в приложениях, использующих AvaloniaUI фреймворк для разработки кроссплатформенных приложений с графическим интерфейсом, о котором недавно вышла статья на Хабре нередко приходится писать следующий код для получения ссылки на элемент управления, объявленный в XAML:


TextBox PasswordTextBox => this.FindControl<TextBox>("PasswordTextBox");

Элемент типа TextBox с именем PasswordTextBox при этом объявлен в XAML следующим образом:


<TextBox x:Name="PasswordTextBox"         Watermark="Please, enter your password..."         UseFloatingWatermark="True"         PasswordChar="*" />

Получать ссылку на элемент управления в XAML может понадобиться в случае необходимости применения анимаций, программного изменения стилей и свойств элемента управления, или использования кроссплатформенных типизированных привязок данных ReactiveUI, таких, как Bind, BindCommand, BindValidation, позволяющих связывать компоненты View и ViewModel без использования синтаксиса {Binding} в XAML-разметке.


public class SignUpView : ReactiveWindow<SignUpViewModel>{    public SignUpView()    {        AvaloniaXamlLoader.Load(this);        // Привязки данных ReactiveUI и ReactiveUI.Validation.        // Можно было бы схожим образом использовать расширение разметки Binding,        // но некоторые разработчики предпочитают описывать биндинги в C#.        // Почему бы не облегчить им (и многим другим) жизнь?        //        this.Bind(ViewModel, x => x.Username, x => x.UserNameTextBox.Text);        this.Bind(ViewModel, x => x.Password, x => x.PasswordTextBox.Text);        this.BindValidation(ViewModel, x => x.CompoundValidation.Text);    }    // Шаблонный код для типизированного доступа к именованным    // элементам управления, объявленным в XAML.    TextBox UserNameTextBox => this.FindControl<TextBox>("UserNameTextBox");    TextBox PasswordTextBox => this.FindControl<TextBox>("PasswordTextBox");    TextBlock CompoundValidation => this.FindControl<TextBlock>("CompoundValidation");}

Код геттеров свойств, позволяющих получить типизированный доступ к элементам управления из XAML-файла, соответствующего SignUpView, выглядит шаблонным. Было бы неплохо научиться генерировать этот код, чтобы, с одной стороны, избежать многословности, а с другой стороны чтобы избежать возможных ошибок и опечаток в имени элемента управления, объявленного в XAML, или в имени его типа.


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


Пример входных и выходных данных


Мы ожидаем, что на вход наш генератор исходного кода будет получать два файла. Для компонента представления с именем SignUpView, данными файлами будут являться XAML-разметка SignUpView.xaml, и code-behind файл SignUpView.xaml.cs, содержащий логику пользовательского интерфейса. Например, для файла разметки пользовательского интерфейса SignUpView.xaml:


<Window xmlns="http://personeltest.ru/aways/github.com/avaloniaui"        xmlns:x="http://personeltest.ru/away/schemas.microsoft.com/winfx/2006/xaml"        x:Class="Avalonia.NameGenerator.Sandbox.Views.SignUpView">    <StackPanel>        <TextBox x:Name="UserNameTextBox"                 Watermark="Please, enter user name..."                 UseFloatingWatermark="True" />        <TextBlock Name="UserNameValidation"                   Foreground="Red"                   FontSize="12" />    </StackPanel></Window>

Содержимое файла SignUpView.xaml.cs будет выглядеть следующим образом:


public partial class SignUpView : Window{    public SignUpView()    {        AvaloniaXamlLoader.Load(this);        // Мы хотим иметь доступ к типизированным элементам управления вот здесь,        // чтобы, например, писать код наподобие вот такого:        // UserNameTextBox.Text = "Violet Evergarden";        // UserNameValidation.Text = "An optional validation error message";    }}

А сгенерированное содержимое SignUpView.xaml.cs должно будет выглядеть следующим образом:


partial class SignUpView{    internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox");    internal global::Avalonia.Controls.TextBlock UserNameValidation => this.FindControl<global::Avalonia.Controls.TextBlock>("UserNameValidation");}

Префиксы global:: здесь нужны для избежания коллизий пространств имён. Дополнительно, необходимо полностью указывать имена типов также для избежания коллизий. По аналогии с WPF, мы маркируем генерируемые свойства как internal. В случае использования partial-классов базовый класс можно указывать только в одной из частей partial-класса, поэтому в сгенерированном коде мы опускаем указание базового класса таким образом пользователи нашего генератора смогут наследоваться от какого угодно наследника Window, будь то ReactiveWindow<TViewModel>, или другой тип окна.


Следует заметить, что при вызове метода FindControl обход дерева элементов производиться не будет Avalonia хранит именованные ссылки на элементы управления в словарях, называемых INameScope в терминологии Avalonia. При желании, Вы можете изучить исходный код методов FindControl и FindNameScope на GitHub.


Реализуем интерфейс ISourceGenerator


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


[Generator]public class EmptyGenerator : ISourceGenerator{    public void Initialize(GeneratorInitializationContext context) { }    public void Execute(GeneratorExecutionContext context) { }}

В методе Initialize предлагается проинициализировать новый генератор исходного кода, а в методе Execute выполнить все важные вычисления, и при необходимости добавить сгенерированные файлы исходного кода в контекст выполнения с помощью вызова метода context.AddSource(fileName, sourceText). При этом, файл проекта генератора исходного кода выглядит следующим образом:


<Project Sdk="Microsoft.NET.Sdk">    <PropertyGroup>        <TargetFramework>netstandard2.0</TargetFramework>        <LangVersion>preview</LangVersion>        <GeneratePackageOnBuild>true</GeneratePackageOnBuild>        <IncludeBuildOutput>false</IncludeBuildOutput>    </PropertyGroup>    <ItemGroup>        <PackageReference            Include="Microsoft.CodeAnalysis.CSharp"            Version="3.8.0-5.final"            PrivateAssets="all" />        <PackageReference            Include="Microsoft.CodeAnalysis.Analyzers"            Version="3.3.1"            PrivateAssets="all" />    </ItemGroup>    <ItemGroup>        <None Include="$(OutputPath)\$(AssemblyName).dll"              Pack="true"              PackagePath="analyzers/dotnet/cs"              Visible="false" />    </ItemGroup></Project>

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


[Generator]public class NameReferenceGenerator : ISourceGenerator{    private const string AttributeName = "GenerateTypedNameReferencesAttribute";    private const string AttributeFile = "GenerateTypedNameReferencesAttribute";    private const string AttributeCode = @"// <auto-generated />using System;[AttributeUsage(AttributeTargets.Class, Inherited=false, AllowMultiple=false)]internal sealed class GenerateTypedNameReferencesAttribute : Attribute { }";    public void Initialize(GeneratorInitializationContext context) { }    public void Execute(GeneratorExecutionContext context)    {        // Добавим код атрибута в файл 'GenerateTypedNameReferencesAttribute.cs'         // проекта разработчика, который решит воспользоваться нашим генератором.        context.AddSource(AttributeFile,            SourceText.From(                AttributeCode, Encoding.UTF8));    }}

Пока ничего сложного мы объявили исходный код атрибута, имя файла, и имя атрибута как константы, с помощью вызова SourceText.From(code) обернули строку в исходный текст, и затем добавили новый исходный файл в проект с помощью вызова context.AddSource(fileName, sourceText). Теперь в проекте, который ссылается на наш генератор, мы можем помечать интересующие нас классы с помощью атрибута [GenerateTypedNameReferences]. Для классов, помеченных данным атрибутом, мы будем генерировать типизированные ссылки на именованные элементы управления, объявленные в XAML. В случае рассматриваемого примера с SignUpView.xaml, code-behind данного файла разметки должен будет выглядеть вот так:


[GenerateTypedNameReferences]public partial class SignUpView : Window{    public SignUpView()    {        AvaloniaXamlLoader.Load(this);        // Мы пока только собираемся генерировать именованные ссылки.        // Если раскомментировать код ниже, проект не скомпилируется (пока).        // UserNameTextBox.Text = "Violet Evergarden";        // UserNameValidation.Text = "An optional validation error message";    }}

Нам необходимо научить наш ISourceGenerator следующим вещам:


  1. Находить все классы, помеченные атрибутом [GenerateTypedNameReferences];
  2. Находить соответствующие классам XAML-файлы;
  3. Извлекать полные имена типов элементов интерфейса, объявленных в XAML-файлах;
  4. Вытаскивать из XAML-файлов имена (значения Name или x:Name) элементов управления;
  5. Генерировать partial-класс и заполнять его типизированными ссылками.

Находим классы, маркированные атрибутом


Для реализации такой функциональности API генераторов исходного кода предлагает реализовать и зарегистрировать интерфейс ISyntaxReceiver, который позволит собрать все ссылки на интересующий синтаксис в одном месте. Реализуем ISyntaxReceiver, который будет собирать все ссылки на объявления классов сборки пользователя нашего генератора:


internal class NameReferenceSyntaxReceiver : ISyntaxReceiver{    public List<ClassDeclarationSyntax> CandidateClasses { get; } =        new List<ClassDeclarationSyntax>();    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)    {        if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax &&            classDeclarationSyntax.AttributeLists.Count > 0)            CandidateClasses.Add(classDeclarationSyntax);    }}

Зарегистрируем данный класс в методе ISourceGenerator.Initialize(GeneratorInitializationContext context):


context.RegisterForSyntaxNotifications(() => new NameReferenceSyntaxReceiver());

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


// Добавим в CSharpCompilation исходник нашего атрибута.var options = (CSharpParseOptions)existingCompilation.SyntaxTrees[0].Options;var compilation = existingCompilation.AddSyntaxTrees(CSharpSyntaxTree    .ParseText(SourceText.From(AttributeCode, Encoding.UTF8), options));var attributeSymbol = compilation.GetTypeByMetadataName(AttributeName);var symbols = new List<INamedTypeSymbol>();foreach (var candidateClass in nameReferenceSyntaxReceiver.CandidateClasses){    // Извлечём INamedTypeSymbol из нашего класса-кандидата.    var model = compilation.GetSemanticModel(candidateClass.SyntaxTree);    var typeSymbol = (INamedTypeSymbol) model.GetDeclaredSymbol(candidateClass);    // Проверим, маркирован ли класс с помощью нашего атрибута.    var relevantAttribute = typeSymbol!        .GetAttributes()        .FirstOrDefault(attr => attr.AttributeClass!.Equals(            attributeSymbol, SymbolEqualityComparer.Default));    if (relevantAttribute == null) {        continue;    }    // Проверим, маркирован ли класс как 'partial'.    var isPartial = candidateClass        .Modifiers        .Any(modifier => modifier.IsKind(SyntaxKind.PartialKeyword));    // Таким образом, список 'symbols' будет содержать только те    // классы, которые маркированы с помощью ключевого слова 'partial'    // и атрибута 'GenerateTypedNameReferences'.    if (isPartial) {        symbols.Add(typeSymbol);    }}

Находим подходящие XAML-файлы


В Avalonia действуют соглашения именования XAML-файлов и code-behind файлов для них. Для файла с разметкой с именем SignUpView.xaml файл code-behind будет называться SignUpView.xaml.cs, а класс внутри него, как правило, называется SignUpView. В нашей реализации генератора типизированных ссылок будем полагаться на данную схему именования. Файлы разметки Avalonia на момент реализации генератора и написания данного материала могли иметь расширения .xaml или .axaml, поэтому код, определяющий имя XAML-файла на основании имени типа будет иметь следующий вид:


var xamlFileName = $"{typeSymbol.Name}.xaml";var aXamlFileName = $"{typeSymbol.Name}.axaml";var relevantXamlFile = context    .AdditionalFiles    .FirstOrDefault(text =>         text.Path.EndsWith(xamlFileName) ||         text.Path.EndsWith(aXamlFileName));

Здесь, typeSymbol имеет тип INamedTypeSymbol и может быть получен в результате обхода списка symbols, который мы сформировали на предыдущем этапе. А ещё здесь есть один нюанс. Чтобы файлы разметки были доступны как AdditionalFiles, пользователю генератора необходимо их дополнительно включить в проект с использованием директивы MSBuild <AdditionalFiles />. Таким образом, пользователь генератора должен отредактировать файл проекта .csproj, и добавить туда вот такой <ItemGroup />:


<ItemGroup>    <!-- Очень важная директива, без которой генераторы исходного         кода не смогут выпотрошить файлы разметки! -->    <AdditionalFiles Include="**\*.xaml" /></ItemGroup>

Подробное описание <AdditionalFiles /> можно найти в материале New C# Source Generator Samples.


Извлекаем полные имена типов из XAML


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


Хорошая новость заключается в том, что фреймворк AvaloniaUI использует новый компилятор XamlX, целиком написанный @kekekeks. Этот компилятор мало того, что не имеет рантайм-зависимостей, умеет находить ошибки в XAML на этапе компиляции, работает намного быстрее загрузчиков XAML из WPF, UWP, XF и других технологий, так ещё и предоставляет нам удобный API для парсинга XAML и разрешения типов. Таким образом, мы можем позволить себе подключить XamlX в проект исходниками (git submodule add ://repo ./path), и написать свой собственный MiniCompiler, который наш генератор исходного кода будет вызывать для компиляции XAML и получения полной информации о типах, даже если они лежат в каких-нибудь сторонних сборках. Реализация XamlX.XamlCompiler в виде нашего маленького MiniCompiler, который мы собираемся натравливать на XAML-файлы, имеет вид:


internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>{    public static MiniCompiler CreateDefault(        RoslynTypeSystem typeSystem,        params string[] additionalTypes)    {        var mappings = new XamlLanguageTypeMappings(typeSystem);        foreach (var additionalType in additionalTypes)            mappings.XmlnsAttributes.Add(typeSystem.GetType(additionalType));        var configuration = new TransformerConfiguration(            typeSystem,            typeSystem.Assemblies[0],            mappings);        return new MiniCompiler(configuration);    }    private MiniCompiler(TransformerConfiguration configuration)        : base(configuration,               new XamlLanguageEmitMappings<object, IXamlEmitResult>(),               false)    {        Transformers.Add(new NameDirectiveTransformer());        Transformers.Add(new DataTemplateTransformer());        Transformers.Add(new KnownDirectivesTransformer());        Transformers.Add(new XamlIntrinsicsTransformer());        Transformers.Add(new XArgumentsTransformer());        Transformers.Add(new TypeReferenceResolver());    }    protected override XamlEmitContext<object, IXamlEmitResult> InitCodeGen(        IFileSource file,        Func<string, IXamlType, IXamlTypeBuilder<object>> createSubType,        object codeGen, XamlRuntimeContext<object, IXamlEmitResult> context,        bool needContextLocal) =>        throw new NotSupportedException();}

В нашем MiniCompiler мы используем дефолтные трансформеры XamlX и один особенный NameDirectiveTransformer, тоже написанный @kekekeks, который умеет преобразовывать XAML-атрибут x:Name в XAML-атрибут Name для того, чтобы впоследствии обходить полученное AST и вытаскивать имена элементов управления было проще. Такой NameDirectiveTransformer выглядит следующим образом:


internal class NameDirectiveTransformer : IXamlAstTransformer{    public IXamlAstNode Transform(        AstTransformationContext context,        IXamlAstNode node)    {        // Нас интересуют только объекты.        if (node is XamlAstObjectNode objectNode)        {            for (var index = 0; index < objectNode.Children.Count; index++)            {                // Если мы встретили x:Name, заменяем его на Name и                 // продолжаем обходить потомков XamlAstObjectNode дальше.                var child = objectNode.Children[index];                if (child is XamlAstXmlDirective directive &&                    directive.Namespace == XamlNamespaces.Xaml2006 &&                    directive.Name == "Name")                    objectNode.Children[index] =                        new XamlAstXamlPropertyValueNode(                            directive,                            new XamlAstNamePropertyReference(                                directive, objectNode.Type, "Name", objectNode.Type),                            directive.Values);            }        }        return node;    }}

Фабрика MiniCompiler.CreateDefault принимает первым аргументом любопытный тип RoslynTypeSystem, который вы не найдёте в исходниках XamlX. Данный тип реализует интерфейс IXamlTypeSystem, а это значит, что всё самое сложное только начинается. Чтобы наш маленький компилятор заработал внутри нашего генератора исходного кода, нам необходимо реализовать систему типов XamlX поверх API семантической модели компилятора Roslyn. Для реализации новой IXamlTypeSystem пришлось реализовывать много-много интерфейсов (IXamlType для классов, IXamlAssembly для сборок, IXamlMethod для методов, IXamlProperty для свойств и др). Реализация IXamlAssembly, например, выглядит вот так:


public class RoslynAssembly : IXamlAssembly{    private readonly IAssemblySymbol _symbol;    public RoslynAssembly(IAssemblySymbol symbol) => _symbol = symbol;    public bool Equals(IXamlAssembly other) =>        other is RoslynAssembly roslynAssembly &&        SymbolEqualityComparer.Default.Equals(_symbol, roslynAssembly._symbol);    public string Name => _symbol.Name;    public IReadOnlyList<IXamlCustomAttribute> CustomAttributes =>        _symbol.GetAttributes()            .Select(data => new RoslynAttribute(data, this))            .ToList();    public IXamlType FindType(string fullName)    {        var type = _symbol.GetTypeByMetadataName(fullName);        return type is null ? null : new RoslynType(type, this);    }}

После реализации всех необходимых интерфейсов мы наконец сможем распарсить XAML инструментами XamlX, создать инстанс нашей реализации RoslynTypeSystem, передав ей в конструктор CSharpCompilation, которую мы уже извлекли из контекста генерации на предыдущем этапе, и трансформировать полученное в результате парсинга AST в AST с включённой информацией о пространствах имён и типах:


var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>());MiniCompiler.CreateDefault(    // 'compilation' имеет тип 'CSharpCompilation'    new RoslynTypeSystem(compilation),    "Avalonia.Metadata.XmlnsDefinitionAttribute")    .Transform(parsed);

Готово! Осталось извлечь все именованные объекты из дерева и дело в шляпе.


Находим именованные объекты XAML


На предыдущем этапе мы уже рассмотрели трансформер AST XamlX, реализующий IXamlAstTransformer, а теперь давайте рассмотрим и напишем посетителя узлов этого AST, реализующий интерфейс IXamlAstVisitor. Наш посетитель будет выглядеть следующим образом:


internal sealed class NameReceiver : IXamlAstVisitor{    private readonly List<(string TypeName, string Name)> _items =        new List<(string TypeName, string Name)>();    public IReadOnlyList<(string TypeName, string Name)> Controls => _items;    public IXamlAstNode Visit(IXamlAstNode node)    {        if (node is XamlAstObjectNode objectNode)        {            // Извлекаем тип AST-узла. Данный тип нам вывел XamlX в            // процессе взаимодействия с нашей RoslynTypeSystem.            //            var clrType = objectNode.Type.GetClrType();            foreach (var child in objectNode.Children)            {                // Если мы в результате обхода потомков встретили свойство,                // которое называется 'Name', и при этом внутри 'Name' лежит строка,                // то добавляем в список элементов '_items' имя и CLR-тип элемента AST.                //                if (child is XamlAstXamlPropertyValueNode propertyValueNode &&                    propertyValueNode.Property is XamlAstNamePropertyReference named &&                    named.Name == "Name" &&                    propertyValueNode.Values.Count > 0 &&                    propertyValueNode.Values[0] is XamlAstTextNode text)                {                    var nsType = $@"{clrType.Namespace}.{clrType.Name}";                    var typeNamePair = (nsType, text.Text);                    if (!_items.Contains(typeNamePair))                        _items.Add(typeNamePair);                }            }            return node;        }        return node;    }    public void Push(IXamlAstNode node) { }    public void Pop() { }}

Процесс парсинга XAML и извлечения типов и имён XAML-элементов теперь выглядит так:


var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>());MiniCompiler.CreateDefault(    // 'compilation' имеет тип 'CSharpCompilation'    new RoslynTypeSystem(compilation),    "Avalonia.Metadata.XmlnsDefinitionAttribute")    .Transform(parsed);var visitor = new NameReceiver();parsed.Root.Visit(visitor);parsed.Root.VisitChildren(visitor);// Теперь у нас есть и типы, и имена элементов.var controls = visitor.Controls;

Генерируем типизированные ссылки


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


private static string GenerateSourceCode(    List<(string TypeName, string Name)> controls,    INamedTypeSymbol classSymbol,    AdditionalText xamlFile){    var className = classSymbol.Name;    var nameSpace = classSymbol.ContainingNamespace        .ToDisplayString(SymbolDisplayFormat);    var namedControls = controls        .Select(info => "        " +                       $"internal global::{info.TypeName} {info.Name} => " +                       $"this.FindControl<global::{info.TypeName}>(\"{info.Name}\");");    return $@"// <auto-generated />using Avalonia.Controls;namespace {nameSpace}{{    partial class {className}    {{{string.Join("\n", namedControls)}       }}}}";}

Добавим полученный код в контекст выполнения GeneratorExecutionContext:


var sourceCode = GenerateSourceCode(controls, symbol, relevantXamlFile);context.AddSource($"{symbol.Name}.g.cs", SourceText.From(sourceCode, Encoding.UTF8));

Готово!


Результат


Инструментарий Visual Studio понимает, что при изменении XAML-файла, включённого в проект как <AdditionalFile />, необходимо вызвать генератор исходного кода ещё раз, и обновить сгенерированные исходники. Таким образом, при редактировании XAML-файлов, ссылки на новые элементы управления, добавляемые в XAML в процессе разработки, будут автоматически становиться доступными из C#-файла с расширением .xaml.cs.


ezgif-1-f52e7303c26f


Исходный код генератора доступен на GitHub.


Интеграция генераторов исходного кода с JetBrains Rider и ReSharper доступна в последних EAP, что позволяет утверждать, что реализованная технология является кроссплатформенной, и будет работать на Windows, Linux, и macOS. В дальнейшем мы собираемся заинтегрировать получившийся генератор в Avalonia, чтобы в новых версиях фреймворка генерация типизированных ссылок стала доступна из коробки.


А вот так выглядит обновлённый пример кода из самого начала статьи, с биндингами и ReactiveUI.Validation:


[GenerateTypedNameReferences]public class SignUpView : ReactiveWindow<SignUpViewModel>{    public SignUpView()    {        AvaloniaXamlLoader.Load(this);        this.Bind(ViewModel, x => x.Username, x => x.UserNameTextBox.Text);        this.Bind(ViewModel, x => x.Password, x => x.PasswordTextBox.Text);        this.BindValidation(ViewModel, x => x.CompoundValidation.Text);    }}

Ссылки


Подробнее..

Перевод Модифицируем паттерн Filter с помощью обобщенных лямбда-выражений

19.01.2021 02:08:11 | Автор: admin

Мы можем оптимизировать паттерн Pipeline & Filter (конвейер и фильтры), сократив количество кода, необходимое на его реализацию, используя лямбда-выражение (упрощенную запись анонимных методов) в качестве конкретного условия фильтрации. В качестве примера для демонстрации этой концепции, было выбрано WPF-приложение с пользовательским интерфейсом. Вот его исходный код.

Паттерн Pipeline & Filter

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

ConditionAggregator - это конвейерный класс, в котором хранится коллекция Condition<T>. Filter<T> владеет ConditionAggregator или Condition<T> для применения условий (condition) фильтрации к набору данных. Когда вызывается функция apply (применить) Filter<T>, выполняется метод check (проверить) ICondition<T>. ConditionAggregator<T> имеет событие OnFilterChanged. Оно срабатывает, когда в классах модели представления изменяется значение коллекции или условия. В следующем разделе будет описано использование паттерна Filter моделью представления.

Код

Использование в модели представления

Объяснение паттерна MVVM можно найти по этой ссылке. Одна из обязанностей модели представления (View Model) в MVVM - обрабатывать взаимодействие с пользователем и изменения данных. В нашем случае изменения значений условий фильтрации должны быть переданы на бизнес-уровень, чтобы применить фильтры к определенной коллекции данных. Изменение значения условия в модели представления стригерит событие ConditionAggregator<T> OnFilterChanged, на которое подписан метод фильтра apply. Ниже приведена диаграмма классов модели представления.

Класс сущности Employee создан для хранения информации о сотрудниках. Generic тип T паттерна проектирования Filter будет заменен классом Employee. EmployeeList содержит список данных о сотрудниках и применяемые фильтры. Конструктор класса получает список условий и переходит к списку фильтров.

public EmployeeList(IEmployeesRepository repository, ConditionAggregator<employee> conditionAggregator)        {            this.repository = repository;            this.filters = new ConcreteFilter<employee>(conditionAggregator);            conditionAggregator.OnFilterChanged += this.FilterList;            _ = initAsync();        }

Метод FilterList подписан на событие OnFilterChanged для применения фильтров к данным, когда происходит изменение условия или значения.

private void FilterList(){    this.Employees = this.filters.Apply(this.employeesFullList);}

EmployeesViewModel подключен к пользовательскому интерфейсу. В этом примере продемонстрирован только один фильтр свойства EmployeeTypeselected, но в ConditionAggregator можно передать множество фильтров. Следующий фрагмент кода - это метод-конструктор, в котором регистрируется условие фильтра.

public EmployeesViewModel(IEmployeesRepository repository)        {            this.repository = repository;            Condition&lt;employee&gt; filterEmployee = new Condition&lt;employee&gt;((e) =&gt; e.employeeCode == this.EmployeeTypeSelected);            this.conditionAggregator = new ConditionAggregator&lt;employee&gt;(new List&lt;condition&lt;employee&gt;&gt; { filterEmployee });            this.EmployeeList = new EmployeeList(repository, this.conditionAggregator);                    }  &lt;/condition&lt;employee&gt;&lt;/employee&gt;&lt;/employee&gt;&lt;/employee&gt;

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

Архитектура приложения

WPF.Demo.DataFilter - это представление пользовательского интерфейса WPF. Он имеет одну сетку и одно поле со списком для фильтрации. Проект WPF.Demo.DataFilter.ViewModels обрабатывает данные, фильтрует изменения и перезагружает данные для обновления пользовательского интерфейса. Проект WPF.Demo.DataFilter.Common представляет собой полную реализацию шаблона Pipeline & Filter. WPF.Demo.DataFilter.DAL загружает простенький json-файл в качестве хранилища данных.

Это основной интерфейс:


Перевод статьи был подготовлен в преддверии старта курса "Архитектура и шаблоны проектирования". А прямо сейчас приглашаем всех желающих на бесплатный демо урок в рамках которого обсудим назначение и структуру шаблона "Интерпретатор", формы Бекуса-Науэра, лексический, синтаксический и семантический анализы, а также разберем практические примеры. Записаться на урок можно по ссылке.


Подробнее..

C программист, испытай себя найди ошибку

01.02.2021 16:08:09 | Автор: admin

Анализатор PVS-Studio регулярно пополняется новыми диагностическими правилами. Что интересно, часто диагностики обнаруживают подозрительные фрагменты кода еще до окончания всех работ. Например, в процессе тестирования на open-source проектах. Одной из подобных интересных 'находок' и хотелось бы поделиться сегодня с вами.

Как говорилось выше, один из этапов тестирования диагностического правила - проверка его работы на реальной кодовой базе. Для этого используется набор отобранных open-source проектов, для которых проводится анализ. Очевидное преимущество такого подхода - возможность посмотреть, как ведёт себя диагностическое правило в "реальных условиях". Менее очевидное - иногда находится что-то настолько интересное, что про это и заметку не грех написать.

Итак, предлагаю посмотреть на код из проекта Bouncy Castle C# и найти в нём ошибку:

public static string ToString(object[] a){  StringBuilder sb = new StringBuilder('[');  if (a.Length > 0)  {    sb.Append(a[0]);    for (int index = 1; index < a.Length; ++index)    {      sb.Append(", ").Append(a[index]);    }  }  sb.Append(']');  return sb.ToString();}

Для тех, кто любит подглядывать, я добавил картинку, чтобы сохранить интригу.

Уверен, многие не смогли найти ошибку без открытия IDE или документации к классу StringBuilder. Ошибка допущена при вызове конструктора:

StringBuilder sb = new StringBuilder('[');

Собственно, об этом и предупреждает статический анализатор PVS-Studio: V3165 Character literal '[' is passed as an argument of the 'Int32' type whereas similar overload with the string parameter exists. Perhaps, a string literal should be used instead. Arrays.cs 193.

Программист хотел создать экземпляр типа StringBuilder, строка в котором будет начинаться с символа '['. Однако из-за опечатки будет получен объект ёмкостью под 91 элемент, не содержащий символов.

Это произошло из-за того, что вместо двойных кавычек использовались одинарные, что привело к вызову не той перегрузки конструктора:

....public StringBuilder(int capacity);public StringBuilder(string? value);....

При вызове конструктора символьный литерал '[' будет неявно приведен к соответствующему значению типа int (91 в Unicode). Это приведет к вызову конструктора с параметром типа int, задающим начальную вместимость. Программист же хотел вызвать конструктор, задающий начало строки.

Для исправления ошибки следует заменить символьный литерал на строковый (т.е. использовать "[", а не '['), что позволит вызвать правильную перегрузку конструктора.

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

Диагностика, срабатывание которой было описано выше, была добавлена в релизе PVS-Studio 7.11. Вы сами можете загрузить последнюю версию анализатора и посмотреть на что способна не только диагностика V3165, но и другие диагностики для языков C, C++, C# и Java.

Кстати, идеи диагностик нам часто предлагают сами пользователи. Так получилось и в этот раз - спасибо пользователю Krypt с ресурса Habr. Если у вас также есть идеи диагностических правил - не стесняйтесь предлагать их!

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

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Valery Komarov. C# Programmer, It's Time to Test Yourself and Find Error.

Подробнее..

Перспективы разработчика в автоматизации тестирования ПО

15.02.2021 10:17:19 | Автор: admin

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

Лирическое отступление

Это только присказка, сказка впереди

В начале 2000-x я работал в IT-компании, которая выполняла несколько аутсорсинговых проектов для компании Integrated Genomics. Проекты были связаны с расшифровкой геномов простейших организмов. К примеру, одна из утилит искала фрагменты (праймеры) с определенными свойствами в геноме кишечной палочки. На входе утилиты была последовательность ДНК, загружаемая из публичной базы геномов ERGO и состоящая из азотистых оснований. На выходе таблица фрагментов и их позиция в цепочке ДНК. Далее эти фрагменты использовались биологами для синтеза геномов. Задача была сравнительно простой. Нужно было лишь позаботиться о том, чтобы программа не выжирала всю оперативную память довольно слабых машин, которые были у нас на тот момент. Сложность других проектов заключалась в том, что они находились на стыке трех дисциплин: биологии, математики и информатики. В тех случаях, когда алгоритм задачи был понятен, его реализация в программном коде не представляла трудности. Но когда сама задача была неопределенной, и не находилось никого кто мог бы ее формализовать, это был серьезный вызов.

Выяснилось, что для того, чтобы успешно решать такие задачи, нужны фундаментальные знания по биологии и высшей математике. Нас, молодых и горячих, это не остановило. Мы нашли англоязычную книгу по биоинформатике профессора Павла Певзнера и приступили к изучению. Поначалу повествование было легким и непринужденным. Во вступлении Павел рассказывал о том, как выживал в Москве в студенческие годы, и это было поистине приятное и расслабляющее чтение. Далее в первых главах речь шла про азотистые основания нуклеотидов ДНК аденин, гуанин, тимин и цитозин и про комплиментарность нуклеиновых кислот, а именно: как основания нуклеотидов способны формировать парные комплексы аденинтимин и гуанинцитозин при взаимодействии цепей нуклеиновых кислот. Было понятно, что такое свойство нуклеотидов играет ключевую роль в репликации ДНК. Я помню, что испытывал подъем и состояние потока, читая это объяснение, и мысленно представлял, как мы сейчас быстренько все это освоим (подумаешь, биология) и сможем брать более серьезные задачи. Мы даже думали о том, что сможем написать свой геномный ассемблер и взяться за расшифровку простейших геномов. Продолжаю читать книгу, и тут бах система уравнений на полстраницы без каких-либо объяснений. Из контекста подразумевалось, что эта система проще пареной репы, и любое объяснение будет оскорблением для читателя. Не было даже сноски для факультативного чтения. Я решил эту страницу пропустить, вернуться к ней позже и пока что продолжить читать дальше вдруг станет понятно. Перелистываю страницу а там еще одна система уравнений уже на всю страницу и скупое описание, часть слов из которого мы не нашли в словаре. Стало понятно, что эту область знаний на стыке геномики и математики нахрапом не возьмешь. Также стало понятно, почему подавляющее большинство коллег, с которыми мы взаимодействовали, имели биологическое и/или математическое образование. В конечном итоге, шаг за шагом погружаясь в основы геномики, нам удалось создать несколько программных продуктов, и тогда я в первый раз всерьез задумался о том насколько результативной и интересной может быть деятельность на стыке нескольких дисциплин.

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

Перспективы разработчика в автоматизации тестирования ПО

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

Исторически сложилось так, что в автоматизацию приходят специалисты с опытом в ручном тестировании ПО. В какой-то момент они осознают, что рутинные операции можно и нужно автоматизировать, либо просто желают попробовать себя в новом качестве. Опыт, привносимый ручными тестировщиками в проект автоматизации, бывает архиполезным. Никто лучше них не знает возможности и слабые места продукта, наиболее трудоемкие сценарии для тестирования, окружение, в котором работает продукт, а также пожелания пользователей и планы на следующие релизы. Как правило, хороший ручной тестировщик четко понимает, что именно он хочет автоматизировать, но с написанием автотестов порой возникают сложности. Почему? Потому что автотесты это такой же программный продукт, как и продукт, который они призваны тестировать. Нужно продумать архитектуру системы автотестов, механизмы их запуска (Continuous Integration, CI) и самое главное нужно писать хороший код. По факту, для ручного тестировщика это зачастую оказывается непросто. Ведь здесь требуется создать полноценный проект, который можно интегрировать в CI, изменять, расширять и переиспользовать. Для этого нужны способности к программированию, накопленный опыт и набитые шишки.

Ручной тестировщик с глубоким пониманием продукта и методик тестирования и разработчик с опытом программирования отлично дополняют друг друга. Первый силен в постановке задачи (use case -> test case), второй в ее реализации. Встает вопрос: почему среди автоматизаторов встречается много ручных тестировщиков? На это есть несколько причин:

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

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

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

Опасение потерять в карьерном росте и зарплате.

Разберем эти моменты.

Автотесты это такой же продукт разработки, как и тот продукт, для которого эти автотесты создаются. Если разработчик идет на позицию автоматизатора в выделенную группу разработки автотестов, он остается на стезе написания программного кода. Да, ему надо иметь представление о тестировании ПО, но базовые знания можно сравнительно быстро получить, пролистав какое-либо руководство по тестированию ПО (например, вот эту книгу). Безусловно, автоматизатору нужно понимать продукт, для которого он пишет автотесты. Но освоить продукт на приемлемом уровне зачастую оказывается легче, чем научиться писать хороший код. Знания о продукте и методиках тестирования будут расширяться по мере ознакомления с багами, написанными на продукт, и общения с ручными тестировщиками. Разработчик не перестанет быть разработчиком, не превратится в ручного тестировщика. Это не его путь. Путь разработчика писать хорошие автотесты.

Задачи у автоматизатора интересные и зачастую сложные. В первую очередь стоит вспомнить про знаменитую пирамиду Фаулера. Модульные, интеграционные, end-to-end тесты подразумевают вдумчивый подход к структуре тестов и выбору инструментов в соответствии с функциональностью продуктов, для которых пишем автотесты. Если говорить о продуктах, разрабатываемых в Veeam, то автоматизатору понадобится работать с REST, WebDriver, Microsoft SQL Server, Amazon Web Services, Microsoft Azure, VMware vCenter, Hyper-V список не исчерпан. У каждого из облаков и гипервизоров свой API и свои скелеты в шкафу. Порой приходится писать код на различных языках программирования, использовать заглушки, семафоры, создавать свои обертки и т.п.

Одну и ту же задачу можно решить по-разному, и автоматизатор ищет наиболее эффективное решение. Вот лишь один из примеров сценарий, реализованный для продукта Veeam ONE. Один из компонентов продукта Business View, который позволяет группировать элементы виртуальной инфраструктуры по различным критериям. Критериев и вариантов их комбинирования очень много, поэтому проверка этой функциональности вручную занимает много времени. Написание автотестов в лоб с имитацией действий ручных тестировщиков было бы неэффективным: тесты для графического интерфейса десктопных приложений, как правило, сложны и трудоемки в разработке, являются хрупкими, их тяжело модифицировать, и выполняются они долго. Мы нашли другое решение: поскольку действия пользователя в UI интерполируются в SQL-запросы к базе данных, мы используем SQL-запросы для создания категорий и групп. Это позволило нам в разумные сроки покрыть автотестами все свойства и операторы, задействованные в Business View.

Как сделать так, чтобы тесты, запускаемые параллельно ради уменьшения общего времени запуска, не мешали друг другу? Как измерить покрытие продукта автотестами? Анализ покрытия кода? Анализ покрытия пользовательских сценариев? Как отслеживать падения сервисов? Как интегрировать автотесты с репортинговыми фреймворками? Как интегрировать автотесты со сборкой билдов в CI? Как прогонять тесты по pull-реквестам? И многое-многое другое.

Нужно обратить внимание на качество самих автотестов. Кто сторожит сторожей? Какому автотесту можно доверять? Автотест должен быть эффективным по критерию количество затраченных на него усилий / полученный результат, автономным, стабильным (нехрупким), быстрым, надежным (никаких false positive и false negative). В автотесте должен быть понятный, хороший код с точки зрения возможностей языка программирования, чтобы этот код можно было легко расширять и изменять.

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

Что в Veeam?

В компании Veeam мы будем рады новым боевым товарищам с опытом программирования на C# (например, web-интерфейсы, десктопные приложения, консольные утилиты и т.п.). У нас в Veeam есть много продуктов. Технологии в них могут различаться. В автотестах для нескольких продуктов мы опираемся на REST и WebDriver. Если у вас нет опыта с этими технологиями, но вы уверенно себя чувствуете в написании кода на C# и питаете интерес к автоматизации тестирования, то, возможно, мы также найдем точки соприкосновения.

Мы будем рады вашему резюме и паре абзацев о том, что вас привлекает в автоматизации, о ваших сильных сторонах и профессиональных планах. Пишите нам на ящик qa@veeam.com внимательно прочитаем. Если укажете в теме письма [Хабр] (например, [Хабр] Позиция автоматизатора), будет плюс в карму =)

Да пребудет с Вами Сила.

Подробнее..

Перевод Что из себя представляет класс Startup и Program.cs в ASP.NET Core

15.02.2021 16:13:26 | Автор: admin

В преддверии старта курса C# ASP.NET Core разработчик подготовили традиционный перевод полезного материала.

Также приглашаем на открытый вебинар по теме
Отличия структурных шаблонов проектирования на примерах. На вебинаре участники вместе с экспертом рассмотрят три структурных шаблона проектирования: Заместитель, Адаптер и Декоратор; а также напишут несколько простых программ и проведут их рефакторинг.


Введение

Program.cs это место, с которого начинается приложение. Файл Program.cs в ASP.NET Core работает так же, как файл Program.cs в традиционном консольном приложении .NET Framework. Файл Program.cs является точкой входа в приложение и отвечает за регистрацию и заполнение Startup.cs, IISIntegration и создания хоста с помощью инстанса IWebHostBuilder, метода Main.

Global.asax больше не входит в состав приложения ASP.NET Core. В ASP.NET Core заменой файла Global.asax является файл Startup.cs.

Файл Startup.cs это также точка входа, и он будет вызываться после выполнения файла Program.cs на уровне приложения. Он обрабатывает конвейер запросов. Класс Startup запускается в момент запуска приложения.

Описание

Что такое Program.cs?

Program.cs это место, с которого начинается приложение. Файл класса Program.cs является точкой входа в наше приложение и создает инстанс IWebHost, на котором размещается веб-приложение.

public class Program {      public static void Main(string[] args) {          BuildWebHost(args).Run();      }      public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args).UseStartup < startup > ().Build();  }  

WebHost используется для создания инстансов IWebHost, IWebHostBuilder и IWebHostBuilder, которые имеют предварительно настроенные параметры. Метод CreateDefaultBuilder() создает новый инстанс WebHostBuilder.

Метод UseStartup() определяет класс Startup, который будет использоваться веб-хостом. Мы также можем указать наш собственный пользовательский класс вместо Startup.

Метод Build() возвращает экземпляр IWebHost, а Run() запускает веб-приложение до его полной остановки.

Program.cs в ASP.NET Core упрощает настройку веб-хоста.

public static IWebHostBuilder CreateDefaultBuilder(string[] args) {      var builder = new WebHostBuilder().UseKestrel().UseContentRoot(Directory.GetCurrentDirectory()).ConfigureAppConfiguration((hostingContext, config) => {          /* setup config */ }).ConfigureLogging((hostingContext, logging) => {          /* setup logging */ }).UseIISIntegration()      return builder;  }

Метод UseKestrel() является расширением, которое определяет Kestrel как внутренний веб-сервер. Kestrel это кроссплатформенный веб-сервер для ASP.NET Core с открытым исходным кодом. Приложение работает с модулем Asp.Net Core, и необходимо включить интеграцию IIS (UseIISIntegration()), которая настраивает базовый адрес и порт приложения.

Он также настраивает UseIISIntegration(), UseContentRoot(), UseEnvironment(Development), UseStartup() и другие доступные конфигурации, например Appsetting.json и Environment Variable. UseContentRoot используется для обозначения текущего пути к каталогу.

Мы также можем зарегистрировать логирование и установить минимальный loglevel, как показано ниже. Это также переопределитloglevel, настроенный в файле appsetting.json.

.ConfigureLogging(logging => { logging.SetMinimumLevel(LogLevel.Warning); }) 

Таким же образом мы можем контролировать размер тела нашего запроса и ответа, включив эту опцию файле program.cs, как показано ниже.

.ConfigureKestrel((context, options) => { options.Limits.MaxRequestBodySize = 20000000; });  

ASP.net Core является кроссплатформенным и имеет открытый исходный код, а также его можно размещать на любом сервере (а не только на IIS, внешнем веб-сервере), таком как IIS, Apache, Nginx и т. д.

Что такое файл Startup?

Обязателен ли файл startup.cs или нет? Да, startup.cs является обязательным, его можно специализировать любым модификатором доступа, например, public, private, internal. В одном приложении допускается использование нескольких классов Startup. ASP.NET Core выберет соответствующий класс в зависимости от среды.

Если существует класс Startup{EnvironmentName}, этот класс будет вызываться для этого EnvironmentName или будет выполнен файл Startup для конкретной среды в целом (Environment Specific), чтобы избежать частых изменений кода/настроек/конфигурации в зависимости от среды.

ASP.NET Core поддерживает несколько переменных среды, таких как Development, Production и Staging. Он считывает переменную среды ASPNETCORE_ENVIRONMENT при запуске приложения и сохраняет значение в интерфейсе среды хоста (into Hosting Environment interface).

Обязательно ли этот класс должен называться startup.cs? Нет, имя класса не обязательно должно быть Startup.

Мы можем определить два метода в Startup файле, например ConfigureServices и Configure, вместе с конструктором.

Пример Startup файла

public class Startup {      // Use this method to add services to the container.      public void ConfigureServices(IServiceCollection services) {          ...      }      // Use this method to configure the HTTP request pipeline.      public void Configure(IApplicationBuilder app) {          ...      }  }  

Метод ConfigureServices

Это опциональный метод в классе Startup, который используется для настройки сервисов для приложения. Когда в приложение поступает какой-либо запрос, сначала вызывается метод ConfigureService.

Метод ConfigureServices включает параметр IServiceCollection для регистрации сервисов. Этот метод должен быть объявлен с модификатором доступа public, чтобы среда могла читать контент из метаданных.

public void ConfigureServices(IServiceCollection services)  {     services.AddMvc();  }  

Метод Configure

Метод Configure используется для указания того, как приложение будет отвечать на каждый HTTP-запрос. Этот метод в основном используется для регистрации промежуточного программного обеспечения (middleware) в HTTP-конвейере. Этот метод принимает параметр IApplicationBuilder вместе с некоторыми другими сервисами, такими как IHostingEnvironment и ILoggerFactory. Как только мы добавим какой-либо сервис в метод ConfigureService, он будет доступен для использования в методе Configure.

public void Configure(IApplicationBuilder app)  {     app.UseMvc();  }

В приведенном выше примере показано, как включить функцию MVC в нашем фреймворке. Нам нужно зарегистрировать UseMvc() в Configure и сервис AddMvc() в ConfigureServices. UseMvc является промежуточным программным обеспечением. Промежуточное программное обеспечение (Middleware) это новая концепция, представленная в Asp.net Core. Для вас доступно множество встроенных промежуточных программ, некоторые из которых указаны ниже.

app.UseHttpsRedirection();  app.UseStaticFiles();  app.UseRouting();  app.UseAuthorization();  UseCookiePolicy();  UseSession(); 

Промежуточное программное обеспечение можно настроить в http-конвейере с помощью команд Use, Run и Map.

Run

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

Use

Это передаст следующему (параметр next) делегату, так что HTTP-запрос будет передан следующему промежуточному программному обеспечению после выполнения текущего, если следующий делегат есть.

Map

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

app.Map("/MyDelegate", MyDelegate);

Чтобы получить более подробную информацию о промежуточном программном обеспечении, переходите сюда

ASP.net Core имеет встроенную поддержку внедрения зависимостей (Dependency Injection). Мы можем настроить сервисы для контейнера внедрения зависимостей, используя этот метод. Следующие способы конфигурационные методы в Startup классе.

AddTransient

Transient (временные) объекты всегда разные; каждому контроллеру и сервису предоставляется новый инстанс.

Scoped

Scoped используются одни и те же объекты в пределах одного запроса, но разные в разных запросах.

Singleton

Singleton объекты одни и те же для каждого объекта и каждого запроса.

Можем ли мы удалить startup.cs и объединить все в один класс с Program.cs?

Ответ: Да, мы можем объединить все классы запуска в один файл.

Заключение

Вы получили базовое понимание того, почему файлы program.cs и startup.cs важны для нашего приложения Asp.net Core и как их можно настроить. Мы также немного познакомились с переменной среды (Environment Variable), внедрение зависимостей, промежуточным программным обеспечением и как его настроить. Кроме того, мы увидели, как можно настроить UseIIsintegration() и UseKestrel().


Узнать подробнее о курсе C# ASP.NET Core разработчик.

Смотреть открытый вебинар по теме Отличия структурных шаблонов проектирования на примерах.

Подробнее..

Перевод Реализуем глобальную обработку исключений в ASP.NET Core приложении

20.02.2021 18:04:03 | Автор: admin

В преддверии старта курса C# ASP.NET Core разработчик подготовили традиционный перевод полезного материала.

Также рекомендуем посмотреть вебинар на тему
Отличия структурных шаблонов проектирования на примерах. На этом открытом уроке участники вместе с преподавателем-экспертом познакомятся с тремя структурными шаблонами проектирования: Заместитель, Адаптер и Декоратор.


Введение

Сегодня в этой статье мы обсудим концепцию обработки исключений в приложениях ASP.NET Core. Обработка исключений (exception handling) одна из наиболее важных импортируемых функций или частей любого типа приложений, которой всегда следует уделять внимание и правильно реализовывать. Исключения это в основном средства ориентированные на обработку рантайм ошибок, которые возникают во время выполнения приложения. Если этот тип ошибок не обрабатывать должным образом, то приложение будет остановлено в результате их появления.

В ASP.NET Core концепция обработки исключений подверглась некоторым изменениям, и теперь она, если можно так сказать, находится в гораздо лучшей форме для внедрения обработки исключений. Для любых API-проектов реализация обработки исключений для каждого действия будет отнимать довольно много времени и дополнительных усилий. Но мы можем реализовать глобальный обработчик исключений (Global Exception handler), который будет перехватывать все типы необработанных исключений. Преимущество реализации глобального обработчика исключений состоит в том, что нам нужно определить его всего лишь в одном месте. Через этот обработчик будет обрабатываться любое исключение, возникающее в нашем приложении, даже если мы объявляем новые методы или контроллеры. Итак, в этой статье мы обсудим, как реализовать глобальную обработку исключений в ASP.NET Core Web API.

Создание проекта ASP.NET Core Web API в Visual Studio 2019

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

  • Откройте Microsoft Visual Studio и нажмите Create a New Project (Создать новый проект).

  • В диалоговом окне Create New Project выберите ASP.NET Core Web Application for C# (Веб-приложение ASP.NET Core на C#) и нажмите кнопку Next (Далее).

  • В окне Configure your new project (Настроить новый проект) укажите имя проекта и нажмите кнопку Create (Создать).

  • В диалоговом окне Create a New ASP.NET Core Web Application (Создание нового веб-приложения ASP.NET Core) выберите API и нажмите кнопку Create.

  • Убедитесь, что флажки Enable Docker Support (Включить поддержку Docker) и Configure for HTTPS (Настроить под HTTPS) сняты. Мы не будем использовать эти функции.

  • Убедитесь, что выбрано No Authentication (Без аутентификации), поскольку мы также не будем использовать аутентификацию.

  • Нажмите ОК.

Используем UseExceptionHandler middleware в ASP.NET Core.

Чтобы реализовать глобальный обработчик исключений, мы можем воспользоваться преимуществами встроенного Middleware ASP.NET Core. Middleware представляет из себя программный компонент, внедренный в конвейер обработки запросов, который каким-либо образом обрабатывает запросы и ответы. Мы можем использовать встроенное middleware ASP.NET Core UseExceptionHandler в качестве глобального обработчика исключений. Конвейер обработки запросов ASP.NET Core включает в себя цепочку middleware-компонентов. Эти компоненты, в свою очередь, содержат серию делегатов запросов, которые вызываются один за другим. В то время как входящие запросы проходят через каждый из middleware-компонентов в конвейере, каждый из этих компонентов может либо обработать запрос, либо передать запрос следующему компоненту в конвейере.

С помощью этого middleware мы можем получить всю детализированную информацию об объекте исключения, такую как стектрейс, вложенное исключение, сообщение и т. д., а также вернуть эту информацию через API в качестве вывода. Нам нужно поместить middleware обработки исключений в configure() файла startup.cs. Если мы используем какое-либо приложение на основе MVC, мы можем использовать middleware обработки исключений, как это показано ниже. Этот фрагмент кода демонстрирует, как мы можем настроить middleware UseExceptionHandler для перенаправления пользователя на страницу с ошибкой при возникновении любого типа исключения.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  {      app.UseExceptionHandler("/Home/Error");      app.UseMvc();  } 

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

[Route("GetExceptionInfo")]  [HttpGet]  public IEnumerable<string> GetExceptionInfo()  {       string[] arrRetValues = null;       if (arrRetValues.Length > 0)       { }       return arrRetValues;  } 

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

app.UseExceptionHandler(                  options =>                  {                      options.Run(                          async context =>                          {                              context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;                              context.Response.ContentType = "text/html";                              var exceptionObject = context.Features.Get<IExceptionHandlerFeature>();                              if (null != exceptionObject)                              {                                  var errorMessage = $"<b>Exception Error: {exceptionObject.Error.Message} </b> {exceptionObject.Error.StackTrace}";                                  await context.Response.WriteAsync(errorMessage).ConfigureAwait(false);                              }                          });                  }              );  

Для проверки вывода просто запустите эндпоинт API в любом браузере:

Определение пользовательского Middleware для обработки исключений в API ASP.NET Core

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

using Microsoft.AspNetCore.Http;    using Newtonsoft.Json;    using System;    using System.Collections.Generic;    using System.Linq;    using System.Net;    using System.Threading.Tasks;        namespace API.DemoSample.Exceptions    {        public class ExceptionHandlerMiddleware        {            private readonly RequestDelegate _next;                public ExceptionHandlerMiddleware(RequestDelegate next)            {                _next = next;            }                public async Task Invoke(HttpContext context)            {                try                {                    await _next.Invoke(context);                }                catch (Exception ex)                {                                    }            }        }    } 

В приведенном выше классе делегат запроса передается любому middleware. Middleware либо обрабатывает его, либо передает его следующему middleware в цепочке. Если запрос не успешен, будет выброшено исключение, а затем будет выполнен метод HandleExceptionMessageAsync в блоке catch. Итак, давайте обновим код метода Invoke, как показано ниже:

public async Task Invoke(HttpContext context)  {      try      {          await _next.Invoke(context);      }      catch (Exception ex)      {          await HandleExceptionMessageAsync(context, ex).ConfigureAwait(false);      }  }  

Теперь нам нужно реализовать метод HandleExceptionMessageAsync, как показано ниже:

private static Task HandleExceptionMessageAsync(HttpContext context, Exception exception)  {      context.Response.ContentType = "application/json";      int statusCode = (int)HttpStatusCode.InternalServerError;      var result = JsonConvert.SerializeObject(new      {          StatusCode = statusCode,          ErrorMessage = exception.Message      });      context.Response.ContentType = "application/json";      context.Response.StatusCode = statusCode;      return context.Response.WriteAsync(result);  } 

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

using Microsoft.AspNetCore.Builder;  using System;  using System.Collections.Generic;  using System.Linq;  using System.Threading.Tasks;    namespace API.DemoSample.Exceptions  {      public static class ExceptionHandlerMiddlewareExtensions      {          public static void UseExceptionHandlerMiddleware(this IApplicationBuilder app)          {              app.UseMiddleware<ExceptionHandlerMiddleware>();          }      }  }  

На последнем этапе, нам нужно включить наше пользовательское middleware в методе Configure класса startup, как показано ниже:

app.UseExceptionHandlerMiddleware();

Заключение

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


Узнать подробнее о курсе C# ASP.NET Core разработчик.

Посмотреть вебинар на тему Отличия структурных шаблонов проектирования на примерах.

Подробнее..

Pythonnet. Как запустить C код из Python

10.05.2021 10:05:19 | Автор: admin

Введение

На сегодняшний день Python является одним из самых популярных языков программирования, но даже это не помогает ему покрыть все потребности программистов. Самый очевидный минус чистого CPython - это его скорость, поэтому некоторые программисты выбирают для своих задач другие языки программирования, а кто-то просто реализует узкие места на C/C++ и подключает их к Python.

Однако бывают случаи, когда есть некая база кода, написанного на C#, а возможности быстро переписать всё на Python/C/C++ нет. Тогда встает вопрос как подключить C# к Python?. Для этого была разработана библиотека pythonnet. В этой статье разберем: как запустить C# код из Python и что из этого может получиться.

Реализация

Для сравнения скорости выполнения C# и Python я буду ссылаться на одну из прошлых статей.

Библиотека pythonnet работает с .dll файлами, поэтому весь код необходимо будет преобразовывать в динамически подключаемые библиотеки. Чтобы создать .dll файл из C# необходимо установить visual studio и при создании проекта указать, что проект будет создан для библиотеки классов (я дал название проекту: MyTestCS, в будущем dll файл будет носить такое же название как и проект):

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

public struct DataGoods    {        public string name;        public int price;        public string unit;        public DataGoods(string name, int price, string unit)        {            this.name = name;            this.price = price;            this.unit = unit;        }    }

Теперь реализуем класс самого магазина. В нем создадим метод для заполнения магазина товарами:

public class ShopClass    {        public string name;        public List<DataGoods> listGoods;        public ShopClass(string name)        {            this.name = name;this.listGoods = new List<DataGoods>();        }        /// <summary>        /// Метод для создания товаров в магазине        /// </summary>        /// <param name="numberGoods"> Количество объектов в магазине </param>        public void createShopClass(int numberGoods) {            List<DataGoods> lGoods = new List<DataGoods>();            for (int i = 0; i < numberGoods; i++) {                lGoods.Add(new DataGoods("телефон", 20000, "RUB"));                lGoods.Add(new DataGoods("телевизор", 45000, "RUB"));                lGoods.Add(new DataGoods("тостер", 2000, "RUB"));            }            this.listGoods = lGoods;        }   }

После того, как класс был создан, приступим к подключению C# кода к Python проекту. Сначала создадим .dll файл из C# проекта (достаточно нажать команду ctrl+shift+B). В папке bin->debug->netstandart2.0 проекта (путь зависит от того, какие конфигурации среды стоят у вас) появится файл с названием проекта и расширением .dll (именно этот файл будет подключаться к программе на Python).

Далее разберемся с проектом на Python. Необходимо установить библиотеку pythonnet, выполнив команду:

pip install pythonnet

В проекте создадим файл main.py, а также поместим библиотеку MyTestCS.dll в папку с проектом:

Теперь можно подключать библиотеку в main.py, для этого сначала импортируем clr (clr позволяет рассматривать пространства имен CLR как пакеты Python):

import clr

Укажем путь до нашего .dll файла:

pathDLL = os.getcwd() + "\\MyTestCS.dll"

Чтобы подгрузить нужную нам библиотеку необходимо прописать следующий код:

clr.AddReference(pathDLL)

После чего можно импортировать модуль и всё, что в нем содержится. Если напрямую сделать импорт MyTestCS:

import MyTestCSprint(MyTestCS)>>> <module 'MyTestCS'>

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

Создадим экземпляр класса ShopClass и DataGoods через Python и обратимся к полям этих классов.

from MyTestCS import ShopClass, DataGoodsshop = ShopClass("Тест магазин")shop.createShopClass(1)goods = DataGoods("чехол для телефона", 500, "RUB")print(shop.name)>>> Тест магазинprint(shop.listGoods)>>> [<MyTestCS.DataGoods object at 0x000001D04C3FE3C8>, <MyTestCS.DataGoods object at 0x000001D04C3FE438>, <MyTestCS.DataGoods object at 0x000001D04C3FE400>]print(shop.listGoods[1].name, shop.listGoods[1].price, shop.listGoods[1].unit)>>> телевизор 45000 RUBprint(goods.name, goods.price, goods.unit)>>> чехол для телефона 500 RUB

Как итог, получилось вызвать код C# из Python и поработать с классами. Теперь протестируем производительность создания 200*100000 товаров через метод createShopClass:

shop = ShopClass("Тест магазин")s = time.time()shop.createShopClass(200 * 100000)print("СОЗДАНИЕ ТОВАРОВ НА C#:", time.time() - s)>>> СОЗДАНИЕ ТОВАРОВ НА C#: 2.9043374061584473

В прошлой статье время создания такого количества товаров заняло примерно 44 секунды. Использование C# вместо Python позволило ускорить этот процесс примерно в 15 раз, что является очень хорошим результатом.

Проблемы

Однако не может же быть всё настолько хорошо, чтобы броситься переписывать куски кода Python на C#. И это так. Попробуем из Python вручную дополнить товарами магазин:

shop = ShopClass("Тест магазин 1")s = time.time()shop.createShopClass(500000)print("СОЗДАЛИ ТОВАР ЧЕРЕЗ C#:", time.time()-s)>>> СОЗДАЛИ ТОВАР ЧЕРЕЗ C#: 0.07325911521911621shop = ShopClass("Тест магазин 2")s = time.time()for _ in range(500000):        goods1 = DataGoods("телефон", 20000, "RUB")        goods2 = DataGoods("телевизор", 45000, "RUB")        goods3 = DataGoods("тостер", 2000, "RUB")        shop.listGoods.extend([goods1, goods2, goods3])print("СОЗДАЛИ ТОВАР ЧЕРЕЗ PYTHON:", time.time()-s)>>> СОЗДАЛИ ТОВАР ЧЕРЕЗ PYTHON: 5.2899720668792725

И проверим аналогичный код, написанный на Python:

istGoods = []class DataGoods2:        def __init__(self, name, price, unit):            self.name = name            self.price = price            self.unit = units = time.time()for _ in range(500000):        goods1 = DataGoods2("телефон", 20000, "RUB")        goods2 = DataGoods2("телевизор", 45000, "RUB")        goods3 = DataGoods2("тостер", 2000, "RUB")        listGoods.extend([goods1, goods2, goods3])print("СОЗДАЛИ PYTHON ОБЪЕКТ:", time.time()-s)>>> СОЗДАЛИ PYTHON ОБЪЕКТ: 1.2972710132598877

Код чистого питона работает быстрее, чем дополнение объекта, созданного из модуля C#. Это связано с тем, что доступ к объектам, написанным на C#, занимает довольно много времени. Чтобы избежать таких проблем, необходимо писать всю логику работы с классом внутри C# кода, и не выносить эту логику в Python. Изменение скорости выполнения кода будет заметно при подсчете суммы всех товаров. Реализуем функцию подсчета суммы товаров на C# (внутри класса ShopClass):

public long getSumGoods() {    long sumGoods = 0;    foreach (DataGoods goods in this.listGoods) {      sumGoods += goods.price;    }    return sumGoods;}

А также на Python:

shop = ShopClass("Магазин 3")shop.createShopClass(1000000)s = time.time()shop.getSumGoods()print("ВРЕМЯ НА СУММУ ТОВАРОВ C#:", time.time()-s)>>> ВРЕМЯ НА СУММУ ТОВАРОВ C#: 0.0419771671295166sumGoods = 0for goods in shop.listGoods:     sumGoods += goods.priceprint("ВРЕМЯ НА СУММУ ТОВАРОВ PYTHON:", time.time()-s)>>> ВРЕМЯ НА СУММУ ТОВАРОВ PYTHON: 6.205681085586548

Python код выполняется гораздо медленнее, чем внутренние методы C#.

Многопоточность

Так как в C# отсутствует GIL, то мне стало интересно протестировать работу многопоточности в C# и попробовать запустить потоки в C# через Python. Для начала протестируем протестируем создание 3х классов ShopClass последовательно и заполним их 3.000.000 товаров:

public class testShop    {        public void testSpeedNoThread(int count)        {            testShopClass(count);            testShopClass(count);            testShopClass(count);        }        public static void testShopClass(int count)        {            ShopClass shop = new ShopClass("Магазин");            shop.createShopClass(count);        }}

Python код для запуска:

tshop = testShop()s = time.time()tshop.testSpeedNoThread(3000000)print("СОЗДАЕМ ПОСЛЕДОВАТЕЛЬНО 3 МАГАЗИНА:", time.time()-s)>>> СОЗДАЕМ ПОСЛЕДОВАТЕЛЬНО 3 МАГАЗИНА: 2.1849117279052734

Дополним класс testShop для работы с потоками новым методом:

public static void testThread(){    ExThread obj = new ExThread();    Thread thr = new Thread(new ThreadStart(obj.mythread1));    Thread thr2 = new Thread(new ThreadStart(obj.mythread1));    Thread thr3 = new Thread(new ThreadStart(obj.mythread1));    thr.Start();    thr2.Start();    thr3.Start();    thr.Join();    thr2.Join();    thr3.Join();}

И создадим новый вспомогательный класс:

public class ExThread{   public void mythread1()     {         ShopClass shop = new ShopClass("Магазин");         shop.createShopClass(3000000);     }}

Запустим Python код для проверки работы потоков:

s = time.time()tshopThread = testShop()tshopThread.testThread()print("СОЗДАЕМ 3 ПОТОКА C# ДЛЯ 3х МАГАЗИНОВ:", time.time()-s)>>> СОЗДАЕМ 3 ПОТОКА C# ДЛЯ 3х МАГАЗИНОВ: 0.6765928268432617

Вывод

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

P.S. есть и другие способы ускорить python, например, написать библиотеку на C/C++ или переписать часть кода на Cython с меньшими проблемами. В данной статье лишь представлена возможность использования C# и Python вместе. Также существует реализация Python для платформы Microsoft.NET под названием IronPython.

Подробнее..

Как мы переосмыслили работу со сценами в Unity

17.10.2020 14:04:36 | Автор: admin

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

Сейчас я вам расскажу о том, как мы написали плагин для Unity на основе пост-процессинга проектов и кодогенератора CodeDom.

Проблема

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

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

Решение

При добавлении сцены в проект, генерируется одноимённый класс с методом Load.

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

Menu.Load();

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

//------------------------------------------------------------------------------// <auto-generated>//     This code was generated by a tool.//     Runtime Version:4.0.30319.42000////     Changes to this file may cause incorrect behavior and will be lost if//     the code is regenerated.// </auto-generated>//------------------------------------------------------------------------------namespace IJunior.TypedScenes{       public class Menu : TypedScene    {        private const string GUID = "a3ac3ba38209c7744b9e05301cbfa453";                public static void Load()        {            LoadScene(GUID);        }    }}

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

namespace IJunior.TypedScenes{    public abstract class TypedScene    {        protected static void LoadScene(string guid)        {            var path = AssetDatabase.GUIDToAssetPath(guid);            SceneManager.LoadScene(path);        }        protected static void LoadScene<T>(string guid, T argument)        {            var path = AssetDatabase.GUIDToAssetPath(guid);            UnityAction<Scene, Scene> handler = null;            handler = (from, to) =>            {                if (to.name == Path.GetFileNameWithoutExtension(path))                {                    SceneManager.activeSceneChanged -= handler;                    HandleSceneLoaders(argument);                }            };            SceneManager.activeSceneChanged += handler;            SceneManager.LoadScene(path);        }        private static void HandleSceneLoaders<T>(T loadingModel)        {            foreach (var rootObjects in SceneManager.GetActiveScene().GetRootGameObjects())            {                foreach (var handler in rootObjects.GetComponentsInChildren<ISceneLoadHandler<T>>())                {                    handler.OnSceneLoaded(loadingModel);                }            }        }    }}

В этой реализации видна ещё одна фишка - передача параметров сценам.

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

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

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

В таком случае мы можем сами создать такой компонент.

using IJunior.TypedScenes;using System.Collections.Generic;using UnityEngine;public class GameLoadHandler : MonoBehaviour, ISceneLoadHandler<IEnumerable<Player>>{    public void OnSceneLoaded(IEnumerable<Player> players)    {        foreach (var player in players)        {            //make avatars        }    }}

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

//------------------------------------------------------------------------------// <auto-generated>//     This code was generated by a tool.//     Runtime Version:4.0.30319.42000////     Changes to this file may cause incorrect behavior and will be lost if//     the code is regenerated.// </auto-generated>//------------------------------------------------------------------------------namespace IJunior.TypedScenes{    public class Game : TypedScene    {        private const string GUID = "976661b7057d74e41abb6eb799024ada";                public static void Load(System.Collections.Generic.IEnumerable<Player> argument)        {            LoadScene(GUID, argument);        }    }}

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

Это не фишка, а скорее недоработка, так как такой функционал быстрее создаст путаницу, нежели будет полезен.

А почему не сделать через N?

Первую версию плагина я осветил на своём YouTube канале в этом видео.

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

Чем плох статический класс с полями, через которые передаются данные для сцены?

Нередко встречаю и такое. Речь идёт о классе по типу этого:

public class GameArguments{    public IEnumerable<Player> Players { get; set; }}

А уже внутри сцены, вероятно, будет группа компонентов, которая достаёт данные из свойств.

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

Ну и опять же сцену придётся запускать по ID или имени.

Чем плох PlayerPerfs

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

Параллель с ASPNet

Мне хотелось получить что-то схожее с строго типизированными View из ASPNet Core. Мы считаем плохим тоном использовать ViewData и стараемся определять ViewModel. В Unity хочется что-то такого же толка с теми же преимуществами.

Отличие Unity в первую очередь в том, что сцена - это обычно более громоздкое предприятие, нежели View в ASPNet. Это решается разбивкой одной сцены на несколько подсцен с режимом загрузки Additive (наш плагин, к слову, его поддерживает), что позволяет скомпоновать сцену из сцен поменьше со своими более атомарными моделями.

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

Где скачать

Плагин мы сделали в паре с Владиславом Койдо в рамках Proof-of-concept. Он ещё не стабилен и не обкатан как следует, но с ним уже можно поиграться.

Репозиторий на GitHub - https://github.com/HolyMonkey/unity-typed-scenes

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

Подробнее..
Категории: C , Gamedev , Разработка игр , Csharp , Unity , Unity3d

Консольная утилита погоды на C с помощью .Net

28.02.2021 12:13:00 | Автор: admin

Что необходимо получить и изучить, чтобы начать получать прогноз погоды на 5 дней?

Во-первых, определиться с поставщиком погодных данных. Во-вторых, разобрать, в каком виде поставляются данные и как мы их можем собирать и отображать с помощью языка программирования C#.

В качестве поставщика погодных данных я выбрал сервис Accuweather. В бесплатной учётной записи на данный момент можно сделать 50 запросов в сутки. Этого достаточно, чтобы можно было посмотреть данные о погоде несколько раз в сутки (даже можно поделиться с другом!).

Для регистрации необходимо пройти по ссылке: https://developer.accuweather.com. После регистрации нужно нажать на кнопку "Add a new App" и заполнить небольшую анкету. По итогу, вы получите ваш личный ApiKey с помощью которого в дальнейшем можно получать обновлённые данные.

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

В разделе "API Reference" самым первым списке установлен раздел "Locations API" с него и начнём. Забегая вперёд, скажу сразу, нельзя так просто взять и отправить в GET запросе название города. Для этого, нам нужно сперва получить Location Key конкретного города. Это значение представлено в виде цифр и уникально для каждого города.

Итак, в разделе Locations API нас интересует метод City Search. Читаем краткое описание к нему: Returns information for an array of cities that match the search text. Сразу берём себе на заметку, что нам возвращается массив с названиями городов.

В странице для запроса вставляем ApiKey, название интересующего города и ставим RU, если хотим получать локализованные данные.

После нажатия на кнопку "Send this request" ниже на странице вы получите результат выполнения. В моём случае он выглядит так:

[  {    "Version": 1,    "Key": "292332",    "Type": "City",    "Rank": 21,    "LocalizedName": "Chelyabinsk",    "EnglishName": "Chelyabinsk",    "PrimaryPostalCode": "",    "Region": {      "ID": "ASI",      "LocalizedName": "Asia",      "EnglishName": "Asia"    },    "Country": {      "ID": "RU",      "LocalizedName": "Russia",      "EnglishName": "Russia"    },    "AdministrativeArea": {      "ID": "CHE",      "LocalizedName": "Chelyabinsk",      "EnglishName": "Chelyabinsk",      "Level": 1,      "LocalizedType": "Oblast",      "EnglishType": "Oblast",      "CountryID": "RU"    },    "TimeZone": {      "Code": "YEKT",      "Name": "Asia/Yekaterinburg",      "GmtOffset": 5,      "IsDaylightSaving": false,      "NextOffsetChange": null    },    "GeoPosition": {      "Latitude": 55.16,      "Longitude": 61.403,      "Elevation": {        "Metric": {          "Value": 233,          "Unit": "m",          "UnitType": 5        },        "Imperial": {          "Value": 764,          "Unit": "ft",          "UnitType": 0        }      }    },    "IsAlias": false,    "SupplementalAdminAreas": [      {        "Level": 2,        "LocalizedName": "Chelyabinsk",        "EnglishName": "Chelyabinsk"      }    ],    "DataSets": [      "AirQualityCurrentConditions",      "AirQualityForecasts",      "Alerts",      "ForecastConfidence"    ]  }]

Как видим, нам вернулся массив из одного города. Как будет выглядеть результат, если городов с одинаковым названием два и больше:

[  {    "Version": 1,    "Key": "294021",    "Type": "City",    "Rank": 10,    "LocalizedName": "Москва",    "EnglishName": "Moscow",    "PrimaryPostalCode": "",    "Region": {      "ID": "ASI",      "LocalizedName": "Азия",      "EnglishName": "Asia"    },    "Country": {      "ID": "RU",      "LocalizedName": "Россия",      "EnglishName": "Russia"    },    "AdministrativeArea": {      "ID": "MOW",      "LocalizedName": "Москва",      "EnglishName": "Moscow",      "Level": 1,      "LocalizedType": "Город федерального подчинения",      "EnglishType": "Federal City",      "CountryID": "RU"    },    "TimeZone": {      "Code": "MSK",      "Name": "Europe/Moscow",      "GmtOffset": 3,      "IsDaylightSaving": false,      "NextOffsetChange": null    },    "GeoPosition": {      "Latitude": 55.752,      "Longitude": 37.619,      "Elevation": {        "Metric": {          "Value": 155,          "Unit": "m",          "UnitType": 5        },        "Imperial": {          "Value": 508,          "Unit": "ft",          "UnitType": 0        }      }    },    "IsAlias": false,    "SupplementalAdminAreas": [      {        "Level": 2,        "LocalizedName": "Tsentralny",        "EnglishName": "Tsentralny"      }    ],    "DataSets": [      "AirQualityCurrentConditions",      "AirQualityForecasts",      "Alerts",      "ForecastConfidence"    ]  },  {    "Version": 1,    "Key": "1397263",    "Type": "City",    "Rank": 85,    "LocalizedName": "Москва",    "EnglishName": "Moskwa",    "PrimaryPostalCode": "",    "Region": {      "ID": "EUR",      "LocalizedName": "Европа",      "EnglishName": "Europe"    },    "Country": {      "ID": "PL",      "LocalizedName": "Польша",      "EnglishName": "Poland"    },    "AdministrativeArea": {      "ID": "10",      "LocalizedName": "Лодзинское воеводство",      "EnglishName": "d",      "Level": 1,      "LocalizedType": "Воеводство",      "EnglishType": "Voivodship",      "CountryID": "PL"    },    "TimeZone": {      "Code": "CET",      "Name": "Europe/Warsaw",      "GmtOffset": 1,      "IsDaylightSaving": false,      "NextOffsetChange": "2021-03-28T01:00:00Z"    },    "GeoPosition": {      "Latitude": 51.816,      "Longitude": 19.657,      "Elevation": {        "Metric": {          "Value": 238,          "Unit": "m",          "UnitType": 5        },        "Imperial": {          "Value": 780,          "Unit": "ft",          "UnitType": 0        }      }    },    "IsAlias": false,    "SupplementalAdminAreas": [      {        "Level": 2,        "LocalizedName": "Восточно-Лодзинский повят",        "EnglishName": "d East"      },      {        "Level": 3,        "LocalizedName": "Новосольна",        "EnglishName": "Nowosolna"      }    ],    "DataSets": [      "AirQualityCurrentConditions",      "AirQualityForecasts",      "Alerts",      "ForecastConfidence",      "FutureRadar",      "MinuteCast",      "Radar"    ]  },  {    "Version": 1,    "Key": "580845",    "Type": "City",    "Rank": 85,    "LocalizedName": "Москва",    "EnglishName": "Moskva",    "PrimaryPostalCode": "",    "Region": {      "ID": "ASI",      "LocalizedName": "Азия",      "EnglishName": "Asia"    },    "Country": {      "ID": "RU",      "LocalizedName": "Россия",      "EnglishName": "Russia"    },    "AdministrativeArea": {      "ID": "KIR",      "LocalizedName": "Киров",      "EnglishName": "Kirov",      "Level": 1,      "LocalizedType": "Республика",      "EnglishType": "Republic",      "CountryID": "RU"    },    "TimeZone": {      "Code": "MSK",      "Name": "Europe/Moscow",      "GmtOffset": 3,      "IsDaylightSaving": false,      "NextOffsetChange": null    },    "GeoPosition": {      "Latitude": 57.968,      "Longitude": 49.104,      "Elevation": {        "Metric": {          "Value": 207,          "Unit": "m",          "UnitType": 5        },        "Imperial": {          "Value": 678,          "Unit": "ft",          "UnitType": 0        }      }    },    "IsAlias": false,    "SupplementalAdminAreas": [      {        "Level": 2,        "LocalizedName": "Verkhoshizhemsky",        "EnglishName": "Verkhoshizhemsky"      }    ],    "DataSets": [      "AirQualityCurrentConditions",      "AirQualityForecasts",      "Alerts",      "ForecastConfidence"    ]  },  {    "Version": 1,    "Key": "2488304",    "Type": "City",    "Rank": 85,    "LocalizedName": "Москва",    "EnglishName": "Moskva",    "PrimaryPostalCode": "",    "Region": {      "ID": "ASI",      "LocalizedName": "Азия",      "EnglishName": "Asia"    },    "Country": {      "ID": "RU",      "LocalizedName": "Россия",      "EnglishName": "Russia"    },    "AdministrativeArea": {      "ID": "PSK",      "LocalizedName": "Псков",      "EnglishName": "Pskov",      "Level": 1,      "LocalizedType": "Область",      "EnglishType": "Oblast",      "CountryID": "RU"    },    "TimeZone": {      "Code": "MSK",      "Name": "Europe/Moscow",      "GmtOffset": 3,      "IsDaylightSaving": false,      "NextOffsetChange": null    },    "GeoPosition": {      "Latitude": 57.449,      "Longitude": 29.185,      "Elevation": {        "Metric": {          "Value": 161,          "Unit": "m",          "UnitType": 5        },        "Imperial": {          "Value": 528,          "Unit": "ft",          "UnitType": 0        }      }    },    "IsAlias": false,    "SupplementalAdminAreas": [      {        "Level": 2,        "LocalizedName": "Porkhovsky",        "EnglishName": "Porkhovsky"      }    ],    "DataSets": [      "AirQualityCurrentConditions",      "AirQualityForecasts",      "Alerts",      "Radar"    ]  },  {    "Version": 1,    "Key": "580847",    "Type": "City",    "Rank": 85,    "LocalizedName": "Москва",    "EnglishName": "Moskva",    "PrimaryPostalCode": "",    "Region": {      "ID": "ASI",      "LocalizedName": "Азия",      "EnglishName": "Asia"    },    "Country": {      "ID": "RU",      "LocalizedName": "Россия",      "EnglishName": "Russia"    },    "AdministrativeArea": {      "ID": "TVE",      "LocalizedName": "Тверь",      "EnglishName": "Tver'",      "Level": 1,      "LocalizedType": "Область",      "EnglishType": "Oblast",      "CountryID": "RU"    },    "TimeZone": {      "Code": "MSK",      "Name": "Europe/Moscow",      "GmtOffset": 3,      "IsDaylightSaving": false,      "NextOffsetChange": null    },    "GeoPosition": {      "Latitude": 56.918,      "Longitude": 32.163,      "Elevation": {        "Metric": {          "Value": 251,          "Unit": "m",          "UnitType": 5        },        "Imperial": {          "Value": 823,          "Unit": "ft",          "UnitType": 0        }      }    },    "IsAlias": false,    "SupplementalAdminAreas": [      {        "Level": 2,        "LocalizedName": "Penovsky",        "EnglishName": "Penovsky"      }    ],    "DataSets": [      "AirQualityCurrentConditions",      "AirQualityForecasts",      "Alerts"    ]  }]

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

Итак, представление о том, с чем нам нужно работать с начала есть. Теперь, осталось переложить всё в C#. Для большего интереса, я буду программировать на языке C# и используя редактор VSCodium. Всё это работает на OpenSuSe Leap 15.2.

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

Класс, который описывает UserApi:

namespace habraweatherappconsole{    public class UserApi    {        public string UserApiProperty { get;set; }    }}

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

        /// <summary>        /// Метод реализуе возможность восстановления списка APIKey в памяти        /// </summary>        public static void ReadUserApiToLocalStorage()        {            XmlSerializer xmlSerializer = new XmlSerializer(typeof(ObservableCollection<UserApi>));            try            {                using (StreamReader sr = new StreamReader("UserApi.xml"))                {                    userApiList = xmlSerializer.Deserialize(sr) as ObservableCollection<UserApi>;                }            }            catch(Exception ex)            {                /* Не вывожу никаких сообщений об ошибке. Потому как, если утилита была запущена впервые                / то файла скорее всего нет. Даже если бы он был и из-за каких-то аппаратных проблем стал недоступен                / то что я могу с этим поделать в таком случае?                */            }        }

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

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

Итак, что мы видим в верхушке получаемого объекта:

    "Version": 1,    "Key": "292332",    "Type": "City",    "Rank": 21,    "LocalizedName": "Chelyabinsk",    "EnglishName": "Chelyabinsk",    "PrimaryPostalCode": "",

Мы видим версию API в виде числа, уникальный ключ города в виде числа, тип населённого пункта в виде строки, Ранг (не смог найти, что это значит) в виде числа, оригинальное и локализованное название города в виде строки и почтовый индекс. Почтовый индекс вернулся пустой, поэтому, беру строку из-за её универсальности.

Следовательно, реализовываем эту часть так:

    public class RootBasicCityInfo    {        public int Version { get; set; }         public string Key { get; set; }         public string Type { get; set; }         public int Rank { get; set; }         public string LocalizedName { get; set; }         public string EnglishName { get; set; }         public string PrimaryPostalCode { get; set; } 

Так же, в json объекте присутствует и другие сведения об искомом городе:

      "Region": {        "ID": "ASI",        "LocalizedName": "Азия",        "EnglishName": "Asia"      },      "Country": {        "ID": "RU",        "LocalizedName": "Россия",        "EnglishName": "Russia"      },      "AdministrativeArea": {        "ID": "MOW",        "LocalizedName": "Москва",        "EnglishName": "Moscow",        "Level": 1,        "LocalizedType": "Город федерального подчинения",        "EnglishType": "Federal City",        "CountryID": "RU"      },

Поэтому, там же реализуем классы для Region, Country, AdministrativeArea примерно так:

public class Region    {        public string ID { get; set; }         public string LocalizedName { get; set; }         public string EnglishName { get; set; }     }    public class Country    {        public string ID { get; set; }         public string LocalizedName { get; set; }         public string EnglishName { get; set; }     }    public class AdministrativeArea    {        public string ID { get; set; }         public string LocalizedName { get; set; }         public string EnglishName { get; set; }         public int Level { get; set; }         public string LocalizedType { get; set; }         public string EnglishType { get; set; }         public string CountryID { get; set; }     }

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

    public class RootBasicCityInfo    {        public int Version { get; set; }         public string Key { get; set; }         public string Type { get; set; }         public int Rank { get; set; }         public string LocalizedName { get; set; }         public string EnglishName { get; set; }         public string PrimaryPostalCode { get; set; }         public Region Region { get; set; }         public Country Country { get; set; }         public AdministrativeArea AdministrativeArea { get; set; }         public TimeZone TimeZone { get; set; }         public GeoPosition GeoPosition { get; set; }         public bool IsAlias { get; set; }         public List<SupplementalAdminArea> SupplementalAdminAreas { get; set; }         public List<string> DataSets { get; set; }     }

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

using System;using System.Collections.ObjectModel;using System.Net;using System.Text.Json;using static System.Console;namespace habraweatherappconsole{    /// <summary>    /// Класс описывает возможность получения поиска    /// и добавления городов для их последующего мониторинга.    /// </summary>    public static class SearchCity    {        /// <summary>        /// Метод реализует возможность получения списка городов.        /// В качестве формального параметра принимается название города        /// которое должно быть указано в классе MainMenu.        /// </summary>        /// <param name="formalCityName"></param>        public static void GettingListOfCitiesOnRequest(string formalCityName)        {            // Получаю ApiKey из списка            string apiKey = UserApiManager.userApiList[0].UserApiProperty;            try            {                string jsonOnWeb = $"http://dataservice.accuweather.com/locations/v1/cities/search?apikey={apiKey}&q={formalCityName}";                WebClient webClient = new WebClient();                string prepareString = webClient.DownloadString(jsonOnWeb);                ObservableCollection<RootBasicCityInfo> rbci = JsonSerializer.Deserialize<ObservableCollection<RootBasicCityInfo>>(prepareString);                DataRepo.PrintКeceivedСities(rbci);            }            catch (Exception ex)            {                WriteLine("Неполучилось отобразить запрашиваемый город."                + "Возможные причины: \n" +                 "* Неправильно указано название города\n"                + "* Нет доступа к интернету\n"                + "Подробнее ниже: \n"                + ex.Message);            }        }    }}

Реализую метод, который выводит на экран полученный список:

        /// <summary>        /// Метод реализует возможность отображать список запрашиваемых городов        /// (Если городов с таким названием больше, чем 1).        /// </summary>        /// <param name="formalListOfCityes"></param>        public static void PrintКeceivedСities (ObservableCollection<RootBasicCityInfo> formalListOfCityes)        {            string pattern = "=====\n" + "Номер в списке: {0}\n" + "Название в оригинале: {1}\n"            + "В переводе:  {2} \n" + "Страна: {3}\n" + "Административный округ: {4}\n"            + "Тип: {5}\n" + "====\n";            int numberInList = 0;            foreach (var item in formalListOfCityes)            {                WriteLine(pattern, numberInList.ToString(),                item.EnglishName, item.LocalizedName, item.Country.LocalizedName,                item.AdministrativeArea.LocalizedName, item.AdministrativeArea.LocalizedType);                numberInList++;            }            Write ("Номер какого города добавить в мониторинг: ");            int num = Convert.ToInt32(Console.ReadLine());            try            {                listOfCityForMonitorWeather.Add(formalListOfCityes[num]);            }            catch (Exception ex)            {                WriteLine("Похоже, вы ошиблись цифрой.\n");                WriteLine(ex.Message);            }            WriteListOfCityMonitoring();        }

Метод, который реализует запись списка со всеми отслеживаемыми городами на диск (чтобы при повторном запуске не тратить драгоценный APIKey)

         /// <summary>        /// Метод реализует возможность записывать список отслеживаемых городов        /// на жёсткий диск.        /// </summary>        private static void WriteListOfCityMonitoring()        {            XmlSerializer xmlSerializer = new XmlSerializer(typeof(ObservableCollection<RootBasicCityInfo>));            using (StreamWriter sw = new StreamWriter("RootBasicCityInfo.xml"))            {                xmlSerializer.Serialize(sw, listOfCityForMonitorWeather);            }        }

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

Поскольку данные о погоде это отдельный Json объект, то по аналогии с тем, как была разобрана информация о искомом городе, нужно разобрать информацию о погоде в этом городе.

Accuweather может предоставить информацию на 1 текущий день, 5, 10 и 15 дней. В ответ будет приходить json объект одного и того же типа. Разница будет только в Get запросе и количестве возвращаемых дней.

Пример того, что возвращается в json объекте:

  "Headline": {    "EffectiveDate": "2021-02-23T07:00:00+03:00",    "EffectiveEpochDate": 1614052800,    "Severity": 3,    "Text": "Окончание понижения температуры: Среда",    "Category": "cold",    "EndDate": "2021-02-24T19:00:00+03:00",    "EndEpochDate": 1614182400,    "MobileLink": "http://m.accuweather.com/ru/ru/moscow/294021/extended-weather-forecast/294021?unit=c",    "Link": "http://www.accuweather.com/ru/ru/moscow/294021/daily-weather-forecast/294021?unit=c"  },  "DailyForecasts": [    {      "Date": "2021-02-23T07:00:00+03:00",      "EpochDate": 1614052800,      "Temperature": {        "Minimum": {          "Value": -24.4,          "Unit": "C",          "UnitType": 17        },        "Maximum": {          "Value": -20.6,          "Unit": "C",          "UnitType": 17        }      },      "Day": {        "Icon": 31,        "IconPhrase": "Холодно",        "HasPrecipitation": false      },      "Night": {        "Icon": 31,        "IconPhrase": "Холодно",        "HasPrecipitation": false      },      "Sources": [        "AccuWeather"      ],      "MobileLink": "http://m.accuweather.com/ru/ru/moscow/294021/daily-weather-forecast/294021?day=1&unit=c",      "Link": "http://www.accuweather.com/ru/ru/moscow/294021/daily-weather-forecast/294021?day=1&unit=c"    },

Следовательно, класс должен выглядеть примерно так:

    public class DailyForecast    {        public DateTime Date { get; set; }         public int EpochDate { get; set; }         public Temperature Temperature { get; set; }         public Day Day { get; set; }         public Night Night { get; set; }         public List<string> Sources { get; set; }         public string MobileLink { get; set; }         public string Link { get; set; }     }         public class RootWeather    {        public Headline Headline { get; set; }         public List<DailyForecast> DailyForecasts { get; set; }     }

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

            string pattern = "=====\n" + "Номер в списке: {0}\n" + "Название в оригинале: {1}\n"            + "В переводе:  {2} \n" + "Страна: {3}\n" + "Административный округ: {4}\n"            + "Тип: {5}\n" + "====\n";            int numberInList = 0;            foreach (var item in DataRepo.listOfCityForMonitorWeather)            {                WriteLine(pattern, numberInList.ToString(),                item.EnglishName, item.LocalizedName, item.Country.LocalizedName,                item.AdministrativeArea.LocalizedName, item.AdministrativeArea.LocalizedType);                numberInList++;            }                        bool ifNotExists = false;            string cityKey = null;            int num = 0;            do            {                ifNotExists = false;                Write("Номер города для просмотра погоды: ");                num = Convert.ToInt32(Console.ReadLine());                                if (num < 0 || num > DataRepo.listOfCityForMonitorWeather.Count - 1)                {                    WriteLine("Такого номера нет. Попробуйте ещё раз.");                    ifNotExists = true;                }            } while(ifNotExists);                        cityKey = DataRepo.listOfCityForMonitorWeather[num].Key;

А затем получаю информацию о погоде:

 // Получаю ApiKey из списка            string apiKey = UserApiManager.userApiList[0].UserApiProperty;                        string jsonUrl = $"http://dataservice.accuweather.com/forecasts/v1/daily/5day/{cityKey}?apikey={apiKey}&language=ru&metric=true";            jsonUrl = webClient.DownloadString(jsonUrl);            RootWeather weatherData = JsonSerializer.Deserialize<RootWeather>(jsonUrl);            string patternWeather = "=====\n" + "Дата: {0}\n" + "Температура минимальная: {1}\n"            +"Температура максимальная: {2}\n" + "Примечание на день: {3}\n" + "Примечание на ночь: {4}\n" + "====\n";            foreach (var item in weatherData.DailyForecasts)            {                WriteLine(patternWeather, item.Date, item.Temperature.Minimum.Value,                item.Temperature.Maximum.Value, item.Day.IconPhrase, item.Night.IconPhrase);            }

Таким образом будет получена и напечатана информация о погоде на следующие 5 дней.

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

Спасибо за уделённое время, удачи!

Подробнее..
Категории: C , Net , Csharp

Создание превью картинок в объектном хранилище с помощьюYandex Cloud Functions

17.03.2021 10:08:48 | Автор: admin

Довольно распространенная задача создание превью картинок для сайта из полноразмерных изображений. Автоматизируем этот процесс с помощью триггера для Yandex Object Storage с функцией в Yandex Cloud Functions, которую он будет запускать с наступлением определенного события в бакете в нашем случае, появлением в нем картинки. Функция сделает из нее превью и сохранит в соседний бакет. Возможна вариация сохранения превью в тот же бакет, но с другим префиксом.

Также с помощью триггера в Yandex.Cloud можно задавать действия не только для объектов в хранилище, но и для событий в очереди.

Триггер для Object Storage и Cloud Functions

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

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

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

Превью на лету

Но у такого решения есть пара ограничений.

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

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

Воспользуемся новой возможностью Yandex.Cloud - runtime для функций C#, чтобы доработать нашу функцию на csharp.

Теперь схема будет работать так.

При первичном обращении за превью:

  1. Пользователь запрашивает картинку, указав в URL ее желаемые размеры.

  2. Объектное хранилище не находит нужного файла и согласно настроенным правилам, переадресует запрос в функцию.

  3. Функция идет за оригинальным изображением в объектное хранилище.

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

  5. Сразу отдает готовый файл пользователю.

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

Особенности технической реализации

Прописываем правила переадресации для бакета, чтобы Object Storage и Cloud Functions смогли работать в связке.

s3.putBucketWebsite({ Bucket: "%bucket_name%", WebsiteConfiguration: { IndexDocument: { Suffix: "index.html" }, RoutingRules: [ { Condition: { HttpErrorCodeReturnedEquals: "404", KeyPrefixEquals: "images/" }, Redirect: { HttpRedirectCode: "302", HostName: "functions.yandexcloud.net", Protocol: "https", ReplaceKeyPrefixWith: "%function_id%?path=", } } ] }})

Если объектное хранилище не найдет (HttpErrorCodeReturnedEquals: "404") запрошенный файл с указанным KeyPrefixEquals, то применит указанный ниже редирект. Подробнее можно посмотреть в документации к AWS S3, а скачать готовый код функции можно в репозитории тут.

В Yandex.Cloud удобно реализовано создание функций с помощью CLI. Вам не надо архивировать код и загружать его в объектное хранилище, достаточно лишь сложить все файлы в директорию и указать на нее при создании версии функции в ключе --source-path. Так же вы можете не передавать все node_modules, а загрузить только package.json и выбрать --runtime nodejs12 или --runtime nodejs14. Все зависимости будут подтянуты в момент создания версии функции.

Обращаться к фалам надо не по обычному хосту %bucket_name%.storage.yandexcloud.net, так как редиректы обрабатываться не будут, а через %bucket_name%.website.yandexcloud.net/PREFIX/%width%x%height%/%path%.

Например, при обращении к %bucket_name%.website.yandexcloud.net/images/500x500/cats/meow.png вы получите картинку которую положили в бакет %bucket_name% по ключу images/cats/meow.png но отмасштабированную до размеров 500x500px.

Чтобы не выстрелить себе в ногу

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

Так же, если у вас очень много изображений, можно задать некоторое значение TTL, создать LRU схему кэширования.

P.S.

Любые вопросы по Serverless и Yandex Cloud Functions обсуждаем у нас в Telegram: Yandex Serverless Ecosystem

Полезные ссылки:

Подробнее..
Категории: C , S3 , Csharp , Yandex.cloud , Serverless , Faas

Уменьшить размер консольного .NET 5.0 приложения

23.03.2021 10:07:12 | Автор: admin

Target Framework Moniker

Давайте знакомиться. В .NET 5.0 для использования Windows Forms или WPF нам недостаточно просто указать net5.0:

<PropertyGroup>  <TargetFramework>net5.0</TargetFramework>  <UseWindowsForms>true</UseWindowsForms></PropertyGroup>

При попытке использования Windows Forms или WPF мы получаем ошибку

C:\Program Files\dotnet\sdk\5.0.201\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.DefaultItems.targets(369,5): error NETSDK1136: The target platform must be set to Windows (usually by including '-windows' in the TargetFramework property) when using Windows Forms or WPF, or referencing projects or packages that do so.

Решение, как подсказывает ошибка состоит в указании Target Framework Moniker

<PropertyGroup>  <TargetFramework>net5.0-windows</TargetFramework>  <UseWindowsForms>true</UseWindowsForms></PropertyGroup>

Как это работает

При сборки автоматически импортируются файлы из Microsoft.NET.Sdk\targets.
Далее в dotnet\sdk\5.0\Sdks\Microsoft.NET.Sdk.WindowsDesktop\targets\Microsoft.NET.Sdk.WindowsDesktop.props содержиться код:

    <FrameworkReference Include="Microsoft.WindowsDesktop.App" IsImplicitlyDefined="true"                        Condition="('$(UseWPF)' == 'true') And ('$(UseWindowsForms)' == 'true')"/>    <FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" IsImplicitlyDefined="true"                        Condition="('$(UseWPF)' == 'true') And ('$(UseWindowsForms)' != 'true')"/>    <FrameworkReference Include="Microsoft.WindowsDesktop.App.WindowsForms" IsImplicitlyDefined="true"                        Condition="('$(UseWPF)' != 'true') And ('$(UseWindowsForms)' == 'true')"/>

Где проблема

Дело в том, что FrameworkReference это транзитивная зависимость: Документ дизайна .NET , Документация NuGet

Это значит, что если у нас есть где-то сборка, которая использует тип из Windows Forms или из WPF мы должны будем все зависимые сборки перевести на 'net5.0-windows'.

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

Если весь код использует Windows Forms или WPF проблемы нет, а если мы в итоге создаём консольное приложение то получаем дополнительные 60МБ при создании самодостаточного файла.

Пример

Библиотека

using System.Windows.Forms;namespace Library{    public class Demo    {        void ShowForm()        {            var f = new Form();            f.Show();        }    }}

Консольное приложение

using System;class Program{    public static void Main()    {        Console.WriteLine("Hello World!");    }}

Обратим внимание, что мы не используем класс Library.Demo.

Соберём самодостаточное приложение с помощью dotnet publish:

dotnet publish ConsoleApp.csproj --self-contained -c Release -r win-x64 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:IncludeAllContentForSelfExtract=true

Результат 81,8МБ !

Благодаря IncludeAllContentForSelfExtract при запуске приложение в %TEMP%\.net мы можем посмотреть из чего оно состоит.

Как же так ?
Мы не использовали Library.Demo, мы указали PublishTrimmed, а Windows Forms к нам пришёл.

Решение

Как видим dotnet publish при обычных настройках не справился со своей работой, поэтому поможем ему !

Шаг 1

В библиотеке укажем вручную зависимость фреймворка и попросим не создавать транзитивную зависимость:

<Project Sdk="Microsoft.NET.Sdk">  <PropertyGroup>    <TargetFramework>net5.0</TargetFramework>    <!-- Укажем зависимости явно -->    <DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>  </PropertyGroup>  <ItemGroup>    <!-- .NET Runtime -->    <!-- В данном случае неважно будет PrivateAssets="all" или нет, всегда добавляется при сборке -->    <FrameworkReference Include="Microsoft.NETCore.App" />        <!-- Windows Desktop -->    <!-- PrivateAssets="all" - зависимость не идёт дальше -->    <FrameworkReference Include="Microsoft.WindowsDesktop.App" PrivateAssets="all"  />        <!-- Можно указать и более конкретно:      Microsoft.WindowsDesktop.App.WPF      Microsoft.WindowsDesktop.App.WindowsForms -->  </ItemGroup></Project>

Документация для DisableImplicitFrameworkReference

Ключевая часть PrivateAssets="all". которая не даёт зависимости распространяться дальше.

Шаг 2

Меняем .net5.0-windows на .net5.0 в нашем консольном приложении

Результат

Собираем той же командой:

dotnet publish ConsoleApp.csproj --self-contained -c Release -r win-x64 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:IncludeAllContentForSelfExtract=true

Получаем файл размером всего 18.8МБ со следующим содержимым

Заключение

Стоит ли делать так в библиотеках ?
Однозначно да !
С одной стороны это позволяет использовать типы из Windows Forms или WPF, с другой стороны у сборщика получается выкинуть всё неиспользованное и выдать меньший размер файла.

Подробнее..
Категории: C , Net , Csharp , Net 5

Pure DI для .NET

16.04.2021 22:16:20 | Автор: admin

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

  • API для определения графа зависимостей

  • композиция объектов

  • управление жизненным циклом объектов

Мне было интересно разобраться как это работает, а лучший способ сделать это - написать свою библиотеку внедрения зависимостей IoC.Container. Она позволяет делать сложные вещи простыми способами: неплохо работает с общими типами - другие так не могут, позволяет создавать код, без зависимостей на инфраструктуру и обеспечивает хорошую производительность, по сравнению с другими похожими решениям, но НЕ по сравнению с чистым DI подходом.

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

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

Что если оставить только лучшее от этих подходов:

  • определять граф зависимостей используя простой API

  • эффективная композиция объектов как при чистом DI

  • решение проблем и "нестыковок" на этапе компиляции

На мой взгляд, эти задачи мог бы на себя взять язык программирования. Но пока большинство языков программирования заняты копированием синтаксического сахара друг у друга, предлагается следующее не идеальное решение - использование генератора кода. Входными данными для него будут .NET типы, ориентированные на внедрение зависимостей и метаданные/API описания графа зависимостей. А результатом работы будет автоматически созданный код для композиции объектов, который проверяется и оптимизируется компилятором и JIT.

Итак, представляю вам бета-версию библиотеки Pure.DI! Цель этой статьи - собрать фидбек, идеи как сделать ее лучше. На данный момент библиотека состоит из двух NuGet пакетов beta версии, с которыми уже можно поиграться:

  • первый пакет Pure.DI.Contracts это API чтобы дать инструкции генератору кода как строить граф зависимостей

  • и генератор кода Pure.DI

Пакет Pure.DI.Contracts не содержит выполняемого кода, его можно использовать в проектах .NET Framework начиная с версии 3.5, для всех версий .NET Standard и .NET Core и, кончено, же для проектов .NET 5 и 6, в будущем можно добавить поддержку и .NET Framework 2, если это будет актуально. Все типы и методы этого пакета это API, и нужны исключительно для того, чтобы описать граф зависимостей, используя обычный синтаксис языка C#. Основная часть этого API был позаимствована у IoC.Container.

.NET 5 source code generator и Roslyn стали основой для генератора кода из пакета Pure.DI. Анализ метаданных и генерация происходит на лету каждый раз когда вы редактируете свой код в IDE или автоматически, когда запускаете компиляцию своих проектов или решений. Генерируемый код не отсвечивается рядом с обычным кодом и не попадает в системы контроля версий. Пример ниже показывает, как это работает.

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

interface IBox<out T> { T Content { get; } }interface ICat { State State { get; } }enum State { Alive, Dead }

Реализация этой абстракции для кота Шрёдингера может быть такой:

class CardboardBox<T> : IBox<T>{    public CardboardBox(T content) => Content = content;    public T Content { get; }}class ShroedingersCat : ICat{  // Суперпозиция состояний  private readonly Lazy<State> _superposition;  public ShroedingersCat(Lazy<State> superposition) =>    _superposition = superposition;  // Наблюдатель проверяет кота   // и в этот момент у кота появляется состояние  public State State => _superposition.Value;  public override string ToString() => $"{State} cat";}

Это код предметной области, который не зависит от инфраструктурного кода. Он написан по технологии DI, а в идеале SOLID.

Композиция объектов создается в модуле с инфраструктурным кодом, который отвечает за хостинг приложения, поближе к точке входа. В этот модуль и необходимо добавить ссылки на пакеты Pure.DI.Contracts и Pure.DI. И в нём же следует оставить генератору кода подсказки как строить граф зависимостей:

static partial class Glue{  // Моделирует случайное субатомное событие,  // которое может произойти, а может и не произойти  private static readonly Random Indeterminacy = new();  static Glue()  {    DI.Setup()      // Квантовая суперпозиция двух состояний      .Bind<State>().To(_ => (State)Indeterminacy.Next(2))      // Абстрактный кот будет котом Шрёдингера      .Bind<ICat>().To<ShroedingersCat>()      // Коробкой будет обычная картонная коробка      .Bind<IBox<TT>>().To<CardboardBox<TT>>()      // А корнем композиции тип в котором находится точка входа      // в консольное приложение      .Bind<Program>().As(Singleton).To<Program>();  }}

Первый вызов статического метода Setup() класса DI начинает цепочку подсказок. Если он находится в static partial классе, то весь сгенерированный код станет частью этого класса, иначе будет создан новый статический класс с постфиксом DI. Метод Setup() имеет необязательный параметр типа string чтобы переопределить имя класса для генерации на свое. В примере в настройке использована переменная Indeterminacy, поэтому класс Glue является static partial, чтобы сгенерированный код мог ссылаться на эту переменную из сгенерированной части класса.

Следующие за Setup() пары методов Bind<>() и To<>() определяют привязки типов зависимостей к их реализациям, например в:

.Bind().To()

ICat - это тип зависимости, им может быть интерфейс, абстрактный класс и любой другой не статический .NET тип. ShroedingersCat - это реализация, ей может быть любой не абстрактный не статический .NET тип. По моему мнению, типами зависимостей лучше делать интерфейсы, так как они имеют ряд преимуществ по сравнению с абстрактными классами. Одна из них - это возможность реализовать одним классом сразу несколько интерфейсов, а потом использовать его для нескольких типов зависимостей. И так, в Bind<>() мы определяем тип зависимости, а в To<>() его реализацию. Между этими методами могут быть и другие методы для управления привязкой:

  • дополнительные методы Bind<>(), если нужно привязать более одного типа зависимости к одной и той же реализации

  • метод As(Lifetime) для определения времени жизни объекта, их может быть много в цепочке, но учитывается последний

  • и метод Tag(object), который принимает тег привязки, их может быть несколько на одну привязку, и учитываются все

На данный момент существует несколько времен жизни, а их список будет пополняться:

  • Transient - значение по умолчанию, когда объект типа будет создаваться каждый раз

  • Singleton - будет создан единственный объект типа, в отличие от классических контейнеров здесь это будет статическое поле статического класса

  • PerThread - будет создано по одному объекту типа на поток

  • PerResolve - в процессе одной композиции объект типа будет переиспользован

  • Binding - позволяет использовать свою реализацию интерфейса ILifetime в качестве времени жизни

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

.Bind().Tag(Fat).Tag(Fluffy).To()

Обратите внимание, что методы Bind<>() и To<>() - это универсальные методы. Каждый содержит один параметр типа для определения типа зависимости, и ее реализации соответственно. Вместо неудобных открытых типов, как например, typeof(IBox<>) в API применяются маркеры универсальных типов, как TT. В нашем случае абстрактная коробка - это IBox, а ее картонная реализация это CardboardBox. Почему открытые типы менее удобны? Потому что по открытым типа невозможно однозначно определить требуемую для внедрения реализацию, в случае когда параметры универсальных типов это другие универсальные типы. Помимо обычных маркеров TT, TT1, TT2 и т.д. в API можно использовать маркеры типов с ограничениями. Их достаточно много в наборе готовых маркеров. Если нужен свой маркер c ограничениями, создайте свой тип и добавьте атрибут [GenericTypeArgument] и он станет маркером универсального типа, например:

[GenericTypeArgument]public class TTMy: IMyInterface { }

Метод To<>() завершает определение привязки. В общем случае реализация будет создаваться автоматически. Будет найден конструктор, пригодный для внедрения через конструктор с максимальным количеством параметров. При его выборе предпочтения будут отданы конструкторам без атрибута [Obsolete]. Иногда нужно переопределить то, как будет создаваться объект или, предположим, вызвать дополнительно еще какой-то метод инициализации. Для этого можно использовать другую перегрузку метода To<>(factory). Например, чтобы создать картонную коробку самостоятельно, привязка

.Bind<IBox>().To<CardboardBox>()

может быть заменена на

.Bind<IBox>().To(ctx => new CardboardBox(ctx.Resolve()))

To<>(factory) принимает аргументом lambda функцию, которая создает объект. А единственный аргумент lambda функции, здесь это - ctx, помогает внедрить зависимости самостоятельно. Генератор в последствии заменит вызов ctx.Resolve() на создание объекта типа TT на месте для лучшей производительности. Помимо метода Resolve() возможно использовать другой метод с таким же названием, но с одним дополнительным параметром - тегом типа object.

Время открывать коробки!

class Program{  // Создаем граф объектов в точке входа  public static void Main() => Glue.Resolve<Program>().Run();  private readonly IBox<ICat> _box;  internal Program(IBox<ICat> box) => _box = box;  private void Run() => Console.WriteLine(_box);}

В точке входа void Main() вызывается метод Glue.Resolve<Program>() для создания всей композиции объектов. Этот пример соответствует шаблону Composition Root, когда есть единственное такое место, очень близкое к точке входа в приложение, где выполняется композиция объектов, граф объектов четко определен, а типы предметной области не связаны с инфраструктурой. В идеальном случае метод Resolve<>() должен использоваться один раз в приложении для создания сразу всей композиции объектов:

static class ProgramSingleton{  static readonly Program Shared =     new Program(      new CardboardBox<ICat>(        new ShroedingersCat(          new Lazy<State>(            new Func<State>(              (State)Indeterminacy.Next(2))))));}

Вследствии того, что привязка для Program определена со временем жизни Singleton то метод Resolve<>() для типа Program каждый раз будет возвращать единственный объект этого типа. О многопоточности и ленивой инициализации не стоит беспокоится, так как этот объект будет создан гарантированно один раз и только при первой загрузке типа в момент первого обращения к статическому полю Shared класса статического приватного класса ProgramSingleton, вложенного в класс Glue.

Есть еще несколько интересных вопросов, которые хотелось бы обсудить. Обратите внимание, что конструктор кота Шрёдингера

ShroedingersCat(Lazy<State> superposition)

требует внедрения зависимости типа Lazy<> из стандартной библиотеки классов .NET. Как же это работает, когда в примере не определена привязка для Lazy<>? Дело в том, что пакет Pure.DI из коробки содержит набор привязок BCL типов. Конечно же, любую привязку можно переопределить при необходимости. Метод DependsOn(), с именем набора в качестве аргумента, позволяет использовать свои наборы привязок.

Другой важный вопрос, какую зависимость нужно внедрить чтобы иметь возможность создавать множество объектов определенного типа и делать это в некоторый момент времени? Все просто - Func<>, как и другие BCL типы поддерживается из коробки. Например, если вместо ICat, тип-потребитель запросит зависимость Func<ICat>, то станет возможным получить столько котов сколько и когда нужно.

Еще одна задача. Есть несколько реализаций с разными тегами, требуется получить все их. Чтобы получить всех котов, требуемую для внедрения зависимость можно определить как IEnumerable<ICat>, ICat[] или другой интерфейс коллекций из библиотеки классов .NET, например IReadOnlyCollection<T>. Конечно же, для IEnumerable<ICat> коты будут создаваться лениво.

Для простого примера, как кот Шрёдингера, такого API будет вполне достаточно. Для других случаев помогут привязки To<>(factory) c lambda функцией в аргументе, где объект можно создать вручную, хотя они не всегда удобны.

Рассмотрим ситуацию, когда есть множество реализаций для внедряемой зависимости, тип-потребитель имеет предпочтения по реализациям и мы хотим использовать автоматические привязки. В API есть все необходимое. Каждая реализация отмечается уникальным тегом в привязке, а внедряемая зависимость, например в параметре конструктора, отмечена атрибутом TagAttribute:

  • привязка: .Bind<ICat>().Tag(Fat).Tag(Fluffy).To<FatCat>()

  • и конструктор потребителя: BigBox([Tag(Fat)] T content) { }

Помимо TagAttribute есть другие предопределенные атрибуты:

  • TypeAttribute - когда нужно обозначить тип внедряемой зависимости вручную, не полагаясь, на тип параметра конструктора, метода, свойства или поля

  • OrderAttribute - для методов, свойств и полей указывается порядок вызова/инициализации

  • OrderAttribute - для конструкторов указывается на сколько один предпочтительнее других

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

  • TypeAttribute<>()

  • TagAttribute<>()

  • OrderAttribute<>()

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

Теперь немного о генераторе и генерируемом коде. Генератор кода, используя синтаксические деревья и семантические модели Roslyn API, отслеживает изменения исходного кода в IDE на лету, строит граф зависимостей и генерирует эффективный код для композиции объектов. При анализе выявляются ошибки или недочеты и компилятор получает соответствующие уведомления. Например, при обнаружении циклической зависимости появится ошибка в IDE или в терминале командной строки при сборке проекта, которая не позволит компилировать код, пока она не будет устранена. Будет указано предположительное место проблемы. В другой ситуации, например, когда генератор не сможет найти требуемую зависимость, он выдаст ошибку компиляции с описанием проблемы. В этом случае необходимо либо добавить привязку для требуемой зависимости, либо переопределить fallback стратегию: привязать интерфейс IFallback к своей реализации. Её метод Resolve<>() вызывается каждый раз когда не удается найти зависимость и: возвращает созданный объект для внедрения, бросает исключение или возвращает значение null для того чтобы оставить поведение по умолчанию. Когда fallback стратегия будет привязана генератор изменит ошибку на предупреждение, полагая что ситуация под вашим контролем, и код станет компилируемым.

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

Подробнее..
Категории: C , Net , Csharp , Di , Inversion

Перевод Как мы взломали шифрование пакетов в BattlEye

20.04.2021 20:11:13 | Автор: admin

Недавно Battlestate Games, разработчики Escape From Tarkov, наняли BattlEye для реализации шифрования сетевых пакетов, чтобы мошенники не могли перехватить эти пакеты, разобрать их и использовать в своих интересах в виде радарных читов или иным образом. Сегодня подробно расскажем, как мы взломали их шифрование спустя несколько часов.


Анализ EFT

Мы начали с анализа самого "Escape from Tarkov". В игре используется Unity Engine, который, в свою очередь, использует C# промежуточный язык, а это означает, что можно очень легко просмотреть исходный код игры, открыв его в таких инструментах, как ILDasm или dnSpy. В этом анализе мы работали с dnSpy.

Unity Engine без опции IL2CPP генерирует игровые файлы и помещает их в GAME_NAME_Data\Managed, в нашем случае это EscapeFromTarkov_Data\Managed. Эта папка содержит все использующие движок зависимости, включая файл с кодом игры Assembly-CSharp.dll, мы загрузили этот файл в dnSpy, а затем искали строку encryption и оказались здесь:

Этот сегмент находится в классе EFT.ChannelCombined, который, как можно судить по переданным ему аргументам, работает с сетью:

Правый клик по переменной channelCombined.bool_2, которая регистрируется как индикатор того, было ли включено шифрование, а затем клик по кнопке Analyze показывают нам, что на эту переменную ссылаются два метода:

Второй из них тот, в котором мы сейчас находимся, так что, дважды щёлкнув по первому, мы окажемся здесь:

Вуаля!Есть вызов BEClient.EncryptPacket, клик по методу приведёт к классу BEClient, его мы можем препарировать и найти метод DecryptServerPacket. Этот метод вызывает функцию pfnDecryptServerPacket в библиотеке BEClient_x64.dll. Она расшифрует данные в пользовательском буфере и запишет размер расшифрованного буфера в предоставленный вызывающим методом указатель.

pfnDecryptServerPacket не экспортируется BattlEye и не вычисляется EFT, на самом деле он поставляется инициализатором BattlEye, который в какой-то момент вызывается игрой. Нам удалось вычислить RVA (Relative Virtual Address), загрузив BattlEye в свой процесс и скопировав то, как игра инициализирует его. Код этой программы лежит здесь.

Анализ BattlEye

В последнем разделе мы сделали вывод, что, чтобы выполнить все свои криптографические задачи, EFT вызывает BattlEye. Так что теперь речь идёт о реверс-инжинеринге не IL, а нативного кода, что значительно сложнее.

BattlEye использует защитный механизм под названием VMProtect, который виртуализирует и изменяет указанные разработчиком сегменты. Чтобы правильно выполнить реверс-инжинеринг защищённого этим обфускатором бинарника, нужно распаковать его.

Распаковка это дамп образа процесса во время выполнения; мы сделали дамп, загрузив его в локальный процесс, а затем поработав в Scylla, чтобы сбросить его память на диск.

Открытие этого файла в IDA, а затем переход к процедуре DecryptServerPacket приведут нас к функции, которая выглядит так:

Это называется vmentry, она добавляет на стек vmkey, а затем вызывает обработчика виртуальной машины vminit. Хитрость вот в чём: из-за того, что инструкции виртуализированы VMProtect, они понятны только самой программе.

К счастью для нас, участник Секретного Клуба can1357 сделал инструмент, который полностью ломает эту защиту, VTIL; его вы найдёте здесь.

Выясняем алгоритм

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

Вот эквивалент в псевдо-Си:

uint32_t flag_check = *(uint32_t*)(image_base + 0x4f8ac);if (flag_check != 0x1b)goto 0x20e445;elsegoto 0x20e52b;

VTIL использует свой собственный набор инструкций, чтобы ещё больше упростить код. Я перевёл его на псевдо-Си.

Мы анализируем эту процедуру, войдя в 0x20e445, который является переходом к 0x1a0a4a, в самом начале этой функции они перемещают sr12 копию rcx (первый аргумент в соглашении о вызове x64 по умолчанию) и хранят его на стеке в [rsp+0x68], а ключ xor в [rsp+0x58]. Затем эта процедура переходит к 0x1196fd, вот он:

И вот эквивалент в псевдо-Си:

uint32_t xor_key_1 = *(uint32_t*)(packet_data + 3) ^ xor_key;(void(*)(uint8_t*, size_t, uint32_t))(0x3dccb7)(packet_data, packet_len, xor_key_1);

Обратите внимание, что rsi это rcx, а sr47 это копия rdx. Так как это x64, они вызывают 0x3dccb7 с аргументами в таком порядке: (rcx, rdx, r8). К счастью для нас, vxcallq во VTIL означает вызов функции, приостановку виртуального выполнения, а затем возврат в виртуальную машину, так что 0x3dccb7 не виртуализированная функция! Войдя в эту функцию в IDA и нажав F5, вы вызовете сгенерированный декомпилятором псевдокод:

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

Эта функция расшифровывает пакет в несмежные 4-байтовые блоки, начиная с 8-го байта, с помощью ключа шифра rolling xor.

Примечание от переводчика:

Rolling xor шифр, при котором операция xor буквально прокатывается [отсюда rolling] по байтам:

  • Первый байт остаётся неизменным.

  • Второй байт это результат xor первого и второго оригинальных байтов.

  • Третий байт результат XOR изменённого второго и оригинального третьего байтов и так далее. Реализация здесь.

Продолжая смотреть на ассемблер, мы поймём, что она вызывает здесь другую процедуру:

Эквивалент на ассемблере x64:

mov t225, dword ptr [rsi+0x3]mov t231, byte ptr [rbx]add t231, 0xff ; uhoh, overflow; the following is psuedomov [$flags], t231 u< rbx:8not t231movsx t230, t231mov [$flags+6], t230 == 0mov [$flags+7], t230 < 0movsx t234, rbxmov [$flags+11], t234 < 0mov t236, t234 < 1mov t235, [$flags+11] != t236and [$flags+11], t235mov rdx, sr46 ; sr46=rdxmov r9, r8sbb eax, eax ; this will result in the CF (carry flag) being written to EAXmov r8, t225mov t244, raxand t244, 0x11 ; the value of t244 will be determined by the sbb from above, it'll be either -1 or 0 shr r8, t244 ; if the value of this shift is a 0, that means nothing will happen to the data, otherwise it'll shift it to the right by 0x11mov rcx, rsimov [rsp+0x20], r9mov [rsp+0x28], [rsp+0x68]call 0x3dce60

Прежде чем мы продолжим разбирать вызываемую им функцию, мы должны прийти к следующему выводу: сдвиг бессмыслен из-за того, что флаг переноса не установлен, а это приводит к возвращаемому из инструкции sbb значению 0; в свою очередь, это означает, что мы не на правильном пути.

Если поискать ссылки на первую процедуру 0x1196fd, то увидим, что на неё действительно ссылаются снова, на этот раз с другим ключом!

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

Теперь, когда мы разобрались с реальным ключом xor и аргументами к 0x3dce60, которые расположены в таком порядке: (rcx, rdx, r8, r9, rsp+0x20, rsp+0x28). Переходим к этой функции в IDA, нажимаем F5 и теперь прочитать её очень легко:

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

Заключение

Это шифрование было не самым сложным для реверс-инжиниринга, и наши усилия, безусловно, были замечены BattlEye; через 3 дня шифрование было изменено на TLS-подобную модель, где для безопасного обмена ключами AES используется RSA. Это делает MITM без чтения памяти процесса неосуществимым во всех смыслах и целях.

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

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Pure.DI следующий шаг

25.04.2021 16:15:44 | Автор: admin

Недавно в этом посте вы познакомились с библиотекой Pure.DI. Этот пакет с анализатором/генератором кода .NET 5 задумывался как помощник, который пишет простой код для композиции объектов в стиле чистого DI, используя подсказки для построения графа зависимостей. Он следит за изменениями, анализирует типы и зависимости между ними, подсвечивает проблемы и предлагает пути решения. Важно отметить, что библиотека Pure.DI - это не контейнер внедрения зависимостей, в её задачи входит:

  • анализ графа зависимостей

  • определение в нем проблем и путей их решения

  • создание эффективного кода для композиции объектов

По обсуждениям в предыдущем посте у меня сложилось впечатление, что необходимо решить следующие вопросы:

  • добавить возможность использовать Pure.DI в инфраструктуре ASP.NET

  • убрать бинарную зависимость на API из пакета Pure.DI.Contracts

  • увеличить производительность для случаев, когда операция Resolve() выполняется многократно

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

Описание графа зависимостей находится в этом классе:

DI.Setup()  .Bind<IDispatcher>().As(Singleton).To<Dispatcher>()  .Bind<IClockViewModel>().To<ClockViewModel>()  .Bind<ITimer>().As(Scoped).To(_ => new Timer(TimeSpan.FromSeconds(1)))  .Bind<IClock>().As(ContainerSingleton).To<SystemClock>();

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

services.AddClockDomain();

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

  • ContainerSingleton - чтобы использовать один объект типа на ASP.NET контейнер

  • Scoped - чтобы использовать по одному объекту типа на каждый ASP.NET scope

Сейчас их нельзя использовать вне контекста ASP.NET, иначе появится ошибка компиляции с информацией об этом.

Для решения вопроса о нежелательной бинарной зависимости на API я удалил пакет Pure.DI.Contracts. Теперь весь API для описания графа зависимостей генерируются на месте и является частью инфраструктурного кода проекта, где этот API и используется. Как итог, в проекты не добавляется ни одной бинарной зависимости, а единственная зависимость типа analyzers на пакет Pure.DI будет использована только во время компиляции и забыта сразу после. И, конечно, ее можно использовать без ограничения в любых проектах, не опасаясь зависеть от чего-то лишнего.

ASP.NET инфраструктура вызывает метод Resolve() для каждого запроса. Чтобы уменьшить накладные расходы на этот вызов, был оптимизирован код, ответственный за сопоставление типа корневого элемента композиции объектов к методу создания этой композиции. С результатами сравнительных тестов можно ознакомиться здесь. Хотелось бы подчеркнуть, что в этом сравнении используется спорный способ получения показателей производительности. Поэтому, эти результаты, дают приблизительную оценку накладных расходов на многократный вызов метода Resolve().

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

Подробнее..
Категории: C , Net , Csharp , Dependency injection , Di

Как WCF сам себе в ногу стреляет посредством TraceSource

21.06.2021 14:12:41 | Автор: admin

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

Предыстория

В дистрибутиве PVS-Studio есть одна утилита под названием CLMonitor.exe, или система мониторинга компиляции. Она предназначена для "бесшовной" интеграции статического анализа PVS-Studio для языков C и C++ в любую сборочную систему. Сборочная система должна использовать для сборки файлов один из компиляторов, поддерживаемых анализатором PVS-Studio. Например: gcc, clang, cl, и т.п.

Стандартный сценарий работы данной Windows утилиты очень простой, всего 3 шага:

  1. Выполняем 'CLMonitor.exe monitor';

  2. Выполняем сборку проекта;

  3. Выполняем 'CLMonitor.exe analyze'.

Первый шаг запускает 'сервер', который начинает отслеживать все процессы компиляторов в системе до тех пор, пока его не остановят. Как только мы запустили сервер выполняем сборку проекта, который мы хотим проанализировать. Если сборка прошла, то нужно запустить анализ. Для этого мы исполняем третий шаг. 'CLMonitor.exe analyze' запускает 'клиент', который говорит серверу: "Всё, хватит, выключайся и давай сюда результаты мониторинга за процессами". В этот момент сервер должен завершить свою работу, а клиент запустить анализ. Подробнее о том, как внутри работает система мониторинга и зачем сервер вообще собирает процессы, мы поговорим чуть позже.

И вот в один прекрасный момент описанный сценарий не заработал, анализ просто-напросто не запустился. Ну и чтобы было поинтереснее, возникла эта проблема не у нас, а у пользователя, который обратился к нам в поддержку. У него стабильно после запуска анализа происходило десятиминутное ожидание ответа от сервера с дальнейшим выходом из программы по timeout'у. В чём причина непонятно. Проблема не воспроизводится. Да... беда. Пришлось запросить дампфайл для процесса нашей утилиты, чтобы посмотреть, что там происходит внутри.

Примечание. Проблема у пользователя возникла при использовании Windows утилиты CLMonitor.exe. Поэтому все дальнейшие примеры будут актуальны именно для Windows.

Как работает CLMonitor.exe

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

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

Зачем мы вообще отлавливаем процессы

Как вы поняли, история начинается с того, что нужно запустить сервер, который будет отлавливать все процессы. Делаем мы это не просто так. Вообще, более удобный способ проанализировать C++ проект это прямой запуск анализатора через утилиту командной строки PVS-Studio_Cmd. У неё, однако, есть существенное ограничение она может проверять только проекты для Visual Studio. Дело в том, что для анализа требуется вызывать компилятор, чтобы он препроцессировал проверяемые исходные файлы, ведь анализатор работает именно с препроцессированными файлами. А чтобы вызвать препроцессор, нужно знать:

  • какой конкретно компилятор вызывать;

  • какой файл препроцессировать;

  • параметры препроцессирования.

Утилита PVS-Studio_Cmd узнает все необходимое из проектного файла (*.vcxproj). Однако это работает только для "обычных" MSBuild проектов Visual Studio. Даже для тех же NMake проектов мы не можем получить необходимую анализатору информацию, потому что она не хранится в самом проектном файле. И это несмотря на то, что NMake также является .vcxproj. Сам проект при этом является как бы обёрткой для другой сборочной системы. Тут в игру и вступают всяческие ухищрения. Например, для анализа Unreal Engine проектов мы используем прямую интеграцию с *Unreal Build Tool * сборочной системой, используемой "под капотом". Подробнее здесь.

Поэтому, для того чтобы можно было использовать PVS-Studio независимо сборочной системы, даже самой экзотической, у нас и появилась утилита CLMonitor.exe. Она отслеживает все процессы во время сборки проекта и отлавливает вызовы компиляторов. А уже из вызовов компиляторов мы получаем всю необходимую информацию для дальнейшего препроцессирования и анализа. Теперь вы знаете, зачем нам нужно мониторить процессы.

Как клиент запускает анализ

Для обмена данными между сервером и клиентом мы используем программный фреймворк WCF (Windows Communication Foundation). Давайте далее кратко опишем, как мы с ним работаем.

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

static ErrorLevels PerformMonitoring(....) {  using (ServiceHost host = new ServiceHost(                       typeof(CLMonitoringContract),                          new Uri[]{new Uri(PipeCredentials.PipeRoot)}))   {    ....    host.AddServiceEndpoint(typeof(ICLMonitoringContract),                             pipe,                             PipeCredentials.PipeName);    host.Open();         ....  }}

Обратите тут внимание на две вещи: *CLMonitoringContract *и ICLMonitoringContract.

*ICLMonitoringContract * это сервисный контракт. *CLMonitoringContract * реализация сервисного контракта. Выглядит это так:

[ServiceContract(SessionMode = SessionMode.Required,                  CallbackContract = typeof(ICLMonitoringContractCallback))]interface ICLMonitoringContract{  [OperationContract]  void StopMonitoring(string dumpPath = null);} [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]class CLMonitoringContract : ICLMonitoringContract{  public void StopMonitoring(string dumpPath = null)  {    ....    CLMonitoringServer.CompilerMonitor.StopMonitoring(dumpPath);  } }

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

public void FinishMonitor(){  CLMonitoringContractCallback сallback = new CLMonitoringContractCallback();  var pipeFactory = new DuplexChannelFactory<ICLMonitoringContract>(           сallback,            pipe,            new EndpointAddress(....));  ICLMonitoringContract pipeProxy = pipeFactory.CreateChannel();  ((IContextChannel)pipeProxy).OperationTimeout = new TimeSpan(24, 0, 0);  ((IContextChannel)pipeProxy).Faulted += CLMonitoringServer_Faulted;  pipeProxy.StopMonitoring(dumpPath);}

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

Всё, теперь вы, хоть немного, имеете представление о внутренней работе утилиты CLMonitor.exe.

Просмотр дамп файла и осознание проблемы

Возвращаемся к нашим баранам. Мы остановились на том, что пользователь отправил нам дамп файлы процессов. Напомню, что у пользователя происходило зависание при попытке запустить анализ процессы клиента и сервера оставались висеть, закрытия сервера не происходило. Плюс ровно через 10 минут выводилось такое сообщение:

**Интересный факт. **Откуда вообще взялись эти 10 минут? Дело в том, что мы задаем время ожидания ответа от сервера намного больше, а именно - 24 часа, как видно в примере кода, приведённом выше. Однако для некоторых операций фреймворк сам решает, что это слишком много и он успеет быстрее. Поэтому берет только часть от изначального значения.

Мы попросили у пользователя снять дамп с двух процессов (клиент и сервер) минуток через 5 после запуска клиента, чтобы посмотреть, что там происходит.

Тут небольшая пауза. Хочется быть честным по отношению к моему коллеге Павлу и упомянуть, что это именно он разобрался в данной проблеме. Я же ее просто чинил, ну и вот сейчас описываю :) Конец паузы.

Дамп 'клиента'

Так вот, когда мы открыли дамп файл клиента, перед глазами предстал следующий список потоков:

Нас интересует главный поток. Он висит на методе, отвечающем за запрос остановки сервера:

public void FinishMonitor(){  ....  ICLMonitoringContract pipeProxy = pipeFactory.CreateChannel();  ((IContextChannel)pipeProxy).OperationTimeout = new TimeSpan(24, 0, 0);  ((IContextChannel)pipeProxy).Faulted += CLMonitoringServer_Faulted;  pipeProxy.StopMonitoring(dumpPath);            // <=  ....}

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

Дамп 'сервера'

Открываем его и видим следующий список потоков:

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

Получается, что у пользователя запускается очень много процессов, которые, конкретно для нас, являются 'мусором'. Ну допустим, что это так. Однако данная картина все равно выглядит очень подозрительно. Почему же таких потоков так много? Ведь, по идее, логирование должно происходить быстро. Очень похоже на то, что все эти потоки висят на какой-то точке синхронизации или критической секции и чего-то ждут. Давайте зайдем на ReferenceSource и посмотрим исходный код метода TraceEvent.

Открываем исходники и действительно видим в методе TraceEvent оператор lock:

Мы предположили, что из-за постоянной синхронизации и логирования накапливается большое количество вызовов методов TraceEvent, ждущих освобождения TraceInternal.critSec. Хм, ну допустим. Однако это пока не объясняет, почему сервер не может ответить клиенту. Посмотрим еще раз в дамп файл сервера и заметим один одинокий поток, который висит на методе DiagnosticsConfiguration.Initialize:

В данный метод мы попадаем из метода NegotiateStream.AuthenticateAsServer, выполняющего проверку подлинности со стороны сервера в соединении клиент-сервер:

В нашем случае клиент-серверное взаимодействие происходит с помощью WCF. Плюс напоминаю, что клиент ждет ответ от сервера. По этому стеку очень похоже, что метод DiagnosticsConfiguration.Initialize был вызван при запросе от клиента и теперь висит и ждет. Хм... а давайте-ка зайдем в его исходный код:

И тут мы замечаем, что в данном методе имеется критическая секция, да еще и на ту же самую переменную. Посмотрев, что вообще такое этот critSec, увидим следующее:

Собственно, у нас уже есть достаточно информации, чтобы подвести итоги.

Интересный факт. Изучая просторы интернета в поисках информации про данную проблему с TraceEvent была обнаружена интересная тема на GitHub. Она немного о другом, но есть один занимательный комментарий от сотрудника компании Microsoft:

"Also one of the locks, TraceInternal.critSec, is only present if the TraceListener asks for it. Generally speaking such 'global' locks are not a good idea for a high performance logging system (indeed we don't recommend TraceSource for high performance logging at all, it is really there only for compatibility reasons)".

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

Итоги изучения дампов

Итак, что мы имеем:

  1. Клиент общается с сервером с помощью фреймворка WCF.

  2. Клиент не может получить ответа от сервера. После 10 минут ожидания он падает по тайм-ауту.

  3. На сервере висит множество потоков на методе TraceEvent и всего один - на Initialize.

  4. Оба метода зависят от одной и той же переменной в критической секции, притом это статическое поле.

  5. Потоки, в которых выполняется метод TraceEvent, бесконечно появляются и из-за lock не могут быстро сделать свое дело и исчезнуть. Тем самым они долго не отпускают объект в lock.

  6. Метод Initialize возникает при попытке клиента завершить работу сервера и висит бесконечно на lock.

Из этого можно сделать вывод, что сервер получил команду завершения от клиента. Чтобы начать выполнять метод остановки работы сервера, необходимо установить соединение и выполнить метод Initialize. Данный метод не может выполниться из-за того, что объект в критической секции держат методы TraceEvent, которые в этот момент выполняются на сервере. Появление новых TraceEvent'ов не прекратится, потому что сервер продолжает работать и отлавливать новые 'мусорные' процессы. Получается, что клиент никогда не получит ответа от сервера, потому что сервер бесконечно логирует отловленные процессы с помощью TraceEvent. Проблема найдена!

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

Теперь остается только воспроизвести и починить проблему.

Воспроизведение проблемы

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

private void CrazyLogging(){  for (var i = 0; i < 30; i++)  {    var j = i;    new Thread(new ThreadStart(() =>    {      while (!Program.isStopMonitor)        Logger.TraceEvent(TraceEventType.Error, 0, j.ToString());    })).Start();  }}

За работу сервера у нас отвечает метод Trace, поэтому добавляем наше логирование в него. Например, вот сюда:

public void Trace(){  ListenersInitialization();  CrazyLogging();  ....}

Готово. Запускаем сервер (я буду это делать с помощью Visual Studio 2019), приостанавливаем секунд через 5 процесс и смотрим что у нас там с потоками:

Отлично! Теперь запускаем клиент (TestTraceSource.exe analyze), который должен установить связь с сервером и остановить его работу.

Запустив, мы увидим, что анализ не начинается. Поэтому опять останавливаем потоки в Visual Studio и видим ту же самую картину из дамп файла сервера. А именно появился поток, который висит на методе DiagnosticsConfiguration.Initialize. Проблема воспроизведена.

Как же её чинить? Ну для начала стоит сказать, что TraceSource это класс, который предоставляет набор методов и свойств, позволяющих приложениям делать трассировку выполнения кода и связывать сообщения трассировки с их источником. Используем мы его потому, что сервер может быть запущен не приаттаченным к консоли, и консольное логирование будет бессмысленно. В этом случае мы логировали всё в Event'ы операционной системы с помощью метода TraceSource.TraceEvent.

Проблему мы "решили" следующим образом. По умолчанию теперь вся информация логируется в консоль, используя метод Console.WriteLine. В большинстве случаев, даже если эта логируемая информация и будет потеряна из-за не приаттаченной консоли, она всё равно не требуется для решения задач в работе утилиты, а проблема исчезла. Плюс изменения заняли какие-то минуты. Однако мы оставили возможность старого способа логирования с помощью специального флага enableLogger.

Код, воспроизводящий проблему

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

Чтобы запустить имитирование работы сервера, запустите .exe с флагом trace. Чтобы запустить клиент, воспользуйтесь флагом analyze.

**Примечание: **количество потоков в методе CrazyLogging следует подбирать индивидуально. Если проблема у вас не воспроизводится, то попробуйте поиграться с этим значением. Также можете запустить данный проект в Visual Studio в режиме отладки.

Точка входа в программу:

using System.Linq;namespace TestTraceSource{  class Program  {    public static bool isStopMonitor = false;    static void Main(string[] args)    {      if (!args.Any())        return;      if (args[0] == "trace")      {        Server server = new Server();        server.Trace();      }      if (args[0] == "analyze")      {        Client client = new Client();        client.FinishMonitor();      }    }    }}

Сервер:

using System;using System.Diagnostics;using System.ServiceModel;using System.Threading;namespace TestTraceSource{  class Server  {    private static TraceSource Logger;    public void Trace()    {      ListenersInitialization();      CrazyLogging();      using (ServiceHost host = new ServiceHost(                          typeof(TestTraceContract),                           new Uri[]{new Uri(PipeCredentials.PipeRoot)}))      {        host.AddServiceEndpoint(typeof(IContract),                                 new NetNamedPipeBinding(),                                 PipeCredentials.PipeName);        host.Open();        while (!Program.isStopMonitor)        {          // We catch all processes, process them, and so on        }        host.Close();      }      Console.WriteLine("Complited.");    }    private void ListenersInitialization()    {      Logger = new TraceSource("PVS-Studio CLMonitoring");      Logger.Switch.Level = SourceLevels.Verbose;      Logger.Listeners.Add(new ConsoleTraceListener());      String EventSourceName = "PVS-Studio CL Monitoring";      EventLog log = new EventLog();      log.Source = EventSourceName;      Logger.Listeners.Add(new EventLogTraceListener(log));    }    private void CrazyLogging()    {      for (var i = 0; i < 30; i++)      {        var j = i;        new Thread(new ThreadStart(() =>        {          var start = DateTime.Now;          while (!Program.isStopMonitor)            Logger.TraceEvent(TraceEventType.Error, 0, j.ToString());        })).Start();      }    }   }}

Клиент:

using System;using System.ServiceModel;namespace TestTraceSource{  class Client  {    public void FinishMonitor()    {      TestTraceContractCallback сallback = new TestTraceContractCallback();      var pipeFactory = new DuplexChannelFactory<IContract>(                                сallback,                                new NetNamedPipeBinding(),                                new EndpointAddress(PipeCredentials.PipeRoot                                                   + PipeCredentials.PipeName));      IContract pipeProxy = pipeFactory.CreateChannel();      pipeProxy.StopServer();      Console.WriteLine("Complited.");        }  }}

Прокси:

using System;using System.ServiceModel;namespace TestTraceSource{  class PipeCredentials  {    public const String PipeName = "PipeCLMonitoring";    public const String PipeRoot = "net.pipe://localhost/";    public const long MaxMessageSize = 500 * 1024 * 1024; //bytes  }  class TestTraceContractCallback : IContractCallback  {    public void JobComplete()    {      Console.WriteLine("Job Completed.");    }  }  [ServiceContract(SessionMode = SessionMode.Required,                    CallbackContract = typeof(IContractCallback))]  interface IContract  {    [OperationContract]    void StopServer();  }  interface IContractCallback  {    [OperationContract(IsOneWay = true)]    void JobComplete();  }  [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]  class TestTraceContract : IContract  {    public void StopServer()    {      Program.isStopMonitor = true;    }  }}

Вывод

Будьте осторожны со стандартным методом TraceSource.TraceEvent. Если у вас теоретически возможно частое использование данного метода в программе, то вы можете столкнуться с подобной проблемой. Особенно если у вас высоконагруженная система. Сами разработчики в таком случае не рекомендуют использовать всё, что связано с классом TraceSource. Если вы уже сталкивались с чем-то подобным, то не стесняйтесь рассказать об этом в комментариях.

Спасибо за просмотр. Незаметно рекламирую свой Twitter.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikolay Mironov. How WCF Shoots Itself in the Foot With TraceSource.

Подробнее..

Еще один способ установки и использования Docker в Windows 10

22.12.2020 22:14:34 | Автор: admin

В этой статье мы подготовим окружение для запуска контейнеров в Windows 10 и создадим простое контейнеризированное .NET приложение

Чтобы все описанные ниже действия были успешно выполнены, потребуется 64-разрядная система с версией не меньше 2004 и сборкой не меньше 18362. Проверим версию и номер сборки, выполнив в PowerShell команду winver

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

Установка WSL 2

Сначала включим компонент Windows Subsystem for Linux (WSL). Для этого запустим PowerShell с правами администратора и выполним первую команду

dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

Выполним следующую команду

dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

Чтобы завершить установку, перезагрузим компьютер shutdown -r -t 1

Установим пакет обновления ядра Linux

Выберем WSL 2 по умолчанию для новых дистрибутивов Linux wsl --set-default-version 2

Для целей этой статьи это необязательно, но установим дистрибутив Linux через Microsoft Store, например, Ubuntu 20.04 LTS

При первом запуске установленного дистрибутива введем имя пользователя и пароль

Чтобы увидеть запущенные дистрибутивы Linux, выполним в PowerShell команду wsl --list --verbose

Чтобы завершить работу дистрибутива Linux, выполним команду wsl --terminate Ubuntu-20.04

Файловая система запущенного дистрибутива Linux будет смонтирована по этому пути \\wsl$

Более подробно о подсистеме WSL

Более подробно об установке подсистемы WSL и устранение неполадок

Установка Docker

Скачаем Docker Desktop для Windows и установим, следуя простым инструкциям

После установки запустим приложение Docker Desktop и установим интеграцию Docker с дистрибутивом Linux (WSL 2)

Теперь отправлять команды Docker можно как через PowerShell, так и через Bash. Выполним команду docker version

Более подробно о Docker Desktop

Запуск контейнеров

Чтобы убедиться, что Docker правильно установлен и работает должным образом, запустим простой контейнер busybox, который всего лишь выведет в консоль переданное сообщение и завершит свое выполнение

docker run busybox echo "hello docker!!!"

Хорошо. Давайте сделаем что-то более интересное. Например, запустим контейнер rabbitmq

docker run --name rabbit1 -p 8080:15672 -p 5672:5672 rabbitmq:3.8.9-management

Разберем выполненную команду:

docker run - запускает контейнер из образа. Если данный образ отсутствует локально, то предварительно он будет загружен из репозитория Docker Hub

--name rabbit1 - присваивает запускаемому контейнеру имя rabbit1

-p 8080:15672 - пробрасывает порт с хоста в контейнер. 8080 - порт на хосте, 15672 - порт в контейнере

rabbitmq:3.8.9-management - имя образа и его тег/версия, разделенные двоеточием

Теперь мы можем извне контейнера взаимодействовать с сервером RabbitMQ через порт 5672 и получить доступ к управлению из браузера через порт 8080

Посмотреть статус контейнеров, в том числе остановленных, можно с помощью команд docker container ls --all или docker ps -a

Чтобы остановить наш контейнер: docker stop rabbit1. Запустить вновь: docker start rabbit1

Более подробно о командах Docker

Отладка .NET приложения запущенного в контейнере

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

docker network create mynet

Далее запустим redis и подключим его к ранее созданной сети. Благодаря параметру -d процесс в контейнере будет запущен в фоновом режиме

docker run --name redis1 --network mynet -d redis

Далее с помощью Visual Studio 2019 создадим новый проект ASP.NET Core Web API, который будет использован для демонстрации отладки

Добавим для взаимодействия с Redis пакет StackExchange.Redis через Package Manager Console

Install-Package StackExchange.Redis -Version 2.2.4

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

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

using System;namespace WebApiFromDocker{    public class RandomWeatherService    {        private Random _randomGenerator;        public RandomWeatherService()        {            _randomGenerator = new Random();        }        public int GetForecast(string city)        {            var length = city.Length;            var temperatureC = _randomGenerator.Next(-length, length);            return temperatureC;        }    }}

Добавим файл RedisRepository.cs, где будет находится служба кеширования сформированных прогнозов

using StackExchange.Redis;using System;using System.Threading.Tasks;namespace WebApiFromDocker{    public class RedisRepository    {        private string _connectionString = "redis1:6379";        private TimeSpan _expiry = TimeSpan.FromHours(1);        public async Task SetValue(string key, string value)        {            using var connection = await ConnectionMultiplexer            .ConnectAsync(_connectionString);            var db = connection.GetDatabase();            await db.StringSetAsync(key.ToUpper(), value, _expiry);        }        public async Task<string> GetValue(string key)        {            using var connection = await ConnectionMultiplexer            .ConnectAsync(_connectionString);            var db = connection.GetDatabase();            var redisValue = await db.StringGetAsync(key.ToUpper());            return redisValue;        }    }}

Зарегистрируем созданные службы в классе Startup

public void ConfigureServices(IServiceCollection services){    services.AddScoped<RandomWeatherService>();    services.AddScoped<RedisRepository>();    services.AddControllers();}

И наконец, изменим созданный автоматически единственный контроллер WeatherForecastController следующим образом

using Microsoft.AspNetCore.Mvc;using System;using System.Threading.Tasks;namespace WebApiFromDocker.Controllers{    [ApiController]    [Route("api/[controller]")]    public class WeatherForecastController : ControllerBase    {        private RandomWeatherService _weather;        private RedisRepository _cache;        public WeatherForecastController(            RandomWeatherService weather,             RedisRepository cache)        {            _weather = weather;            _cache = cache;        }        //GET /api/weatherforecast/moscow        [HttpGet("{city}")]        public async Task<WeatherForecast> GetAsync(string city)        {            int temperatureC;            var cachedTemperatureCString = await _cache.GetValue(city);            if (!string.IsNullOrEmpty(cachedTemperatureCString))            {                temperatureC = Convert.ToInt32(cachedTemperatureCString);            }            else            {                temperatureC = _weather.GetForecast(city);                await _cache.SetValue(city, temperatureC.ToString());            }            var forecast = new WeatherForecast(            city, DateTime.UtcNow, temperatureC);            return forecast;        }    }}

Помимо прочего в проект автоматически был добавлен файл Dockerfile с инструкциями для Docker. Оставим его без изменений

FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS baseWORKDIR /appEXPOSE 80FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS buildWORKDIR /srcCOPY ["WebApiFromDocker/WebApiFromDocker.csproj", "WebApiFromDocker/"]RUN dotnet restore "WebApiFromDocker/WebApiFromDocker.csproj"COPY . .WORKDIR "/src/WebApiFromDocker"RUN dotnet build "WebApiFromDocker.csproj" -c Release -o /app/buildFROM build AS publishRUN dotnet publish "WebApiFromDocker.csproj" -c Release -o /app/publishFROM base AS finalWORKDIR /appCOPY --from=publish /app/publish .ENTRYPOINT ["dotnet", "WebApiFromDocker.dll"]

В результате получим следующую структуру проекта

Если по какой-то невероятной причине Вам понадобятся исходники, то они здесь

Запустим наше приложение в контейнере под отладкой

После того как контейнер будет запущен, также подключим его к сети mynet

docker network connect mynet WebApiFromDocker

После убедимся, что все необходимые контейнеры находятся в одной сети

docker network inspect mynet

Далее установим Breakpoint в единственном методе контроллера и пошлем запрос через Postman, или через любой браузер

http://localhost:49156/api/weatherforecast/moscow 

Кстати, используемый порт в Вашем случае может отличаться и его можно посмотреть в окне Containers

Результат в окне Postman

Дополнительно убедимся, что значение зафиксировано в redis, подключившись с помощью консоли redis-cli

Хорошо, все сработало, как и задумано!

Подробнее..
Категории: Net , Docker , Csharp

Roslyn API, или из-за чего PVS-Studio очень долго проект анализировал

22.04.2021 16:18:43 | Автор: admin

Многие ли из вас использовали сторонние библиотеки при написании кода? Вопрос риторический, ведь без применения сторонних библиотек разработка некоторых продуктов затягивалась бы на очень-очень большое время, потому что для решения каждой проблемы приходилось бы "изобретать велосипед". Однако в использовании сторонних библиотек кроме плюсов имеются и минусы. Один из этих минусов недавно коснулся и анализатора PVS-Studio для C#. Анализатор долгое время не мог закончить анализ большого проекта из-за использования метода SymbolFinder.FindReferencesAsync из Roslyn API в диагностике V3083.

Жизнь в PVS-Studio, как обычно, шла своим чередом. Разрабатывались новые диагностики, улучшался статический анализатор, писались новые статьи. Как вдруг! У одного из пользователей нашего анализатора на его большом проекте в течение дня шёл анализ и никак не мог закончиться. Alarm! Alarm! Свистать всех наверх! И мы свистали, получили дампы от пользователя и начали разбираться в причинах долгого анализа. При подробном изучении проблемы выяснилось, что дольше всех работали 3 C# диагностики. Одной из них оказалась диагностика под номером V3083. К этой диагностике уже и раньше было повышенное внимание, но пора было предпринять конкретные действия. V3083 предупреждает о некорректных вызовах C# событий. Например, в коде:

public class IncorrectEventUse{  public event EventHandler EventOne;    protected void InvokeEventTwice(object o, Eventers args)  {    if (EventOne != null)    {      EventOne(o, args);              EventOne.Invoke(o, args);    }  }}

V3083 укажет на вызовы обработчиков события EventOne в методе InvokeEventTwice. О причинах опасности этого кода вы можете узнать более подробно в документации этой диагностики. Со стороны, логика работы V3083 очень простая:

  • найти вызов события;

  • проверить, корректно ли это событие вызывается;

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

Из-за этой простоты становится еще интереснее понять причину долгой работы диагностики.

Причина замедления

На самом же деле логика чуть-чуть сложнее. V3083 в каждом файле для каждого типа создает только одно срабатывание анализатора на событие, куда записывает номера всех строк (для навигации в различных плагинах: Visual Studio, Rider, SonarQube), где событие некорректно вызывается. Получается, первым делом необходимо найти все места вызова события. Для подобной задачи в Roslyn API уже имеется метод SymbolFinder.FindReferencesAsync, который и был использован в V3083, чтобы не "изобретать велосипед".

Этот метод советуют использовать во многих руководствах: первое, второе, третье и т. д. Возможно, в каких-то простых случаях скорости работы этого метода и достаточно. Однако, чем больше кодовая база проекта, тем дольше этот метод будет работать. На 100 % мы убедились в этом только после изменения V3083.

Ускорение V3083 после изменения

При изменении кода диагностики или ядра анализатора необходимо проверить, что ничего из того, что раньше работало, не сломалось. Для этого у нас имеются позитивные и негативные тесты на каждую диагностику, юнит тесты для ядра анализатора, а также база open-source проектов (которых уже почти 90 штук). Для чего нам база open-source проектов? На ней мы запускаем наш анализатор для проверки его в "боевых условиях", а также этот прогон служит дополнительной проверкой, что мы ничего не сломали в анализаторе. У нас уже имелся прогон анализатора на этой базе до изменения V3083. Все, что нам осталось сделать, это совершить аналогичный прогон после изменения V3083 и выяснить выигрыш во времени. Результаты нас приятно удивили. Без использования SymbolFinder.FindReferencesAsync в V3083 мы получили ускорение на тестах на 9 %. Если кому-то эти цифры показались незначительными, то вот вам характеристики компьютера, на котором производились замеры:

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

Заключение

Пусть всем, кто использует Roslyn API, эта заметка будет предостережением! И вы не допустите наших ошибок. Причем, это касается не только метода SymbolFinder.FindReferencesAsync, но и всех других методов класса Microsoft.CodeAnalysis.FindSymbols.SymbolFinder, которые используют один и тот же механизм.

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

Кроме диагностик мы занялись другими оптимизациями анализатора PVS-Studio, о чем расскажем в следующих статьях и заметках.

Изменение диагностики V3083 пока что не попало в релиз, поэтому версия анализатора 7.12 работает с использованием SymbolFinder.FindReferencesAsync.

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

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Valery Komarov. Roslyn API: Why PVS-Studio Was Analyzing the Project So Long.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru