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

Asp

Перевод Сравнение производительности ASP.NET Core-проектов на Linux и Windows в службе приложений Azure

08.05.2021 14:13:54 | Автор: admin
Что быстрее ASP.NET Core-приложение, развёрнутое в Docker-контейнере на Linux, или такая же программа, но запущенная на Windows-сервере, учитывая то, что всё это работает в службе приложений Azure? Какая из этих конфигураций предлагает более высокий уровень производительности, и о каком уровне производительности можно говорить?



Недавно меня заинтересовали эти вопросы, после чего я решил сам всё проверить, сделав следующее:

  • Подготовил простое ASP.NET Core-приложение, используя NET Core 2.0 и C#.
  • Развернул один экземпляр этого приложения в службе Standard S1 на Windows-хосте, расположенном в регионе Western Europe.
  • Развернул ещё один экземпляр этого приложения в службе Standard S1 с использованием Linux-хоста (регион Western Europe) и Docker-контейнера.
  • Для создания нагрузки на серверы использовал Apache Benchmark, запросы отправлялись из Варшавы (Польша) с клиентской системы, работающей под управлением Ubuntu 17.04 и подключённой к интернету по Wi-Fi.
  • Повторил испытания, воспользовавшись средством Visual Studio Web Performance Test. Запросы к исследуемым серверам выполнялись из того же города, но в данном случае роль клиента выполнял компьютер, на котором установлена Windows 10, подключённый к интернету через проводную сеть.
  • Проанализировал данные, полученные в ходе тестирования, и сравнил результаты.

В обоих случаях приложение собрано с использованием инструмента командной строки dotnet в конфигурации Release. Хостилось приложение с использованием кросс-платформенного сервера Kestrel и было размещено за прокси-сервером, используемым службами Azure. В распоряжении каждого экземпляра приложения были ресурсы, обеспечиваемые планом обслуживания Standard S1, то есть отдельная виртуальная машина. В ходе выполнения тестов я, кроме того, сравнил производительность ASP.NET Core-приложения, работающего в Docker-контейнере, с производительностью приложений, основанных на других стеках технологий, которые мне доводилось исследовать в последнее время (Go, Python 3.6.2, PyPy 3). В сервере Kestrel используется libuv поэтому мне интересно было сравнить его с uvloop, учитывая то, что последний является обёрткой для libuv и asyncio (это встроенный инструмент Python для создания конкурентного кода). Я думаю, что многие NET-разработчики гордятся скоростью Kestrel и работой, проведённой инженерами Microsoft, но при этом забывают о том, что им стоило бы испытывать чувство благодарности и к libuv С-библиотеке для организации асинхронного ввода-вывода, которая появилась до Kestrel и используется, кроме того, в Node.js и в других подобных проектах. И, кроме того, стоит помнить о том, что, когда перед ASP.NET Core-приложениями находится IIS, нужно принимать во внимание некоторые дополнительные соображения.

Подготовка веб-приложения


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

  • При получении запроса без параметров сообщение Hello World с отметкой времени.
  • При получении запроса со строковым параметром n, представляющим собой число, находящееся в диапазоне от 1 до 100 ответ с телом размером n Кб.

app.Run(async (context) =>{var request = context.Request;var s = request.Query["s"];if (string.IsNullOrEmpty(s)) {// возврат простого Hello Worldvar now = DateTime.UtcNow;await context.Response.WriteAsync($"Hello World, from ASP.NET Core and Net Core 2.0! {now.ToString("yyyy-MM-dd HH:mm:ss.FFF")}");return;}// {...}});

Развёртывание приложения на Windows-машине


Для развёртывания проекта на Windows-машине я быстро создал проект в VSTS (Visual Studio Team Services), настроил службы с использованием шаблонов ARM, собрал и развернул приложение с применением задач VSTS.


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


Работа с Windows-проектом в Microsoft Azure

В этом GitHub-репозитории вы можете найти код приложения и ARM-шаблоны.

Развёртывание приложения на Linux-машине с использованием Docker-контейнеров


Образ Docker был подготовлен и опубликован с использованием того же подхода, о котором я уже писал, рассказывая о запуске образов Docker в Azure. Так как я уже об этом рассказывал, повторяться тут я не буду. Единственное, на что я хочу обратить ваше внимание это то, что в одной группе ресурсов Azure нельзя смешивать планы служб приложений, рассчитанные на Linux и Windows. Поэтому для каждого из вариантов приложения я создал собственную группу ресурсов.


Работа с Linux-проектом в Microsoft Azure

Код приложения и файлы для создания образов Docker можно найти в этом репозитории.

Тестирование приложений с использованием Apache Benchmark


Мне нравится утилита Apache Benchmark (ab). Ей удобно пользоваться и она выдаёт результаты, применимые при практической оценке производительности систем, такие, как количество запросов в секунду (Requests Per Second, RPS), время ответа на разных перцентилях (например сведения о том, что 95% запросов обрабатывается в пределах n мс, а 5% в пределах m мс, и так далее). Эта утилита идеально подходит для исследования производительности отдельных методов. Я выполнил несколько групп тестов, каждую в разное время дня. План испытаний выглядел так:
Сценарий Конфигурация
Hello World 5000 запросов, 150 одновременных пользователей
Ответ с телом в 1 Кб 5000 запросов, 150 одновременных пользователей
Ответ с телом в 10 Кб 2000 запросов, 150 одновременных пользователей
Ответ с телом в 100 Кб 2000 запросов, 150 одновременных пользователей

# пример командыab -n 5000 -c 150 -l http://linuxaspcorehelloworld-dev-westeurope-webapp.azurewebsites.net/

Выходные данные ab выглядят примерно так:

This is ApacheBench, Version 2.3 <$Revision: 1757674 $>Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/Licensed to The Apache Software Foundation, http://www.apache.org/Benchmarking linuxaspcorehelloworld-dev-westeurope-webapp.azurewebsites.net (be patient)Server Software:    KestrelServer Hostname:    linuxaspcorehelloworld-dev-westeurope-webapp.azurewebsites.netServer Port:      80Document Path:     /Document Length:    72 bytesConcurrency Level:   150Time taken for tests:  22.369 secondsComplete requests:   5000Failed requests:    0Keep-Alive requests:  0Total transferred:   1699416 bytesHTML transferred:    359416 bytesRequests per second:  223.53 [#/sec] (mean)Time per request:    671.065 [ms] (mean)Time per request:    4.474 [ms] (mean, across all concurrent requests)Transfer rate:     74.19 [Kbytes/sec] receivedConnection Times (ms)min mean[+/-sd] median  maxConnect:    40 489 289.9  425  3813Processing:  42 170 227.6  109  3303Waiting:    41 153 152.9  105  2035Total:    106 659 359.2  553  4175Percentage of the requests served within a certain time (ms)50%  55366%  65075%  75080%  78690%  94895%  103698%  128299%  1918100%  4175 (longest request)

И, наконец, я собрал результаты воедино с использованием Python-скрипта, после чего посчитал средние показатели по всем тестам и построил графики двух видов:

  • Количество запросов в секунду (Requests per seconds, RPS), обработанных сервером.
  • 95-й перцентиль времени ответа, другими словами показатель, отвечающий на вопрос о том, в пределах какого количества миллисекунд будет дан ответ на 95% запросов.

Анализ результатов, полученных с помощью Apache Benchmark


В ходе выполнения тестовых сценариев Hello World и Ответ с телом в 1 Кб на серверы было отправлено около 250 тысяч запросов. Их количество для сценариев, в которых применялись ответы с телом размером 10 Кб и 100 Кб, находилось в районе 110 70 тысяч запросов. Эти показатели не выражены в точных цифрах, так как иногда инфраструктура Azure закрывает соединения (connection reset by peer), но количество проанализированных запросов, всё равно, является достаточно большим. Не обращайте особого внимания на абсолютные значения, приводимые ниже: они имеют смысл лишь в плане сравнения друг с другом, так как они зависят и от клиента, и от сервера. Результаты, показанные в следующей таблице, получены с использованием клиента, подключённого к интернету по Wi-Fi-сети.
Хост Сценарий RPS (среднее значение) 95% запросов выполнено в пределах мс
Linux Hello World 232,61 1103,64
Linux Ответ с телом в 1 Кб 228,79 1129,93
Linux Ответ с телом в 10 Кб 117,92 1871,29
Linux Ответ с телом в 100 Кб 17,84 14321,78
Windows Hello World 174,62 1356,8
Windows Ответ с телом в 1 Кб 171,59 1367,23
Windows Ответ с телом в 10 Кб 108,08 2506,95
Windows Ответ с телом в 100 Кб 17,37 16440,27

Эти результаты позволяют сделать вывод о том, что, если тело ответа невелико, приложение, работающее в Docker-контейнере на Linux, гораздо быстрее обрабатывает HTTP-запросы, чем его версия, работающая в среде Windows. Разница между разными вариантами запуска приложения уменьшается по мере роста тела ответа, хотя, всё равно, Linux-вариант оказывается быстрее Windows-варианта. Такой результат может показаться удивительным, так как Windows-сервер, работающий в службе приложений Azure, представляет собой более зрелое решение, нежели Linux-сервер, работающий там же. С другой стороны, виртуализация Docker, в сравнении с другими технологиями виртуализации приложений, не отличается особой требовательностью к системным ресурсам. Возможно, имеются и другие отличия в конфигурациях, позволяющие Linux-хосту работать эффективнее, что особенно заметно при работе с ответами, тела которых невелики.
Сценарий Linux, прирост RPS в сравнении с Windows
Hello World +33,21%
Ответ с телом в 1 Кб +33,33%
Ответ с телом в 10 Кб +9,1%
Ответ с телом в 100 Кб +2,7%

Вот диаграммы, иллюстрирующие вышеприведённые данные.


Запросов в секунду, средний показатель (больше лучше)


Время, в течение которого обрабатываются 95% запросов (меньше лучше)


Время, в течение которого обрабатываются 95% запросов (меньше лучше)

Тестирование приложений с использованием инструментов Visual Studio Web Performance


Вышеописанные испытания производительности были повторены с использованием инструментов Visual Studio Ultimate Web Performance. Клиентская система, в географическом смысле, располагалась там же, где и раньше, но в этот раз она была подключена к интернету по проводной сети. Вот исходный код тестов. В данном случае использовались следующие настройки:

  • Тесты выполнялись по 5 минут.
  • В начале количество пользователей равнялось 50.
  • Каждые 10 секунд количество пользователей увеличивалось на 10.
  • Максимальным количеством пользователей было 150.


Тестирование серверов с помощью Visual Studio Web Performance (оригинал)

Анализ результатов, полученных с помощью Visual Studio Web Performance


Результаты тестов, проведённых с помощью Visual Studio Web Performance, отличаются от тех, что были проведены с помощью Apache Benchmark, но при этом Linux-система показала себя даже лучше, чем прежде. Различия в результатах испытаний можно объяснить следующими фактами:

  • Инструменты VS Web Performance, вероятно, по-другому работают с соединениями.
  • Для выполнения тестов использовались разные клиенты, по-разному подключённые к интернету.

Но, всё равно, допустимо напрямую сравнивать результаты испытаний только тогда, когда они выполнены с использованием одного и того же клиента, с применением одних и тех же инструментов и настроек.
Хост Сценарий RPS (среднее значение) 95% запросов выполнено в пределах мс
Linux Hello World 649 340
Linux Ответ с телом в 1 Кб 622 380
Linux Ответ с телом в 10 Кб 568 370
Linux Ответ с телом в 100 Кб 145 1220
Windows Hello World 391 400
Windows Ответ с телом в 1 Кб 387 420
Windows Ответ с телом в 10 Кб 333 510
Windows Ответ с телом в 100 Кб 108 1560

Вот таблица со сравнением Linux- и Windows-вариантов приложения.
Сценарий Linux, прирост RPS в сравнении с Windows
Hello World +65,98%
Ответ с телом в 1 Кб +60,72%
Ответ с телом в 10 Кб +70,57%
Ответ с телом в 100 Кб +34,26%

Вот визуализация этих данных.


Запросов в секунду, средний показатель (больше лучше)


Время, в течение которого обрабатываются 95% запросов (меньше лучше)

Сравнение с другими стеками технологий в Docker


Я могу провести сравнение Linux+Docker-варианта исследуемого приложения с приложениями, при создании которых использовались другие технологии, испытанные мной за последние дни, лишь используя сценарий Hello, World. Но при этом не могу не отметить, что даже при таком походе производительность Windows-варианта выглядит слишком низкой. Думаю, что стандартная конфигурация Kestrel (например количество потоков) не оптимизирована для планов Standard S1. Например вот средние значения RPS, полученные для других стеков технологий, испытания которых проводились с использованием тех же настроек.
Стек технологий Сценарий Hello World, RPS
Go 1.9.1 net/http ~1000, с пиками в ~1100
Python 3.6.1 uvloop, httptools ~1000, с пиками в ~1200
Python 3.6.1 Sanic, uvloop ~600, с пиками в ~650
PyPy 3, Gunicorn, Gevent, Flask ~600, с пиками в ~650

Эти результаты не снижают ценности ранее выполненных испытаний, в ходе которых одно и то же ASP.NET Core-приложение исследовалось в средах Windows и Linux.

Итоги


Развёртывание проектов в службе приложений Azure с использованием Linux и Docker не ухудшает производительности приложений, что идёт вразрез с тем, чего многие могли бы ожидать, учитывая то, что Windows-хостинг этой платформы представляет собой более зрелое решение. Применение Linux в Azure, на самом деле, выгоднее применения Windows, особенно в тех случаях, когда речь идёт об обработке запросов, предусматривающих отправку ответов, тела которых имеют небольшие размеры.

Чем вы пользуетесь на серверах Linux или Windows?

Подробнее..

Перевод Сравнение производительности ASP.NET Core-проектов на Linux и Windows в службе приложений Azure. Продолжение

09.05.2021 14:14:20 | Автор: admin
В моём предыдущем материале речь шла о сравнении производительности ASP.NET Core-приложений, запускаемых в Windows и в среде Linux + Docker, работающих в службе приложений Azure. Эта тема интересна многим поэтому я решил написать продолжение.



Я снова провёл испытания, используя подход, отличающийся от прежнего лучшей воспроизводимостью, такой, который даёт более надёжные результаты. Теперь я генерирую веб-нагрузку на серверы с помощью облачных инструментов Azure Cloud Agents, применяя Visual Studio и VSTS. И, более того, в то время как ранее я выполнял тесты с использованием HTTP, теперь тестирование проводилось с применением HTTPS.

Выполнение тестов в облачной среде


Благодаря отличной работе, проведённой Microsoft, запуск тестов в облаке это очень просто. Делается это с помощью инструментов Visual Studio Web Performance, с использованием учётной записи VSTS. Я провёл по две серии нагрузочных тестов для каждого из следующих сценариев:

  • Ответ, в теле которого содержится текст Hello World и отметка времени.
  • Ответ с телом в 1 Кб.
  • Ответ с телом в 10 Кб.
  • Ответ с телом в 50 Кб.
  • Ответ с телом в 100 Кб.

Вот как были настроены тесты:

  • Тесты выполнялись по 5 минут.
  • В начале количество пользователей равнялось 50.
  • Каждые 10 секунд количество пользователей увеличивалось на 10.
  • Максимальным количеством пользователей было 150.
  • Запросы выполнялись из того же региона (Western Europe), где были развёрнуты исследуемые приложения.


Результаты тестов (оригинал)

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


Пример выходных данных теста (оригинал)

Я использовал те же тесты, которые применялись в прошлый раз (найти соответствующий код можно здесь).

Что же у меня получилось теперь?

Анализ результатов


Полученные в этот раз результаты согласуются с теми, которые были получены в прошлый раз, при использовании клиентской системы, подключённой к интернету по проводной сети. А именно, ASP.NET Core-приложение, развёрнутое в Linux с применением Docker-контейнера, оказывается гораздо быстрее, чем оно же, развёрнутое на Windows-хосте (оба варианта работают в рамках соответствующего плана служб приложений). Результаты новых тестов даже сильнее, чем результаты прежних, указывают на превосходство Linux-варианта, особенно при работе с запросами, предусматривающими возврат ответов с более объёмными телами.

Вот сводные результаты испытаний, отражающие количество запросов, обработанных в секунду (RPS).
Сценарий Linux Windows Linux +%
Hello World 646,6 432,85 +49,38%
Ответ с телом в 1 Кб 623,05 431,95 +44,24%
Ответ с телом в 10 Кб 573,6 361,9 +58,5%
Ответ с телом в 50 Кб 415,5 210,05 +97,81%
Ответ с телом в 100 Кб 294,35 143,25 +105,48%

Вот среднее время ответа (мс).
Сценарий Linux Windows Linux +%
Hello World 168,85 242,2 -30,28%
Ответ с телом в 1 Кб 171,25 249,8 -31,45%
Ответ с телом в 10 Кб 184,2 292,7 -37,07%
Ответ с телом в 50 Кб 233,3 542,85 -57,02%
Ответ с телом в 100 Кб 365,05 817,35 -55,34%


Запросов в секунду, средний показатель (больше лучше)


Время, в течение которого обрабатываются 95% запросов (меньше лучше)

Вот .xlsx-файл с результатами тестирования, а вот аналогичный .ods-файл.

В чём Linux показывает себя хуже Windows (и так ли это на самом деле)?


Почти все нагрузочные тесты на Linux-хосте приводили к превышению допустимой нагрузки на процессор (Processor\% Processor Time) с выдачей соответствующих предупреждений. При этом ни один из тестов, проводимых на Windows-хосте, не привёл к появлению подобных предупреждений. Я не вполне уверен в том, что правильно понял документацию по этому показателю производительности, по умолчанию включаемому во все новые нагрузочные тесты, создаваемые в Visual Studio. Если кто-то в этом разбирается буду благодарен за пояснения.

Странные графики, касающиеся производительности и пропускной способности Windows-системы


Я обратил внимание на странную закономерность в графиках VSTS, отражающих производительность и пропускную способность систем в ходе нагрузочного тестирования. В случае с Linux-системами эти графики представляют собой довольно-таки плавные линии. А вот Windows-графики напоминают нечто вроде пил. Вот соответствующие графики для сценария, в котором в теле ответа содержится 10 Кб данных.


Графики производительности и пропускной способности для Linux


Графики производительности и пропускной способности для Windows

Другие графики можно найти здесь. Вот графики (Linux и Windows) для сценария, где в теле ответа содержится 50 Кб данных.

Итоги


В свете моих предыдущих испытаний учитывая полученные здесь результаты, могу сказать, что, с точки зрения производительности, в Azure оправдано использование конфигурации Linux + Docker.

Заключительные замечания


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

Я решил провести эти тесты производительности и опубликовать результаты лишь из-за того, что планирую создать веб-сервис для приложения, написанного мной на Python. Мне было интересно узнать о том, удастся ли мне получить удовлетворительные результаты в среде Azure с использованием Linux-хоста, на котором работает Docker. Для разработки моего сервиса я планирую использовать PyPy 3, Gunicorn, Gevent и Flask. И я полагаю, что проект, основанный на этом стеке технологий, будет работать быстрее аналогичного ASP.NET Core-проекта, использующего сервер Kestrel. Но это уже совсем другая история, и чтобы говорить об этом с уверенностью надо провести соответствующие тесты.

Какими стеками технологий вы пользуетесь для разработки веб-сервисов?

Подробнее..

Перевод Тест пропускной способности ASP.NET Core 5.0 в Kestrel, IIS, Nginx и Caddy

05.06.2021 18:10:05 | Автор: admin
Начиная с версии 2.2. ASP.NET Core поддерживает режим внутрипроцессного размещения приложения (InProcess) в IIS, направленный на улучшение производительности кода. Рик Страл написал статью, в которой подробно исследовал эту тему. С тех пор прошло три года, теперь платформа ASP.NET Core добралась до версии 5.0. Как это повлияло на производительность ASP.NET Core-проектов на различных серверах?



Результаты исследования Рика Страла


Рик Страл в своей статье занимался тестирование ASP.NET Core-кода на Windows в Kestrel и в IIS (в режимах InProcess и OutOfProcess). Его интересовало количество запросов в секунду, обрабатываемых системой. В результате он пришёл к выводу о том, что первое место по производительности получает использование IIS в режиме InProcess, второе Kestrel, третье IIS в режиме OutOfProcess.

Обзор эксперимента


Рик не провёл испытания, позволяющие выявить различия в выполнении ASP.NET Core-кода на Windows- и на Linux-серверах. А вопрос о том, что в 2021 году лучше выбрать для проектов, основанных на ASP.NET Core 5.0, интересует многих из тех, кого я знаю. Поэтому я решил, используя подход к тестированию, похожий на тот, которым пользовался Рик, узнать о том, сколько запросов в секунду может обработать ASP.NET Core 5.0-приложение на Windows и на Linux.

Тестовое окружение


Так как Windows 10, Ubuntu Desktop и другие настольные операционные системы не в полной мере отражают особенности сопоставимых с ними серверных дистрибутивов, я решил выбрать для тестов серверные версии соответствующих ОС. Это были свежеустановленные копии систем с последними патчами, перед выполнением тестов они были один раз перезагружены.

Windows-сервер:


  • Провайдер: Microsoft Azure East Asia Region.
  • ОС: Windows Server 2019 Data Center.
  • Характеристики системы: B2S / 2 vCPU, 4GB RAM, Premium SSD.
  • Окружение: IIS с поддержкой статического и динамического сжатия контента, отсутствие интеграции с ASP.NET 3.5 или 4.x, на сервере установлена платформа ASP.NET Core 5.0.2 Runtime.


Сведения о Windows-сервере

Linux-сервер


  • Провайдер: Microsoft Azure East Asia Region.
  • ОС: Ubuntu Server 20.04 LTS.
  • Характеристики системы: B2S / 2 vCPU, 4GB RAM, Premium SSD.
  • Окружение: включено использование BBR, установлены Nginx, Caddy и ASP.NET Core 5.0.2 Runtime.


Сведения о Linux-сервере

Инструменты для проведения тестов


Рик пользовался West Wind Web Surge, но этот инструмент доступен только на платформе Windows, а это нас не устроит. Я решил воспользоваться опенсорсным кросс-платформенным инструментом bombardier, о котором однажды писали в официальном .NET-блоге Microsoft.

Тестовое приложение


Я создал новый проект ASP.NET Core 5.0 Web API, в котором имеется лишь один метод:

[ApiController][Route("[controller]")]public class TestController : ControllerBase{[HttpGet]public string Get(){return $"Test {DateTime.UtcNow}";}}

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

Проект скомпилирован с применением конфигурации Release и опубликован с использованием FDD. Настройки логирования оставлены в стандартном состоянии:

"LogLevel": {"Default": "Information","Microsoft": "Warning","Microsoft.Hosting.Lifetime": "Information"}

Методика тестирования


Я запускал проект, используя следующие конфигурации:

  • Kestrel.
  • IIS в режиме InProcess.
  • IIS в режиме OutOfProcess.
  • Обратный прокси Nginx.
  • Обратный прокси Caddy.

Затем я применял bombardier. В течение 10 секунд, по 2 соединениям, велась работа с конечной точкой, доступной на localhost. После прогревочного раунда испытаний я, друг за другом, проводил ещё 3 раунда и на основе полученных данных вычислял показатель количества запросов, обработанных исследуемой системой за секунду (Request per Second, RPS).

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

Результаты тестов


Windows + Kestrel


Средний RPS: 18808


Windows + Kestrel

Windows + IIS в режиме InProcess


Средний RPS: 10089


Windows + IIS в режиме InProcess

Windows + IIS в режиме OutOfProcess


Средний RPS: 2820


Windows + IIS в режиме OutOfProcess

Linux + Kestrel


Средний RPS: 10667


Linux + Kestrel

Linux + Nginx


Средний RPS: 3509


Linux + Nginx

Linux + Caddy


Средний RPS: 3485


Linux + Caddy

Итоги


Вот как выглядят результаты тестирования (от самой быстрой комбинации ОС и серверного ПО до самой медленной):

  • Windows + Kestrel (18808)
  • Linux + Kestrel (10667)
  • Windows + IIS в режиме InProcess (10089)
  • Linux + Nginx (3509)
  • Linux + Caddy (3485)
  • Windows + IIS в режиме OutOfProcess (2820)

Мои результаты отличаются от тех, что получил Рик, тестируя ASP.NET Core 2.2-проект. В его тестах производительность IIS в режиме InProcess оказывалась выше, чем производительность Kestrel. Но сейчас Kestrel оказывается быстрее IIS в режиме InProcess, и это кажется вполне логичным и ожидаемым.

А вот неожиданностью для меня стало то, что производительность Windows-серверов c Kestrel оказалась выше производительности аналогичных Linux-серверов. Это меня удивило.

В режиме обратного прокси-сервера Nginx и Caddy, в общем-то, показывают одинаковую производительность. И та и другая конфигурации обходят IIS в режиме OutOfProcess.

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

Дополнение


Мне сообщили, что в .NET 5 у DateTime.UtcNow могут быть проблемы, касающиеся производительности. Я провёл повторные испытания, заменив эту конструкцию на Activity.Current?.Id ?? HttpContext.TraceIdentifier. Получившиеся у меня результаты были выражены немного более высокими показателями, но общая картина осталась почти такой же.

А если увеличить число подключений с 2 до 125 сервер Kestrel, и на Windows, и на Linux, способен дать гораздо более высокую пропускную способность.

Каким серверным ПО вы пользуетесь в своих проектах?


Подробнее..

Я десять лет страдал от ужасных архитектур в C приложениях и вот нашел, как их исправить

01.09.2020 18:20:28 | Автор: admin


Я второй десяток лет участвую в разработке приложений для бизнеса на .NET и каждый раз вижу одни и те же проблемы быдлокод и беспорядок. Месиво из сервисов, UoW, DTO-шек, классов-хелперов. В иных местах и прямой доступ в базу данных руками, логика в статических классах, километровые портянки конфигурации IoC.


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


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


Откуда берётся бардак


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


Если отбросить человеческие аспекты и оставить технически-архитектурные, то откуда берется весь бардак?


Это IoC!


Нет, вы только поймите правильно: инверсия зависимостей и лайфтайм-менеджмент из одной точки приложения это прекрасно. Но что лежит в наших контейнерах? Connection к базе данных (один на подключение), пачка каких-нибудь параметров и credentials, надёрганных из web.config (обычно синглтон) и огромные, бескрайние километры разнообразных сервисов-UoW-репозиториев. В худшем случае в формате интерфейс-реализации.


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


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


Это потому что нет unit-тестов!


Я вам расскажу почему их нет.


Вот давайте по чесноку: все же пробовали писать unit-тесты для традиционных C#-проектов, сделанных по методологии "UoW и Repository"? На всё вышеупомянутое содержимое контейнера надо написать заглушки и вбить тестовые данные (руками). Ну или пойти попросить себе виртуалку под тесты, на которую надо вкатить БД и залить данные, заботливо украденные с продакшена. И подчищать их после каждого прогона тестов.


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


Такими темпами вы постепенно забиваете на написание тестов, ограничиваясь проверками каких-нибудь тривиальных хреновин вроде того, что метод копирования полей в класс из 10 строчек действительно, блин, копирует поля! Такие тесты остаются в системе навечно и всегда выполняются "для галочки", падая, может, раз в год, когда неопытный джун сдуру сотрёт строчку при мердже. Всё остальное время они представляют собой театр безопасности кода. Все прекрасно понимают что реальное тестирование происходит "вручную", QA-отделом (в лучшем случае автоматизировано, end-to-end), а пользователи вроде не жалуются.


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


Это потому что мы лезем в базу руками!


Да неужели? А я-то думал что вы, как настоящие мужики, терпите, пока O/RM удаляет 3000 объектов. Все, кто хочет чтобы их приложение более-менее сносно работало по скорости рано или поздно лезут в базу руками. Ну потому что она база. Потому что объектная модель натягивается на реляционную как сова на глобус долго, медленно и со слезами (см. object-relational impedance mismatch). А тут ещё и O/RM поощряет такие кренделя, оставляя доступ напрямую к базе (ибо как если этого не делать его пользователи с потрохами сожрут). Как же тут не соблазниться возможностью решить задачу быстро и просто, когда нужно медленно и сложно. Безусловно, любители посылать базе SQL из приложения сами виноваты. Однако делают это не от хорошей жизни.


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


Это потому что мы не следуем паттернам!


Да кто ж вам мешает пожалуйста, следуйте. Но давайте, попробуем реализовать простую фичу. Что там надо? Написать репозиторий пользователей. Интерфейс + реализация, сделать то же самое для заказов, на основе них создать Unit of Work. Разумеется, тоже побив на интерфейс и реализацию. Потом сделать две DTOшки (а возможно больше), потом сервис, в котором использовать описанный Unit of Work, тоже разбив на две части.


Пол-дня прошло, у вас уже 10 файлов, описывающие 10 новых типов, а к разработке фичи вы ещё даже и не приступали. Потом созвон с заказчиком, обсуждение ТЗ, собеседование джуна и багфиксы. Фича же занимает своё место в долгом ящике.


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


И их продукты иногда стреляют, после чего переписывать прототип уже нет времени надо фичи деливерить! И если повезёт, то продукт разрастается, получает прибыль и инвестиции. А после нескольких таких фазовых переходов превращается в ту самую огромную IT-компанию, где золотые слитки и джуниоры.


Это потому что у нас нет архитектора!


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


Да и не готовят особо нигде этих ваших архитекторов. Сами учатся ковырять UML, бедняги. Только вот нарисовать сколь угодно красивый дизайн, декомпозировать всё на классы и подсистемы, задействовать десяток бриджей, проксей и стратегий можно. И ревьюить весь код, самолично отрывая руки тем, кто посмеет нарушить целостность системы можно. И жарко спорить на созвонах о том, что выделить в интерфейс и куда положить метод тоже можно. Но это всё до первого дедлайна.


Выясняется, что красивая и грамотная архитектура (в 99% случаев) требует кучи телодвижений для решения простейших задач и внесения простейших правок. В итоге на красивый дизайн кладётся моржовый хрен и пишется так, чтобы быстрее решить задачу, учесть правки, пофиксить баг. Вершина пост-иронии если на самого архитектора падает 50 тикетов. Ах это незабываемое зрелище, ах эти незабываемые звуки! Принципиальный блюститель паттернов и правил сам начинает писать портянки попахивающего кода в обход всякой архитектуры. Пыхтит, корчится, шаблон трещит на весь офис лишь бы начальство по жопе не дало за сорванные сроки. Словами не передать: блажен, кто видел мир в его минуты роковые.


Жизнь сурова между "правильно, но долго" и "быстро, но неправильно" в критических условиях всегда выбирается второй вариант. Архитекторов, которые могут сделать так, чтобы правильно было проще, чем неправильно, я не встречал. Коль скоро оно так происходящее печально, но не удивительно.


Это потому что фрейморк XXX фигня!


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


Обычно тезис из подзаголовка идёт в комплекте с "давайте всё перепишем на YYY"! Именно такие сентенции выдают розовощёкие детишки, протерев лицо от печенек с конференционного кофе-брейка. Зуб даю, именно там они про волшебный YYY и услышали.


И если бы эти ребята поглощали печенья чуть меньше, а своей головой думали чуть больше они бы заметили, что YYY, который сделала для себя компания GGG ориентирован на решение проблем GGG. В выпуске подкаста "Мы обречены" со мной, Фил хорошо сформулировал на примере redux: "redux это фреймворк для управления состоянием Но не твоим состоянием!". И пусть вдохновляющие примеры а-ля "делаем на YYY приложение для подсчёта коров за 5 минут" не вводят вас в заблуждение. Между подсчётом коров и вашей условной системой автоматизации похоронного бизнеса всё же есть некоторая разница.


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


Очень часто технология YYY предсказуемо рвётся от несовместимости себя любимой с объективными реалиями вашей системы. А ещё бывает так, что YYY ещё незрелый, страдает от детских болезней и будет готов к production-использованию спустя пару лет. Если его использовать прямо сейчас, то проект или благополучно подохнет, или всё вернётся на круги своя, а розовощёкие мальчики поедут кушать ещё больше печенья и искать очередную серебряную пулю.


Как прибрать весь этот мусор


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


Жизнеспособность в долгосрочной перспективе


Я уже несколько раз в одно рыло апдейтил 2000 файлов в проекте (один раз даже с помощью VB.NET-C# транслятора), больше не хочу. Хочу видеть архитектурно такую систему, которая без разрывов в паху будет резаться на части и горизонтально расширяться логически.


Минимум кода, максимум логики


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


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


Чёткая организация


Задолбало думать куда положить очередной сервис, в какую сборку поместить интерфейс, в какой неймспейс кинуть DTO-шку. Так и ладно я мне эти размышления просто замедляют работу. А придёт на проект сотня джуниоров они ведь начнут кидать всё, куда ни попадя потому что Вася у нас художник, Вася так видит. А умудрённых сединами дедов на них на всех не хватит, чтобы в каждом отдельном случае давать ценные указания. Хочу чтобы архитектурный каркас предусматривал чёткие, понятные даже обезьяне правила и форсировал их соблюдение. Можно выпендриться и сказать "формируя у разработчика императив поведения", но я скромный.


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


Строгая типизация


Больше строгой типизации! Нам дали в руки компилятор так давайте выжмем из него по-максимуму! Пусть трудится на благо бизнеса. Собрать ваши интерфейсы, написанные по образу и подобию Java-вских, компилятор всегда сможет. Другое дело что пишем-то мы на C# и языковых возможностей у нас куда как больше дженерики вон хорошие, лямбды, разные модификаторы доступа, экстеншны, ковариантность!


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


Удобная абстракция от внешних систем


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


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


Я к тому, что к багам в логике у меня один подход, к багам с внешними системами другой. Хочется чтобы дизайн системы как-то ну я не знаю Эксплицитно обыгрывал этот момент.


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


Решение проблем с тестированием


Тесты штука нужная и важная. Не компилятором единым. Мой опыт подсказал, что регрессионного unit-тестирования на кейсах более чем достаточно. Если делать и поддерживать такие тесты будет дёшево, то их можно будет сделать много. А тут уже можно взять количеством. Как говорится, главное завалить а там ногами запинаем.


Категорически необходимо как-то решить проблему с хранением тестовых данных. Тестовая база для разработки у меня есть, конечно же. Но поднимать её в рамках CI-билда категорически не хочется. Да и потом я не хочу тестировать сервер баз данных! Его уже тестируют умные дяденьки за большие деньги. Надо чтобы тестировался мой код, который я написал. А всё остальное не надо.


Вбивать руками тестовые данные, которые уже, блин, лежат в базе тоже не хочу. Разве умная машина не может сделать это сама?


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


Тут ещё вот что надо заметить: как мы разрабатываем? Заводим тестовые данные, что-то пишем, запускаем-дебажим, уточняем требования, снова что-то пишем, снова дебажим Потом в какой-то момент коммитим задачу, пропускаем через QA, мерджим бренч, после чего задача считается решённой. Это логично, все так делают. Так вот, хочется впаять автоматизированное тестирование в этот процесс так, чтобы после приёмки, когда все от QA до бизнеса сказали "да, это работает правильно", настрогать тестов, не меняя функциональность и кинуть в общую кучу, дабы прогонялись каждый билд. Можно даже какой-нибудь code coverage замутить. Меняешь логику видишь что упало. Ну круто же!


А вот TDD на фиг если честно. Невозможно загодя написать тесты для фичи, функциональность которой толком не известна до момента её сдачи. Поэтому ориентироваться на TDD я не стану.


И таким образом...


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


За сим откланяюсь, до следующей статьи.


Кому не терпится посмотреть репозиторий тут

Подробнее..

Я 20 лет наслаждаюсь разнообразием архитектур и хочу поделиться мыслями

09.09.2020 10:06:55 | Автор: admin


Сначала хотел написать комментарий к статье "Я десять лет страдал от ужасных архитектур в C#...", но понял две вещи:

1. Слишком много мыслей, которыми хочется поделиться.
2. Для такого объёма формат комментария неудобен ни для написания, ни для прочтения.
3. Давно читаю Хабр, иногда комментирую, но ни разу не писал статей.
4. Я не силён в нумерованных списках.

Disclaimer: я не критикую @pnovikov или его задумку в целом. Текст качественный (чувствуется опытный редактор), часть мыслей разделяю. Архитектур много, но это нормально (да, звучит как название корейского фильма).

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

И, конечно, всё сказанное здесь является личным мнением на основе моего опыта и когнитивных искажений.

О моём мнении


Я часто принимаю архитектурные решения. Когда-то большие, когда-то маленькие. Изредка придумываю архитектуру с нуля. Ну, как с нуля наверняка всё придумано до нас, но про что-то мы не знаем, поэтому приходится придумывать. И не из любви к велосипедостроению (скажем так, не только из любви к нему), а потому что для некоторых задач не нашлось готового решения, которое устроило бы по всем параметрам.

Почему я считаю, что совершенно разные архитектуры имеют право на жизнь? Можно порассуждать о том, что программирование искусство, а не ремесло, но я не буду. Моё мнение: когда-то искусство, когда-то ремесло. Речь не об этом. Главное, что задачи разные. И люди. Уточню под задачами подразумеваются требования бизнеса.

Если когда-то мои задачи станут однотипными, я напишу или попрошу кого-то написать нейросеть (а может, хватит и скрипта), которая меня заменит. А сам займусь чем-то менее безрадостным. Пока же мой и, надеюсь, ваш личный апокалипсис не наступил, давайте подумаем, как влияют задачи и прочие условия на разнообразие архитектур. TL&DR; разнообразно.

Производительность или масштабируемость


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

Сроки


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

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

В моей практике сроки редко приводят к революциям в архитектуре, но бывает. И это прекрасно.

Скорость и качество разработки


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

В принципе, в одних случаях всё сводится к фактору сроков. А в других к поддерживаемости, о ней далее.

Поддерживаемость


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

Вот вы сделали заказной проект. Успешно, в сроки и бюджет уложились, заказчик всем доволен. Было и у меня такое. Теперь вы смотрите на то, что использовали и думаете так вот она золотая жила! Мы сейчас используем все эти наработки, быстро сделаем один B2B-продукт, и Сначала всё хорошо. Продукт сделали, пару раз продали. Наняли ещё продавцов и разработчиков (нужно больше золота). Заказчики довольны, платят за сопровождение, случаются новые продажи

А потом один из заказчиков говорит человеческим голосом мне бы вот эту штуковину совсем по-другому сделать сколько это может стоить?. Ну, подумаешь несколько ifчиков с другим кодом воткнуть (допустим, некогда было DI прикрутить), что плохого может случиться?

И в первый раз действительно ничего плохого не случится. Я бы даже не советовал в такой ситуации что-то специальное городить. Преждевременное усложнение архитектуры сродни преждевременной оптимизации. Но когда это случается во второй и третий раз это повод вспомнить про такие штуки как DI, паттерн стратегия, Feature Toggle и иже с ними. И, на время, это поможет.

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

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

Решения? Например, разделить продукт на модули с отдельным жизненным циклом (не забывая тестировать их взаимодействие). Это снизит сложность сопровождения, хотя и усложнит разработку. И я сейчас не о благодатной для холиваров теме монолит vs. микросервисы в монолите тоже можно устроить подобное (хотя и сложнее, на мой взгляд).

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

И к чему всё это?


Я не хочу вас (и себя) утомлять перечислением других причин для изменений в архитектуре. Давайте сейчас согласимся, что архитектуры имеют свойство со временем меняться, в зависимости от многих факторов. А значит: идеальная архитектура, решающая ну вот все проблемы не существует.

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

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

Обсуждение статьи про исправление архитектур



А что IoC?


Про IoC соглашусь, что портянкам место в армии, а модули это вселенское добро. Но вот всё остальное

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

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

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

Правда, уточню наши условия работы:

  • Модули у нас не те, которые предоставляются почти любым IoC-фреймворком а вот прям модули которые общаются между собой удалённо через API (иногда можно, из соображений производительности, поселить их в одном процессе, но схема работы не поменяется).
  • IoC используется максимально простой и максимально просто зависимости втыкаются в параметры конструктора.
  • Да, у нас сейчас микросервисная архитектура, но и здесь стараемся не мельчить.

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

А что не так с ORM и зачем прямой доступ к БД?


Да я и сам скажу, что не так многие из них слишком далеки от SQL. Но не все. Поэтому, вместо того, чтобы терпеть, пока O/RM удаляет 3000 объектов или придумывать ещё один, найдите тот, который вас устроит.

Совет: попробуйте LINQ to DB. Он хорошо сбалансирован, есть методы Update/Delete для нескольких строк. Только осторожно вызывает привыкание. Да, нет каких-то фич EF и немного другая концепция, но мне понравился намного больше EF.

Кстати, приятно, что это разработка наших соотечественников. Игорю Ткачеву респект (не нашёл его на Хабре).

А что не так с тестами на БД?


Да они будут медленнее, чем на данных в памяти. Фатально ли это? Да нет, конечно же. Как решать эту проблему? Вот два рецепта, которые лучше применять одновременно.

Рецепт 1. Берёшь крутого разработчика, который любит делать всякие прикольные штуки и обсуждаешь с ним, как красиво решить эту проблему. Мне повезло, потому что force решил проблему быстрее, чем она появилась (даже не помню, обсуждали её или нет). Как? Сделал (за день, вроде) тестовую фабрику для ORM, которая подменяет основное подмножество операций на обращения к массивам.

Для простых юнит-тестов идеально. Альтернативный вариант юзать SQLite или что-то подобное вместо больших БД.
Комментарий от force: Тут надо сделать пару уточнений. Во-первых, мы стараемся не использовать сырые запросы к базе данных в коде, а максимально используем ORM, если он хороший, то лезть в базу с SQL наголо не требуется. Во-вторых, разница поведения с базой есть, но ведь мы не проверяем вставку в базу, мы проверяем логику, и небольшое различие в поведении тут несущественно, т.к. особо ни на что не влияет. Поддержка корректной тестовой базы гораздо сложнее.

Рецепт 2. Бизнес-сценарии я предпочитаю тестировать на настоящих БД. А если в проекте заявлена возможность поддержки нескольких СУБД, тесты выполняются для нескольких СУБД. Почему? Да всё просто. В утверждении не хочется тестировать сервер баз данных, увы, происходит подмена понятий. Я, знаете ли, тестирую не то, что join работает или order by.

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

Обычно подобные тесты у меня выглядят так:

  • Для группы тестов (Fixture) с нуля генерируется по метаданным БД. Если нужно заполняются необходимые справочники.
  • Каждый сценарий сам добавляет нужные данные в процессе прохождения (пользователи ведь тоже это делают). В тестах на производительность не так, но это уже совсем другая история
  • После каждого теста лишние данные (кроме справочников) удаляются.

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

Транзакция и email


Просто дополню историю транзакция в БД по каким-то причинам упала, а e-mail ушёл. А какое веселье будет, когда транзакция подождёт недоступного почтового сервера, поставив колом всю систему из-за какого-нибудь уведомления, которое пользователь потом отправит в корзину, не читая

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

Итоги


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

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

P.S. Если у вас будет желание обсудить что-то в комментариях, буду рад принять в этом участие.
Подробнее..

Фильтры действий, или Как просто улучшить читаемость кода

25.01.2021 16:17:27 | Автор: admin

Введение


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

Роль фильтров в процессе обработки запроса


Сначала обсудим сами фильтры: для чего же они нужны? Фильтры позволяют выполнять определённые действия на различных стадиях обработки запроса в ASP.NET Core. Существуют следующие встроенные фильтры:

  • Фильтры авторизации (Authorization filters) выполняются самыми первыми и определяют, может ли пользователь выполнить текущий запрос.
  • Фильтры ресурсов (Resource filters) вызываются после фильтров авторизации и необходимы, как следует из названия, для обработки ресурсов. В частности, данный тип фильтров применяют в качестве механизма кэширования.
  • Фильтры действий (Action Filters) выполняют указанные в них операции до и после выполнения метода контроллера, обрабатывающего запрос.
  • Фильтры исключений (Exception Filters) используются для перехвата необработанных исключений, произошедших при создании контроллера, привязке модели и выполнении кода фильтров действий и методов контроллера.
  • И наконец, самыми последними вызываются фильтры результатов (Result Filters), если метод контроллера был выполнен успешно. Данный тип фильтров чаще всего используется, чтобы модифицировать конечные результаты, например, мы можем создать свой заголовок ответа, в котором добавим нужную нам информацию.


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



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

Внутреннее устройство фильтров действий


Фильтры действий в ASP.NET


Интерфейс IActionFilter, который нужно реализовать, чтобы создать фильтр действий, существовал ещё в ASP.NET MVC. Он определяет методы OnActionExecuting, который вызывается перед выполнением метода контроллера, и OnActionExecuted, который вызывается сразу после. Ниже представлен пример простейшего фильтра действий, который выводит информацию во время отладки приложения до и после выполнения метода контроллера:

public class CustomActionFilter:IActionFilter {         public void OnActionExecuting(ActionExecutingContext filterContext)         {             Debug.WriteLine("Before Action Execution");         }         public void OnActionExecuted(ActionExecutedContext filterContext)         {             Debug.WriteLine("After Action Execution");         } }


Чтобы использовать вышеуказанный фильтр, его нужно зарегистрировать. Для этого в файле FilterConfig.cs, который находится в папке App_Start, следует добавить следующую строку:

public static void RegisterGlobalFilters(GlobalFilterCollection filters) {         filters.Add(new HandleErrorAttribute());         filters.Add(new CustomActionFilter()); }


Но гораздо удобнее использовать фильтры как атрибуты. Для этих целей существует абстрактный класс ActionFilterAttribute, который унаследован от класса FilterAttribute, а также реализует интерфейсы IActionFilter и IResultFilter. Таким образом, наш класс можно переписать следующим образом:

public class CustomActionFilterAttribute:ActionFilterAttribute {         public override void OnActionExecuting(ActionExecutingContext filterContext)         {             Debug.WriteLine("Before Action Execution");         }         public override void OnActionExecuted(ActionExecutedContext filterContext)         {             Debug.WriteLine("After Action Execution");         } } 


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

public class HomeController : Controller {         [CustomActionFilter]         public ActionResult Index()         {             return View();         } }


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

Фильтры действий в ASP.NET Core


С появлением ASP.NET Core в фильтрах действий произошёл ряд изменений. Кроме интерфейса IActionFilter, теперь имеется ещё и IAsyncActionFilter, который определяет единственный метод OnActionExecutionAsync. Ниже приведён пример класса, реализующего интерфейс IAsyncActionFilter:

public class AsyncCustomActionFilterAttribute:Attribute, IAsyncActionFilter {         public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)         {             Debug.WriteLine("Before Action Execution");             await next();             Debug.WriteLine("After Action Execution");         } } 


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

Применяют такой фильтр так же, как и синхронный:

public class HomeController : Controller {         [AsyncCustomActionFilter]         public ActionResult Index()         {             return View();         } }


Также изменения затронули абстрактный класс ActionFilterAttribute: теперь он наследуется от класса Attribute и реализует синхронные и асинхронные интерфейсы для фильтров действий (IActionFilter и IAsyncActionFilter) и для фильтров результатов (IResultFilter и IAsyncResultFilter), а также интерфейс IOrderedFilter.

Фильтры действий в действии


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

public class Employee {         [Required(ErrorMessage = "First name is required")]         public string FirstName { get; set; }         [Required(ErrorMessage = "Last name is required")]         public string LastName { get; set; }         [AgeRestriction(MinAge = 18, ErrorMessage = "Date of birth is incorrect")]         public DateTime DateOfBirth { get; set; }         [StringLength(50, MinimumLength = 2)]         public string Position { get; set; }         [Range(45000, 200000)]         public int Salary { get; set; } } 


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

После того как были реализованы методы POST и PUT, мы видим, что оба метода содержат повторяющиеся части кода:

[HttpPost] public IActionResult Post([FromBody] Employee value) {             if (value == null)             {                 return BadRequest("Employee value cannot be null");             }             if (!ModelState.IsValid)             {                 return BadRequest(ModelState);             }             // Perform save actions             return Ok(); } [HttpPut] public IActionResult Put([FromBody] Employee value) {             if (value == null)             {                 return BadRequest("Employee value cannot be null");             }             if (!ModelState.IsValid)             {                 return BadRequest(ModelState);             }             // Perform update actions             return Ok(); } 


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

public class EmployeeValidationFilterAttribute : ActionFilterAttribute {         public override void OnActionExecuting(ActionExecutingContext context)         {             var employeeObject = context.ActionArguments.SingleOrDefault(p => p.Value is Employee);             if (employeeObject.Value == null)             {                 context.Result = new BadRequestObjectResult("Employee value cannot be null");                 return;             }             if (!context.ModelState.IsValid)             {                 context.Result = new BadRequestObjectResult(context.ModelState);             }         } } 


Теперь удаляем ставшие ненужными части кода и применяем созданный нами атрибут-фильтр:

public class EmployeeController : ControllerBase {         [EmployeeValidationFilter]         [HttpPost]         public IActionResult Post([FromBody] Employee value)         {             // Perform save actions             return Ok();         }         [EmployeeValidationFilter]         [HttpPut]         public IActionResult Put([FromBody] Employee value)         {             // Perform update actions             return Ok();         } } 


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

[EmployeeValidationFilter] public class EmployeeController : ControllerBase {             // Perform update actions } 


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

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

public class LoggingFilter: IActionFilter {         private readonly ILogger _logger;         public LoggingFilter(ILoggerFactory loggerFactory)         {             _logger = loggerFactory.CreateLogger<LoggingFilter>();         }         public void OnActionExecuted(ActionExecutedContext context)         {             _logger.LogInformation($"{context.ActionDescriptor.DisplayName} executed");         }         public void OnActionExecuting(ActionExecutingContext context)         {             _logger.LogInformation($"{context.ActionDescriptor.DisplayName} is executing");         } } 


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

services.AddControllers(options => {                 options.Filters.Add<LoggingFilter>(); }); 


Если же нам нужно применить фильтр, например, к определённому методу контроллера, то следует его использовать вместе с ServiceFilterAttribute:

[HttpPost] [ServiceFilter(typeof(LoggingFilter))] public IActionResult Post([FromBody] Employee value) 


ServiceFilterAttribute является фабрикой для других фильтров, реализующей интерфейс IFilterFactory и использующей IServiceProvider для получения нужного фильтра. Поэтому в Startup.cs нам необходимо зарегистрировать наш фильтр следующим образом:

services.AddSingleton<LoggingFilter>(); 


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

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

public class ProviderFilter : IActionFilter {         private readonly IDataProvider _dataProvider;         public ProviderFilter(IDataProvider dataProvider)         {             _dataProvider = dataProvider;         }         public void OnActionExecuted(ActionExecutedContext context)         {         }         public void OnActionExecuting(ActionExecutingContext context)         {             object idValue;             if (!context.ActionArguments.TryGetValue("id", out idValue))             {                 throw new ArgumentException("id");             }             var id = (int)idValue;             var result = _dataProvider.GetElement(id);             if (result == null)             {                 context.Result = new NotFoundResult();             }             else             {                 context.HttpContext.Items.Add("result", result);             }         } } 


Применить этот фильтр можно так же, как и фильтр из предыдущего примера, с помощью ServiceFilterAttribute.

Фильтры действий раньше очень часто применяли, чтобы заблокировать контент для определённых браузеров на основе информации о User-Agent. На ранних этапах становления веб-разработки многие сайты создавались исключительно для наиболее популярных браузеров, остальные же считались запрещёнными. Сейчас данный подход является нежелательным, т.к. рекомендуется создавать такую HTML-разметку, которую смогло бы поддерживать большинство браузеров. Тем не менее, в некоторых случаях разработчику важно знать источник запроса. Ниже представлен пример получения User-Agent-информации в фильтре действий:

public class BrowserCheckFilter : IActionFilter {         public void OnActionExecuting(ActionExecutingContext context)         {             var userAgent = context.HttpContext.Request.Headers[HeaderNames.UserAgent].ToString().ToLower();             // Detect if a user uses IE             if (userAgent.Contains("msie") || userAgent.Contains("trident"))             {                 // Do some actions              }         }         public void OnActionExecuted(ActionExecutedContext context)         {         } } 


Стоит, однако, заметить, что вышеуказанный метод имеет ещё один недостаток. Многие браузеры умеют прятать или подделывать значения, указанные в User-Agent, поэтому данный способ не является однозначно достоверным в определении типа пользовательского браузера.

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

public class LocalizationActionFilterAttribute: ActionFilterAttribute {         public override void OnActionExecuting(ActionExecutingContext filterContext)         {             var language = (string)filterContext.RouteData.Values["language"] ?? "en";             var culture = (string)filterContext.RouteData.Values["culture"] ?? "GB";             Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo($"{language}-{culture}");             Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo($"{language}-{culture}");         } } 


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

                endpoints.MapControllerRoute(name:"localizedRoute",                     pattern: "{language}-{culture}/{controller}/{action}/{id}",                     defaults: new                     {                         language = "en",                         culture = "GB",                         controller = "Date",                         action = "Index",                         id = "",                     });


Код выше создаёт маршрут с именем localizedRoute, у которого в шаблоне имеется параметр, отвечающий за локализацию. Значение по умолчанию для этого параметра en-GB.

Теперь создадим контроллер с именем DateController, который будет обрабатывать наш запрос, и представление, которое будет отображать локализованную дату. Код контроллера просто возвращает представлению текущую дату:

[LocalizationActionFilter] public class DateController : Controller {         public IActionResult Index()         {             ViewData["Date"] = DateTime.Now.ToShortDateString();             return View();         } }  


После того как пользователь перешёл по ссылке localhost:44338/Date, он увидит в браузере следующее:

image
На скриншоте выше текущая дата представлена с учётом локализации, заданной по умолчанию, т.е. с en-GB. Теперь, если пользователь перейдёт по ссылке, в которой будет явно указана культура, например, en-US, то он увидит следующее:


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

Заключение


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

Перевод Что из себя представляет класс 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 разработчик.

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

Подробнее..

Учим ASP.NET Core новым трюкам на примере Json Rpc 2.0

06.04.2021 04:10:16 | Автор: admin

Хотите добиться нестандартного поведения от aspnet core? Мне вот понадобилось добавить прозрачную поддержку Json Rpc. Расскажу о том, как я искал решения для всех хотелок, чтобы вышло красиво и удобно. Может быть, вам пригодятся знания о разных точках расширения фреймворка. Или о тонкостях поддержки Json Rpc, даже на другом стеке/языке.


В результате получилась библиотека, которая позволяет работать с Json Rpc, вообще не задумываясь, что он спрятан под капотом. При этом пользователю не нужно уметь ничего нового, только привычный aspnet mvc.


Введение


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


В тексте под Aspnet подразумевается ASP.Net Core MVC, в частности все писалось на 2.2, с прицелом на то, что выйдет 5.x и допилим под него.

И Json Rpc протокол JSON RPC 2.0 поверх HTTP.

Еще для чтения стоит ознакомиться с терминами протокола: method, params, request, notification...


Зачем все это?


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


У нас для синхронного общения по HTTP принят Json Rpc 2.0. А у шарпистов основной фреймворк ASP.NET Core MVC, и он заточен под REST. И на нем уже написано некоторое количество сервисов. Если немного абстрагироваться, то REST, JSON RPC, и любой RPC вообще об одном и том же: мы хотим, чтобы на удаленной стороне что-то произошло, передаем параметры, ожидаем результат. А еще транспорт совпадает, все по HTTP. Почему бы не воспользоваться привычным aspnet для работы с новым протоколом? Хочется при этом поддержать стандарт полностью: в компании много разных стеков, и у всех Json Rpc клиенты работают немного по-разному. Будет неприятно нарваться на ситуацию, когда запросы например от питонистов не заходят, и нужно что-то костылить.


Со стороны aspnet-а можно делать типовые вещи разными способами, лишь бы разработчику было удобно. Довольно много ресурсов командой уже потрачено на то, чтобы разобраться, какой из способов больше нам подходит. А ведь еще нужно поддерживать единообразие. Чтобы никто не сходил с ума, читая код сервиса, который написан коллегой полгода назад. То есть вы нарабатываете best practices, поддерживаете какие-то небольшие библиотечки вокруг этого, избавляетесь от бойлерплейта. Не хочется это терять.


Еще немаловажный момент: желательно, чтобы опыт у разработчиков не терял актуальность, то есть не затачивался на внутренние костыли и самописные фреймворки. Если ты три года пишешь веб-сервисы на шарпе, претендуешь на мидлосеньора, а потом не можешь сделать, например, авторизацию общепринятыми способами в пустом проекте, потому что у вас было принято писать в коде контроллера if(cookie.Contains(userName)) это беда.


Конечно, протокол уже реализован на C#, и не раз. Ищем готовые библиотеки. Выясняется, что они либо тащат свои концепции, то есть придется долго вникать, как это готовить. Либо делают почти то, что нужно, но тяжело кастомизируются и переизобретают то, что в aspnet уже есть.

Собираем хотелки и пишем код


Чего хочется добиться? Чтобы как обычно писать контроллеры, накидывать фильтры и мидлвари, разбирать запрос на параметры. Чтобы наработанные best practices и библиотеки для aspnet подходили as-is. И при этом не мешать работать существующему MVC коду. Так давайте научимся обрабатывать Json Rpc теми средствами, что нам предоставляет фреймворк!


Request Routing


Казалось бы, HTTP уже есть, и нам от него надо только обрабатывать POST и возвращать всегда 200 OK. Контент всегда в JSON. Все прекрасно, сейчас напишем middleware и заживем.
Но не тут-то было! Мидлварь написать можно, только потом будем получать все запросы в один action, а в нем придется switch(request.Method) или запускать хендлеры из DI каким-нибудь костылем. А кто будет авторизацию и прочие фильтры прогонять в зависимости от метода? Переизобретать все это заново ящик Пандоры: делаешь свой аналог пайплайна, а потом придется поддерживать общий код и для aspnet, и для своего пайплайна. Ну, чтобы не было внезапных различий между тем, как вы проверяете роли или наличие какого-то заголовка.
Значит, придется влезть в роутинг и заставить его выбирать controller и action, глядя на тело HTTP запроса, а не только на url.


ActionMethodSelectorAttribute


К сожалению, все часто используемые инструменты для роутинга не позволяют парсить запрос или выполнять произвольный код. Есть стандартный роутер, но его нельзя без лишних проблем расширить или что-то в нем перегрузить. IRouter целиком писать, конечно же, не надо, если мы хотим гарантировать что обычный MVC не сломается. Казалось бы, есть Endpoint Routing, но в 2.2 сделана его начальная и неполная реализация, и чего-то полезного с ним напрямую не сделаешь. Костыли типа переписывания еndpoint на свой (после того, как Endpoint Routing отработал) почему-то не взлетели. Можно, конечно, прямо в middleware сделать редирект на нужный url, и это будет работать, только url испортится.


После долгих поисков был найден ActionMethodSelectorAttribute, который делает как раз то, что нужно: позволяет вернуть true/false в момент выбора controller и action! У него есть контекст с именем текущего метода-кандидата и его контроллера. Очень удобно.


Остается две проблемы: как повесить атрибут на все нужные нам контроллеры, и как не парсить тело каждый раз, когда выполняется проверка на "пригодность" метода?


Conventions


Атрибуты на контроллеры можно расставлять кодогенерацией, но это слишком сложно. У фреймворка и на этот случай есть решение: IControllerModelConvention, IActionModelConvention. Это что-то вроде знакомых многим Startup Filters: запускаются один раз на старте приложения и позволяют сделать все что угодно с метаданными контроллеров и методов, например переопределить роутинг, атрибуты, фильтры.


С помощью conventions мы можем решить сразу несколько задач. Сначала определимся, как мы будем отличать Json Rpc контроллеры от обычных. Не долго думая, идем по тому же пути, что и Microsoft: сделаем базовый класс по аналогии с ControllerBase.


public abstract class JsonRpcController : ControllerBase {}

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


полезный код в ControllerConvention
public void Apply(ControllerModel controllerModel){    if (!typeof(JsonRpcController).IsAssignableFrom(controllerModel.ControllerType))    {        return;    }    controllerModel.Selectors.Clear();    controllerModel.Filters.Insert(0, new ServiceFilterAttribute(typeof(JsonRpcFilter)));}

Selectors отвечают за роутинг, и я честно не смог найти, почему это коллекция. В любом случае, нам не нужен стандартный роутинг по правилам MVC, поэтому удаляем все, что есть. Забегая вперед, применяем JsonRpcFilter, который будет отвечать за оборачивание ActionResult.


А вот ActionConvention
public void Apply(ActionModel actionModel){    if (!typeof(JsonRpcController).IsAssignableFrom(actionModel.Controller.ControllerType))    {        return;    }    actionModel.Selectors.Clear();    actionModel.Selectors.Add(new SelectorModel()    {        AttributeRouteModel = new AttributeRouteModel() {Template = "/api/jsonrpc"},        ActionConstraints = {new JsonRpcAttribute()}    });}

Здесь на каждый метод в наших контроллерах мы повесим один и тот же route, который потом вынесем в настройки.


И добавим тот самый атрибут
class JsonRpcAttribute : ActionMethodSelectorAttribute{    public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)    {        var request = GetRequest();  // пока не понятно как        // return true если action подходит под запрос, например:        return request.Method == action.DisplayName;    }}

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


Middleware


Пишем middleware, которая будет проверять, что запрос похож на Json Rpc: правильный Content-Type, обязательно POST, и тело содержит подходящий JSON. Запрос можно десериализовать в объект и сложить в HttpContext.Items. После этого его можно будет достать в любой момент.


Есть одна загвоздка: у middleware еще нет информации о типе, в который нужно десериализовать params, поэтому мы пока оставим их в виде JToken.


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


Parameter Binding


Мы научились подбирать контроллер и метод под запрос. Теперь нужно что-то делать с аргументами. Можно достать то, что десериализовала middleware из HttpContext.Items, и десериализовать JToken вручную в нужный тип, но это бойлерплейт и ухудшение читаемости методов. Можно взять JSON целиком из тела запроса с помощью [FromBody], но тогда всегда будет присутствовать шапка протокола: id, версия, метод. Придется каждую модель оборачивать этой шапкой: Request<MyModel> или class MyModel: RequestBase, и снова получим бойлерплейт.


Эти решения были бы еще терпимы, если бы протокол не вставлял палок в колеса.


Разные params


Json Rpc считает, что параметры, переданные массивом [] это одно и то же, что и параметры, переданные объектом {}! То есть, если нам прислали массив, нужно подставлять их в свой метод по порядку. А если прислали объект, то разбирать их по именам. Но вообще, оба сценария должны работать для одного и того же метода. Например, вот такие params эквивалентны и должны одинаково биндиться:


{"flag": true, "data": "value", "user_id": 1}[1, "value", true]

public void DoSomething(int userId, string data, bool flag)

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


Реализация


Посмотрим, что нам доступно для управления биндингом. Есть IModelBinder и IModelBinderProvider, но они смотрят на тип объекта. Заранее мы не знаем, какой тип пользователь захочет биндить. Может быть, int или DateTime. Мы не хотим конфликтовать с aspnet, поэтому просто добавить свой биндер для всех типов нельзя. Есть IValueProvider, но он возвращает только строки. Наконец, есть атрибуты FromBody, FromQuery и так далее. Смотрим в реализацию, находим интерфейс IBinderTypeProviderMetadata. Он нужен, чтобы возвращать нужный binder для параметра. Как раз то, что нужно!


Пишем свой FromParamsAttribute
 [AttributeUsage(AttributeTargets.Parameter)]public class FromParamsAttribute : Attribute, IBindingSourceMetadata, IBinderTypeProviderMetadata{    public BindingSource BindingSource => BindingSource.Custom;    public Type BinderType => typeof(JsonRpcModelBinder);}

Теперь атрибут придется повесить на каждый параметр. Вернемся к conventions и напишем реализацию IParameterModelConvention. Только оказывается, что нужно не использовать атрибут, а создавать


`BindingInfo` с той же информацией
public void Apply(ParameterModel parameterModel){    if (!typeof(JsonRpcController).IsAssignableFrom(parameterModel.Action.Controller.ControllerType))    {        return;    }    if (parameterModel.BindingInfo == null)    {        parameterModel.BindingInfo = new BindingInfo()        {            BinderType = typeof(JsonRpcModelBinder),            BindingSource = BindingSource.Custom        };    }}

Проверка на BindingInfo == null позволяет использовать другие атрибуты, если нужно. То есть можно смешивать FromParams и штатные FromQuery, FromServices. Ну а по умолчанию, если ничего не указано, convention применит BindingInfo, аналогичный FromParams.


Удобства


Стоит учесть сценарий, когда неудобно разбирать params на отдельные аргументы. Что, если клиент просто прислал свой объект "как есть", а в нем очень много полей? Нужно уметь биндить params целиком в один объект:


{"flag": true, "data": "value", "user_id": 1}

public void DoSomething(MyModel model)

Но что делать, если придет json-массив? Теоретически, можно бы узнать порядок properties в объекте, и биндить по порядку. Но из рефлексии этого не достать, такая информация просто не сохраняется. Поэтому массив в объект сбиндить не получится без костылей типа атрибутов с номерами Но можно сделать проще: сделаем эту фичу опциональной. Да, она не работает с массивами, что ломает поддержку протокола, поэтому придется выбирать. Добавим параметр в наш атрибут:


BindingStyle
public enum BindingStyle { Default, Object, Array }...public FromParamsAttribute(BindingStyle bindingStyle){    BindingStyle = bindingStyle;}

Default поведение по умолчанию, когда содержимое params биндится в аргументы. Object когда пришел json-объект, и мы биндим его в один параметр целиком. Array когда пришел json-массив и мы биндим его в коллекцию. Например:


// это успешно сбиндится: {"flag": true, "data": "value", "user_id": 1}// а это будет ошибкой: [1, "value", true]public void DoSomething1([FromParams(BindingStyle.Object)] MyModel model)// это успешно сбиндится: [1, "value", true]// а это будет ошибкой: {"flag": true, "data": "value", "user_id": 1}public void DoSomething2([FromParams(BindingStyle.Array)] List<object> data)

Всю эту логику придется реализовать в JsonRpcModelBinder. Приводить здесь код нет смысла: его много, но он тривиальный. Разве что упомяну несколько интересных моментов:


Как сопоставить имя аргумента и ключ в json-объекте?


JsonSerizlizer не позволяет "в лоб" десериализовать ключ json объекта как шарповое имя property или аргумента. Зато позволяет сериализовать имя в ключ.


// вот так не получится{"user_id": 1} => int userId//  зато можно наоборот и запомнить это в метаданныхint userId => "user_id"

То есть нужно для каждого аргумента узнать его "json-имя" и сохранить в метаданных. У нас уже есть conventions, там и допишем нужный код.


Учимся у aspnet


Кода в биндере получается много, как ни крути. Оказалось полезно подсмотреть в реализацию стандартного ModelBinder и взять оттуда подходы: свой контекст, обработка ошибок, результат биндинга как отдельный объект.


Регистрация в DI-контейнере


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


резолвить зависимости вручную
public Task BindModelAsync(ModelBindingContext context){    var service = context.HttpContext.RequestServices.GetServices<IService>();    // ...}

Error handling


Все ошибки протокол предлагает возвращать в виде специального ответа, в котором могут быть любые детали. Еще там описаны некоторые крайние случаи и ошибки для них. Придется перехватывать все exception-ы, заворачивать их в Json Rpc ответ, уметь прятать stack trace в зависимости от настроек (мы же не хотим высыпать все подробности на проде?). А еще нужно дать пользователю возможность вернуть свою Json Rpc ошибку, вдруг у кого-то на этом логика построена. В общем, ошибки придется перехватывать на разных уровнях. После написания десятого try/catch внутри catch поневоле начинаешь задумываться, что неплохо бы иметь возможность писать код с гарантией отсутствия exception-ов, или хотя бы с проверкой, что ты перехватил все, что можно...


Action вернул плохой ActionResult или Json Rpc ошибку


Возьмем IActionFilter.OnResultExecuting и будем проверять, что вернулось из метода: нормальный объект завернем в Json Rpc ответ, плохой ответ, например 404, завернем в Json Rpc ошибку. Ну или метод уже вернул ошибку по протоколу.


Binding failed


Нам пригодится IAlwaysRunResultFilter.OnActionExecuting: можно проверить context.ModelState.IsValid и понять, что биндинг упал. В таком случае вернем ошибку с сообщением, что не получилось у биндера. Если ничего не делать, то в action попадут кривые данные, и придется проверять каждый параметр на null или default.


Схожим образом работает стандартный ApiControllerAttribute: он возвращает 400, если биндинг не справился.


Что-то сломалось в pipeline


Если action или что-нибудь в pipeline выбросит exception, или решит записать HttpResponse, то единственное место, где мы еще можем что-то сделать с ответом, это middleware. Придется и там проверять, что получилось после обработки запроса: если HTTP статус не 200 или тело не подходит под протокол, придется заворачивать это в ошибку. Кстати, если писать ответ прямо в HttpResponse.Body, то сделать с ним уже ничего не получится, но для этого тоже будет решение чуть ниже.


Ошибки это сложно


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


class JsonRpcErrorFactory{    IError NotFound(object errorData){...}    IError InvalidRequest(object errorData){...}    IError Error(int code, string message, object errorData){...}    IError Exception(Exception e){...}    // и так далее}

Batch


Batch-запросы aspnet не поддерживает никак. А они требуются стандартом. И хочется, чтобы на каждый запрос из батча был свой пайплайн, чтобы в тех же фильтрах не городить огород. Можно, конечно, сделать прокси, который будет разбирать батч на отдельные запросы, отправлять их на localhost, потом собирать ответ. Но это кажется безумным оверхедом из-за сериализации HTTP body в байты, установления соединения После долгих путешествий по issues в Github, находим грустный тред о том, что батчи когда-то были, но пока нет и неизвестно когда вернутся. А еще они есть в OData, но это целый отдельный мир, фреймворк поверх фреймворка погодите-ка, мы же тоже пишем что-то такое!. Там же находим идею и репозиторий с реализацией: можно скопировать HttpContext и в middleware позвать next() со своим контекстом, а потом собрать результат и отправить все вместе уже в настоящий HttpContext. Это поначалу немного ломает мозг, потому что мы привыкли к мантре: нужно передавать управление вызовом next(context), и по-другому никто эту штуку не использует.


Таким образом, middleware будет парсить Json Rpc запрос, создавать копию контекста, и вызывать пайплайн дальше. Это же пригодится для перехвата ошибок, если кто-то решит писать прямо в HttpResponse.Body: мы вместо настоящего body подсунем MemoryStream и проверим, что там валидный JSON.


У этого подхода есть минус: мы ломаем стриминг для больших запросов/ответов. Но что поделать, JSON не подразумевает потоковую обработку. Для этого, конечно, есть разные решения, но они гораздо менее удобны, чем Json.NET.


ID


Протокол требует поле id в запросе, при чем там могут быть число, строка или null. В ответе должен содержаться такой же id. Чтобы одно и то же поле десериализовалось как число или строка, пришлось написать классы-обертки, интерфейс IRpcId и JsonConverter, который проверяет тип поля и десериализует в соответствующий класс. В момент, когда мы сериализуем ответ, из HttpContext.Items достаем IRpcId и прописываем его JToken-значение. Таким образом, пользователю не надо самому заморачиваться с проставлением id и нет возможности забыть об этом. А если нужно значение id, можно достать из контекста.


Notification


Если id отсутствует, то это не запрос, а уведомление (notification). На notification не должен уходить ответ: ни успешный, ни с ошибкой, вообще никакой. Ну, то есть по HTTP-то мы вернем 200, но без тела. Чтобы все работало одинаково для запросов и нотификаций, пришлось выделить абстракцию над ними, и в некоторых местах проверять, запрос ли это и нужно ли сериализовать ответ.


Сериализация


Aspnet умеет сериализовать JSON. Только у него свои настройки, а у нас свои. Сериализация настраивается с помощью Formatters, но они смотрят только на Content-Type. У Json Rpc он совпадает с обычным JSON, поэтому просто так добавить свой форматтер нельзя. Вложенные форматтеры или своя реализация плохая идея из-за сложности.


Решение оказалось простым: мы уже оборачиваем ActionResult в фильтре, там же можно


подставить нужный форматтер
...var result = new ObjectResult(response){    StatusCode = 200,};result.Formatters.Add(JsonRpcFormatter);result.ContentTypes.Add(JsonRpcConstants.ContentType);...

Здесь JsonRpcFormatter это наследник JsonOutputFormatter, которому переданы нужные настройки.


Configuration


Нужно дать пользователю разные "ручки". При чем удобно дать настройку по умолчанию (например дефолтный route) и возможность умолчание обойти, например, атрибутом: когда вот для этого контроллера нужен свой особенный route.


Имя метода


У Json Rpc запросов есть поле method, которым определяется, что должно быть вызвано на сервере. И это поле просто строка. Ей пользуются как угодно. Придется научить сервер понимать распространенные варианты.


public enum MethodStyle {ControllerAndAction, ActionOnly}

ControllerAndAction будет интерпретировать method как class_name.method_name.


ActionOnly просто method_name.


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


Сериализация


Еще встает вопрос с JSON-сериализацией. Формат "шапки" строго обозначен в протоколе, то есть переопределять его нужно примерно никогда. А вот формат полей params, result и error.data оставлен свободным. Пользователь может захотеть сериализацию со своими особыми настройками. Нужно дать такую возможность, при этом не позволяя сломать сериализацию шапки и не накладывая особых требований на пользователя. Например, для работы с шапкой используются хитрые JsonConverterы, и не хотелось бы чтобы они как-то торчали наружу. Для этого сделана минимальная обертка поверх JsonSeralizer, чтобы пользователь мог зарегистрировать в DI свой вариант и не сломать REST/MVC.


Нестандартные ответы


Бывает, что нужно вернуть бинарный файл по HTTP, или Redirect для авторизации. Это явно идет в разрез с Json Rpc, но очень удобно. Такое поведение нужно разрешать при необходимости.


Объединяем все вместе


Добавим классы с опциями, чтобы рулить умолчаниями
public class JsonRpcOptions{    public bool AllowRawResponses { get; set; }  // разрешить ответы не по протоколу?    public bool DetailedResponseExceptions { get; set; }  // маскировать StackTrace у ошибок?    public JsonRpcMethodOptions DefaultMethodOptions { get; set; }  // см. ниже    public BatchHandling BatchHandling { get; set; }  // задел на параллельную обработку батчей в будущем}public class JsonRpcMethodOptions{        public Type RequestSerializer { get; set; }  // пользовательский сериалайзер    public PathString Route { get; set; }  // маршрут по умолчанию, например /api/jsonrpc    public MethodStyle MethodStyle { get; set; }  // см. выше}

И атрибуты, чтобы умолчания переопределять:


  • FromParams про который было выше
  • JsonRpcMethodStyle чтобы переопределить MethodStyle
  • JsonRpcSerializerAttribute чтобы использовать другой сериалайзер.

Для роутинга свой атрибут не нужен, все будет работать со стандартным [Route].


Подключаем


Пример кода, который использует разные фичи. Важно заметить, что это никак не мешает обычному коду на aspnet!


Startup.cs
services.AddMvc()    .AddJsonRpcServer()    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);// или с опциямиservices.AddMvc()    .AddJsonRpcServer(options =>    {        options.DefaultMethodOptions.Route = "/rpc";        options.AllowRawResponses = true;    })    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Контроллер
public class MyController : JsonRpcController{    public ObjectResult Foo(object value, bool flag)    {        return Ok(flag ? value : null);    }    public void BindObject([FromParams(BindingStyle.Object)] MyModel model)    {    }    [Route("/test")]    public string Test()    {        return "test";    }    [JsonRpcMethodStyle(MethodStyle.ActionOnly)]    public void SpecialAction()    {    }    [JsonRpcSerializer(typeof(CamelCaseJsonRpcSerializer))]    public void CamelCaseAction(int myParam)    {    }}

Клиент


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


HttpClient


В .net core HttpClient научили работать с DI, типизировать и вообще все стало гораздо удобнее. Грех не воспользоваться!


Batch


По протоколу ответы на батч-запросы нужно разбирать по их id. Поэтому, чтобы сделать батч-запрос, пользователь должен заранее задать id, и запомнить их. А потом перебирать их, и из батч-ответа пытаться достать конкретный ответ или ошибку.


Обработка ошибок


Снова сложности с обработкой ошибок. Дело в том, что мы не знаем, с какими правилами сериализации сервер вернул ошибку: со стандартными, как в "шапке", или с кастомными, например когда мы договорились на клиенте и на сервере использовать camelCase, но у сервера что-то сломалось в middleware и дело до нашего action не дошло вообще. Поэтому придется пробовать десериализовать и так, и так. Здесь нет очевидно хорошего решения, поэтому интерфейс response содержит


Разные методы для интерпретации ответа
T GetResponseOrThrow<T>();  // достать успешный ответ, если нет - достать ошибку и выбросить ее как исключениеT AsResponse<T>(); // только достать ответError<JToken> AsAnyError(); // достать ошибку, не десериализуя ееError<T> AsTypedError<T>(); // достать ошибку по правилам сериализации как в запросе или по дефолтным, если не получилосьError<ExceptionInfo> AsErrorWithExceptionInfo(); // достать ошибку с деталями exception-а с сервера

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


Developer Experience


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


TODO


В планах: добавить поддержку aspnetcore 5.x, добить покрытие тестами до 100%, перевести документацию на русский и добавить параллельную обработку батчей. Ну и, конечно же, поддерживать проект как нормальный open source: любой фидбек, пуллреквесты и issues приветствуются!


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


Ссылки


Исходники


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


Бонус


Статья лежала в черновиках почти год, за это время к библиотеке была добавлена поддержка автодокументации Swagger и OpenRpc. А еще сейчас в разработке поддержка OpenTelemetry. Кому-нибудь интересны подробности? Там местами такая жуть...

Подробнее..

Обновления ASP.NET Core в .NET 6 Preview 1

25.02.2021 10:21:06 | Автор: admin

Новая версия .NET, 6 Preview 1, уже доступна и готова к вашей оценке. Это первая предварительная версия .NET 6, следующего крупного обновления платформы .NET. Ожидается, что .NET 6 поступит в полноценный доступ в ноябре этого года и будет выпуском с долгосрочной поддержкой (LTS).

Если вы работаете с Windows и используете Visual Studio, мы рекомендуем установить последнюю предварительную версию Visual Studio 2019 16.9. Если вы используете macOS, мы рекомендуем установить последнюю предварительную версию Visual Studio 2019 для Mac 8.9.

Основная работа, запланированная с ASP.NET Core в .NET 6

.NET 6 использует открытый процесс планирования, поэтому вы можете изучить все основные темы, запланированные для этого релиза, на Blazor-веб-сайте themesof.net. В дополнение к этим верхнеуровневым темам мы собираемся также предоставить множество улучшений, ориентированных на пользователей. Вы можете найти список основных задач, запланированных для ASP.NET Core в .NET 6, в нашем выпуске дорожной карты. Вот некоторые из основных функций ASP.NET Core, запланированных для выпуска .NET 6:

Мы приветствуем отзывы и участие в процессе планирования и создания на GitHub.

Что нового в ASP.NET Core в .NET 6 Preview 1?

  • Поддержка IAsyncDisposableв MVC

  • DynamicComponent

  • InputElementReferenceразделен на релевантные компоненты

  • dotnet watchтеперь являетсяdotnet watch runпо дефолту

  • Nullable reference type annotations

Начало работы

Чтобы начать работу с ASP.NET Core в .NET 6 Preview 1, установите .NET 6 SDK.

Обновление существующего проекта

Чтобы обновить существующее приложение ASP.NET Core с .NET 5 до .NET 6 Preview 1:

  • Обновите целевую платформу для вашего приложения, доnet6.0.

  • Обновите все ссылки на пакеты Microsoft.AspNetCore.* до6.0.0-preview.1.*.

  • Обновите все ссылки на пакеты Microsoft.Extensions.* до6.0.0-preview.1.*.

См. полный список критических изменений в ASP.NET Core для .NET 6 здесь.

DynamicComponent

DynamicComponent - это новый встроенный компонент Blazor, который можно использовать для динамической визуализации компонента, указанного по типу.

<DynamicComponent Type="@someType" />

Параметры могут быть переданы визуализируемому компоненту с помощью dictionary:

<DynamicComponent Type="@someType" Parameters="@myDictionaryOfParameters" />@code {    Type someType = ...    IDictionary<string, object> myDictionaryOfParameters = ...}

InputElementReferenceразделен на релевантные компоненты

Соответствующие встроенные компоненты Blazor ввода теперь предоставляют удобную ссылку ElementReference для базового ввода, что упрощает распространенные сценарии, такие как установка фокуса пользовательского интерфейса на вводе. Затронутые компоненты: InputCheckbox, InputDate, InputFile, InputNumber, InputSelect, InputText и InputTextArea.

dotnet watchтеперь являетсяdotnet watch runпо дефолту

Запуск dotnet watch теперь будет запускать dotnet watch run по умолчанию, экономя драгоценное время ввода.

Nullable Reference Type Annotations

Мы применяем аннотации обнуляемости к частям ASP.NET Core. Значительное количество новых API было аннотировано в .NET 6 Preview 1.

Используя новую функцию C# 8, ASP.NET Core может обеспечить дополнительную безопасность во время компиляции при обработке ссылочных типов, например защиту от исключений нулевых ссылок. Проекты, которые выбрали использование аннотаций, допускающих значение NULL, могут видеть новые предупреждения во время сборки от API-интерфейсов ASP.NET Core.

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

<PropertyGroup>    <Nullable>enable</Nullable></PropertyGroup>

Подробности читайте здесь.

Подробнее..

Перевод Объединяем Blazor и Razor Pages в одном ASP.NET Core 3 приложении

25.08.2020 00:11:37 | Автор: admin
Перевод статьи подготовлен в преддверии старта курса C# ASP.NET Core разработчик.





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



Предисловие


Выход Blazor на золото должен произойти через две недели. Многие вещи в проекте еще подвержены достаточно резким изменениям, и последняя предварительная 9-я версия значительно усложнила взаимодействие между компонентами Razor Pages и Blazor: теперь невозможно передавать параметры из страницы Razor в компонент Blazor с помощью Html.RenderComponentAsync. Это может измениться в будущем, но вполне вероятно, что в .NET Core 3.0 он появится с этим ограничением.

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

Шаг первый: поддержка Blazor


Итак, у нас есть уже существующее Razor Pages приложение, которое было преобразовано в .NET Core 3.



Для начала, вам нужно добавить в свое приложение поддержку Blazor. Эта поддержка позволит вам отображать Blazor-компоненты с Razor-страницы. Официальная документация полностью проводит вас по этому процессу, но вот его краткое изложение.

Startup.cs:

Нам необходимо добавить Services.AddServerSideBlazor в ConfigureServices и endpoints.MapBlazorHub в Configure:



_Layout.cshtml:

JS-библиотека Blazor необходима для подключения Blazor на стороне сервера. Она может быть добавлена в _Layout.cshtml:



?
<script src="_framework/blazor.server.js"></script>

_Imports.razor:

Нам также потребуется новый файл с именем _Imports.razor. Он должен быть добавлен в папку Pages:



_Imports.razor используется для установки using-выражений для ваших Blazor-компонентов. Начать можно со следующего:

?
@using System.Net.Http@using Microsoft.AspNetCore.Components.Forms@using Microsoft.AspNetCore.Components.Routing@using Microsoft.JSInterop@using Microsoft.AspNetCore.Components.Web

И на этом все. Теперь наше приложение поддерживает Blazor. Мы можем проверить это, скопировав классический компонент Counter (счетчик) в наше приложение



?
@page "/counter" <h1>Counter</h1><p>Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code {    int currentCount = 0;     void IncrementCount()    {        currentCount++;    }}

И отредактируем Privacy.cshtml, чтобы включить компонент Counter:

<a href="http://personeltest.ru/aways/mikaelkoskinen.net/post/combining-razor-blazor-pages-single-asp-net-core-3-application#">?</a>@page@model PrivacyModel@{    ViewData["Title"] = "Privacy Policy";}<h1>@ViewData["Title"]</h1> <p>Use this page to detail your site's privacy policy.</p> <component>@(await Html.RenderComponentAsync<Counter>(RenderMode.Server))</component>

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



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

Шаг второй: поддержка Blazor Pages


Наш Blazor-компонент определяет маршрут /counter:



Но переход по нему не работает:



Наша цель заставить работать маршрутизацию на Blazor-страницы. И мы хотим, чтобы Blazor-страницы использовали тот же макет, что и Razor-страницы. Для этого нам понадобится несколько вещей, начиная с маршрутизатора (Router).

App.razor:

Создайте новый файл App.razor в папке
Pages
:



Компонент Router определен в App.razor:

?

@using Microsoft.AspNetCore.Components.Routing <Router AppAssembly="typeof(Program).Assembly">    <Found Context="routeData">        <RouteView RouteData="routeData"/>    </Found>    <NotFound>        <h1>Page not found</h1>        <p>Sorry, but there's nothing here!</p>    </NotFound></Router>

Router автоматически просматривает все Blazor-компоненты с помощью page-директивы и добавляет к ним маршруты.

_Host.cshtml:

Нам также нужна страница, которая будет использоваться как хост для Blazor-страниц. Ее можно назвать как угодно, но в шаблонах Blazor по умолчанию используется _Host.cshtml, которое вполне нас устроит (впрочем, как и любое другое). В _Host.cshtml мы можем определить макет, который в нашем случае будет таким же, как у Razor-страниц.



_Host.cshtml содержит вызов Html.RenderComponentAsync:

?

@page "/blazor" @{    Layout = "_Layout";} <app>    @(await Html.RenderComponentAsync<App>(RenderMode.Server))</app>

Startup.cs:

И, наконец, небольшое дополнение к методу Configure Startup.cs. Ранее мы добавляли MapBlazorHub, а теперь нам нужно добавить вызов MapFallbackToPage, указывающий на новый _Host.cshtml:



И на этом все! Теперь нам просто нужно протестировать наш сетап. Добавьте счетчик страниц Blazor (Counter) в свой макет, отредактировав Pages/Shared/_Layout.cshtml:



Когда мы запускаем приложение, мы видим рабочую Blazor-страницу в нашем Razor Pages приложении:



И мы не ломали поддержку добавления Blazor-компонентов в Razor Pages:



Примечания


Следует отметить пару моментов:

  • Маршруты Blazor работают только тогда, когда они указывают на корень. Если /counter изменить, например, на /products/counter, страница не сможет загрузить требуемый blazor.server.js. Вместо этого мы получим 404. Должна быть возможность изменить тег script, чтобы он мог загружать требуемый скрипт независимо от локации, но, похоже, это изменилось с предварительной 8-й версии в предварительной 9-й, и я не смог заставить его работать. Вот скриншот 404, показывающий проблему:
  • Если вам удалось загрузить скрипт, вы, вероятно, столкнетесь с теми же проблемами с Blazor hub: скрипты пытаются найти hub в /products/blazor вместо blazor. Чтобы обойти это, вы можете вручную запускать соединение между сервером и браузером:

?

<script src="~/_framework/blazor.server.js" autostart="false"></script><script>  Blazor.start({    configureSignalR: function (builder) {      builder.withUrl('/_blazor');    }  });</script>

Пример кода


Пример кода для этого проекта доступен на GitHub.



Хотите узнать о нашем курсе подробнее? Вам сюда.



Читать ещё:




Подробнее..

Перевод Сжатие ответов в GRPC для ASP.NET CORE 3.0

27.08.2020 12:18:32 | Автор: admin
Перевод статьи подготовлен в преддверии старта курса C# ASP.NET Core разработчик.



В этом эпизоде моей серии статей о gRPC и ASP.NET Core мы рассмотрим подключение функции сжатия ответов (response compression) служб gRPC.

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

Эта статья является частью серии о gRPC и ASP.NET Core.

КОГДА СЛЕДУЕТ ВКЛЮЧАТЬ СЖАТИЕ В GRPC?


Короткий ответ: это зависит от ваших полезных нагрузок (payloads).
Длинный ответ:
gRPC использует protocol buffer в качестве инструмента сериализации сообщений запросов и ответов, отправляемых по сети. Protocol buffer создает двоичный формат сериализации, который по умолчанию предназначен для небольших эффективных полезных нагрузок. По сравнению с обычными полезными нагрузками в формате JSON, protobuf дает более скромный размер сообщений. JSON довольно подробный и удобочитаемый. В результате он включает имена свойств в данные, передаваемые по сети, что увеличивает количество байтов, которые должны быть переданы.

В качестве идентификаторов данных, передаваемых по сети, protocol buffer использует целые числа. Он использует концепцию base 128 variants, которая позволяет полям со значениями от 0 до 127 требовать только один байт для транспортировки. Во многих случаях существует возможность ограничить ваши сообщения полями в этом диапазоне. Для больших целых чисел требуется более одного байта.

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

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

По умолчанию серверная интеграция ASP.NET Core не использует сжатие, но мы можем включить его для всего сервера или определенных служб. Это кажется разумным вариантом по умолчанию, поскольку вы можете отслеживать свои ответы для различных методов с течением времени и оценивать выгоду от их сжатия.

КАК ВКЛЮЧИТЬ СЖАТИЕ ОТВЕТОВ В GRPC?


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

НАСТРОЙКА НА УРОВНЕ СЕРВЕРА


services.AddGrpc(o =>{   o.ResponseCompressionLevel = CompressionLevel.Optimal;   o.ResponseCompressionAlgorithm = "gzip";});

Startup.cs на GitHub

При регистрации сервиса gRPC в контейнере инъекции зависимостей с помощью метода AddGrpc внутри ConfigureServices, у нас есть возможность произвести настройку в GrpcServiceOptions. На этом уровне параметры влияют на все службы gRPC, которые реализует сервер.

Используя перегрузку расширяющего метода AddGrpc, мы можем предоставить Action<GrpcServiceOptions>. В приведенном выше фрагменте кода мы выбрали алгоритм сжатия gzip. Мы также можем установить CompressionLevel, манипулируя временем, которое мы жертвуем на сжатие данных для получения меньшего размера. Если параметр не уазан, текущая реализация по умолчанию использует CompressionLevel.Fastest. В предыдущем фрагменте мы предоставили для сжатия больше времени, чтобы уменьшить количество байт до минимально возможного размера.

НАСТРОЙКА НА УРОВНЕ СЕРВИСА


services.AddGrpc()   .AddServiceOptions<WeatherService>(o =>       {           o.ResponseCompressionLevel = CompressionLevel.Optimal;           o.ResponseCompressionAlgorithm = "gzip";       });

Startup.cs на GitHub

В результате вызова AddGrpc возвращается IGrpcServerBuilder. Мы можем вызвать для билдера расширяющий метод под названием AddServiceOptions, чтобы предоставить параметры для каждой службы отдельно. Этот метод является универсальным и принимает тип службы gRPC, к которой должны применяться параметры.

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

ЗАПРОС ОТ КЛИЕНТА GRPC


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

var channel = GrpcChannel.ForAddress("https://localhost:5005");

Program.cs на GitHub

Созданные таким образом каналы уже отправляют заголовок grpc-accept-encoding, который включает тип сжатия gzip. Сервер считывает этот заголовок и определяет, что клиент разрешает возвращать сжатые ответы.

Один из способов визуализировать эффект сжатия включить логирование для нашего приложения во время разработки. Этого можно сделать, изменив файл appsettings.Development.json следующим образом:

{ "Logging": {   "LogLevel": {       "Default": "Debug",       "System": "Information",       "Grpc": "Trace",       "Microsoft": "Trace"   } }}

appsettings.Development.json на GitHub

При запуске нашего сервера мы получаем гораздо более подробные консольные логи.

info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]     Executing endpoint 'gRPC - /WeatherForecast.WeatherForecasts/GetWeather'dbug: Grpc.AspNetCore.Server.ServerCallHandler[1]     Reading message.dbug: Microsoft.AspNetCore.Server.Kestrel[25]     Connection id "0HLQB6EMBPUIA", Request id "0HLQB6EMBPUIA:00000001": started reading request body.dbug: Microsoft.AspNetCore.Server.Kestrel[26]     Connection id "0HLQB6EMBPUIA", Request id "0HLQB6EMBPUIA:00000001": done reading request body.trce: Grpc.AspNetCore.Server.ServerCallHandler[3]     Deserializing 0 byte message to 'Google.Protobuf.WellKnownTypes.Empty'.trce: Grpc.AspNetCore.Server.ServerCallHandler[4]     Received message.dbug: Grpc.AspNetCore.Server.ServerCallHandler[6]     Sending message.trce: Grpc.AspNetCore.Server.ServerCallHandler[9]     Serialized 'WeatherForecast.WeatherReply' to 2851 byte message.trce: Microsoft.AspNetCore.Server.Kestrel[37]     Connection id "0HLQB6EMBPUIA" sending HEADERS frame for stream ID 1 with length 104 and flags END_HEADERStrce: Grpc.AspNetCore.Server.ServerCallHandler[10]     Compressing message with 'gzip' encoding.trce: Grpc.AspNetCore.Server.ServerCallHandler[7]     Message sent.info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]     Executed endpoint 'gRPC - /WeatherForecast.WeatherForecasts/GetWeather'trce: Microsoft.AspNetCore.Server.Kestrel[37]     Connection id "0HLQB6EMBPUIA" sending DATA frame for stream ID 1 with length 978 and flags NONEtrce: Microsoft.AspNetCore.Server.Kestrel[37]     Connection id "0HLQB6EMBPUIA" sending HEADERS frame for stream ID 1 with length 15 and flags END_STREAM, END_HEADERSinfo: Microsoft.AspNetCore.Hosting.Diagnostics[2]     Request finished in 2158.9035ms 200 application/grpc

Log.txt на GitHub

В 16-й строке этого лога мы видим, что WeatherReply (по сути, массив из 100 элементов WeatherData в данном примере) был сериализован в protobuf и имеет размер 2851 байт.

Позже, в 20-й строке мы видим, что сообщение было сжато с помощью кодировки gzip, а в 26-й строке мы можем увидеть размер фрейма данных для этого вызова, который составляет 978 байт. В данном случае данные были хорошо сжаты (на 66%), поскольку повторяющиеся элементы WeatherData содержат текст, и многие значения в сообщении повторяются.

В этом примере сжатие gzip хорошо повлияло на размер данных.

ОТКЛЮЧЕНИЕ СЖАТИЯ ОТВЕТА В РЕАЛИЗАЦИИ МЕТОДА СЛУЖБ


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

Давайте посмотрим на серверный лог при вызове метода службы, который передает сообщения WeatherData с сервера. Если вы хотите узнать больше о потоковой передаче на сервере, вы можете почитать мою предыдущую статью Потоковая передача данных на сервере с gRPC и .NET Core.

info: WeatherForecast.Grpc.Server.Services.WeatherService[0]     Sending WeatherData responsedbug: Grpc.AspNetCore.Server.ServerCallHandler[6]     Sending message.trce: Grpc.AspNetCore.Server.ServerCallHandler[9]     Serialized 'WeatherForecast.WeatherData' to 30 byte message.trce: Grpc.AspNetCore.Server.ServerCallHandler[10]     Compressing message with 'gzip' encoding.trce: Microsoft.AspNetCore.Server.Kestrel[37]     Connection id "0HLQBMRRH10JQ" sending DATA frame for stream ID 1 with length 50 and flags NONEtrce: Grpc.AspNetCore.Server.ServerCallHandler[7]     Message sent.

Log.txt на GitHub

В 6-й строке мы видим, что отдельное сообщение WeatherData имеет размер 30 байт. В 8-й строке происходит сжатие, а в 10-й мы видим, что длина данных теперь составляет 50 байтов больше, чем исходное сообщение. В этом случае для нас нет никакой выгоды от gzip сжатия, мы видим увеличение общего размера сообщения, отправляемого по сети.

Мы можем отключить сжатие для конкретного сообщения, установив WriteOptions для вызова в методе службы.

public override async Task GetWeatherStream(Empty _, IServerStreamWriter<WeatherData> responseStream, ServerCallContext context){   context.WriteOptions = new WriteOptions(WriteFlags.NoCompress);   // реализация метода, который записывает в поток}

WeatherService.cs на GitHub

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

При потоковой передаче ответов это значение также можно установить в IServerStreamWriter.

public override async Task GetWeatherStream(Empty _, IServerStreamWriter<WeatherData> responseStream, ServerCallContext context){      responseStream.WriteOptions = new WriteOptions(WriteFlags.NoCompress);   // реализация метода записи в поток}

WeatherService.cs на GitHub

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

info: WeatherForecast.Grpc.Server.Services.WeatherService[0]     Sending WeatherData responsedbug: Grpc.AspNetCore.Server.ServerCallHandler[6]     Sending message.trce: Grpc.AspNetCore.Server.ServerCallHandler[9]     Serialized 'WeatherForecast.WeatherData' to 30 byte message.trce: Microsoft.AspNetCore.Server.Kestrel[37]     Connection id "0HLQBMTL1HLM8" sending DATA frame for stream ID 1 with length 35 and flags NONEtrce: Grpc.AspNetCore.Server.ServerCallHandler[7]     Message sent.

Log.txt на GitHub

Теперь 30-байтное сообщение имеет длину 35 байтов в DATA фрейме. Есть небольшие накладные расходы, которые составляют дополнительные 5 байтов, о которых нам здесь не нужно беспокоиться.

ОТКЛЮЧЕНИЕ СЖАТИЯ ОТВЕТА ИЗ КЛИЕНТА GRPC


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

Единственный способ, который я нашел в своем исследовании API на сегодняшний день, это настроить канал, передав экземпляр GrpcChannelOptions. Одно из свойств этого класса предназначено для CompressionProviders IList<ICompressionProvider>. По умолчанию, когда это значение равно null, реализация клиента автоматически добавляет поставщика сжатия Gzip. Это означает, что сервер может использовать gzip для сжатия сообщений ответов, как мы уже видели.

private static async Task Main(){   using var channel = GrpcChannel.ForAddress("https://localhost:5005", new GrpcChannelOptions { CompressionProviders = new List<ICompressionProvider>() });   var client = new WeatherForecastsClient(channel);   var reply = await client.GetWeatherAsync(new Empty());   foreach (var forecast in reply.WeatherData)  {       Console.WriteLine($"{forecast.DateTimeStamp.ToDateTime():s} | {forecast.Summary} | {forecast.TemperatureC} C");   }   Console.WriteLine("Press a key to exit");   Console.ReadKey();}

Program.cs на GitHub
В этом примере клиентского кода мы устанавливаем GrpcChannel и передаем новый экземпляр GrpcChannelOptions. Мы присваиваем свойству CompressionProviders пустой список. Поскольку теперь мы не указываем поставщиков в нашем канале, когда вызовы создаются и отправляются через этот канал, они не будут включать какие-либо кодировки сжатия в заголовок grpc-accept-encoding. Сервер видит это и не применяет gzip сжатие к ответу.

РЕЗЮМЕ


В этом посте мы исследовали возможность сжатия сообщений ответов от сервера gRPC. Мы обнаружили, что в некоторых случаях (но не во всех) это может привести к уменьшению размера полезной нагрузки. Мы видели, что по умолчанию вызовы клиентов включают в заголовки значение gzip grpc-accept-encoding. Если сервер настроен на применение сжатия, он будет делать это только в том случае, если поддерживаемый тип сжатия совпадает с заголовком запроса.

Мы можем настроить GrpcChannelOptions при создании канала для клиента, чтобы отключить gzip сжатие. На сервере мы можем настроить сразу весь сервер или отдельную службу на сжатие ответов. Мы также можем переопределить и отключить это на уровне каждого метода службы.

Чтобы узнать больше о gRPC, вы можете прочитать все статьи, которые являются частью моей серии о gRPC и ASP.NET Core.



ВСЁ О КУРСЕ




Читать ещё:

Подробнее..

Варианты использования конфигурации в ASP.NET Core

08.09.2020 02:17:37 | Автор: admin
Для получения конфигурации приложения обычно используют метод доступа по ключевому слову (ключ-значение). Но это бывает не всегда удобно т.к. иногда требуется использовать готовые объекты в коде с уже установленными значениями, причем с возможностью обновления значений без перезагрузки приложения. В данном примере предлагается шаблон использования конфигурации в качестве промежуточного слоя для ASP.NET Core приложений.

Предварительно рекомендуется ознакомиться с материалом: Metanit Конфигурация, Как работает конфигурация в .NET Core.

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


Необходимо реализовать ASP NET Core приложение с возможностью обновления конфигурации в формате JSON во время работы. Во время обновления конфигурации текущие работающие сессии должны продолжать работать с предыдущим вариантов конфигурации. После обновления конфигурации, используемые объекты должны быть обновлены/заменены новыми. Конфигурация должна быть десериализована, не должно быть прямого доступа к объектам IConfiguration из контроллеров. Считываемые значения должны проходить проверку на корректность, при отсутствии как таковых заменяться значениями по умолчанию. Реализация должна работать в Docker контейнере.

Классическая работа с конфигурацией


GitHub: ConfigurationTemplate_1
Проект основан на шаблоне ASP NET Core MVC. Для работы с файлами конфигурации JSON используется провайдер конфигурации JsonConfigurationProvider. Для добавления возможности перезагрузки конфигурации приложения, во время работы, добавим параметр: reloadOnChange: true.
В файле Startup.cs заменим:
public Startup(IConfiguration configuration) {   Configuration = configuration; }

На
public Startup(IConfiguration configuration) {            var builder = new ConfigurationBuilder()    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);   configuration = builder.Build();   Configuration = configuration;  }

.AddJsonFile добавляет JSON файл, reloadOnChange:true указывает на то, что при изменение параметров файла конфигурации, они будут перезагружены без необходимости перезагружать приложение.
Содержимое файла appsettings.json:
{  "AppSettings": {    "Parameter1": "Parameter1 ABC",    "Parameter2": "Parameter2 ABC"    },  "Logging": {    "LogLevel": {      "Default": "Information",      "Microsoft": "Warning",      "Microsoft.Hosting.Lifetime": "Information"    }  },  "AllowedHosts": "*"}

Контроллеры приложения вместо прямого обращения к конфигурации будут использовать сервис: ServiceABC. ServiceABC класс который первоначальные значения берет из файла конфигурации. В данном примере класс ServiceABC содержит только одно свойство Title.
Содержимое файла ServiceABC.cs:
public class ServiceABC{  public string Title;  public ServiceABC(string title)  {     Title = title;  }  public ServiceABC()  { }}

Для использования ServiceABC необходимо его добавить в качестве сервиса middleware в приложение. Добавим сервис как AddTransient, который создается каждый раз при обращении к нему, с помощью выражения:
services.AddTransient<IYourService>(o => new YourService(param));
Отлично подходит для легких сервисов, не потребляющих память и ресурсы. Чтение параметров конфигурации в Startup.cs осуществляется с помощью IConfiguration, где используется строка запроса с указанием полного пути расположения значения, пример: AppSettings:Parameter1.
В файле Startup.cs добавим:
public void ConfigureServices(IServiceCollection services){  //Считывание параметра "Parameter1" для инициализации сервиса ServiceABC  var settingsParameter1 = Configuration["AppSettings:Parameter1"];  //Добавление сервиса "Parameter1"              services.AddScoped(s=> new ServiceABC(settingsParameter1));  //next  services.AddControllersWithViews();}

Пример использования сервиса ServiceABC в контроллере, значение Parameter1 будет отображаться на html странице.
Для использования сервиса в контроллерах добавим его в конструктор, файл HomeController.cs
public class HomeController : Controller{  private readonly ILogger<HomeController> _logger;  private readonly ServiceABC _serviceABC;  public HomeController(ILogger<HomeController> logger, ServiceABC serviceABC)    {      _logger = logger;      _serviceABC = serviceABC;    }  public IActionResult Index()    {      return View(_serviceABC);    }

Добавим видимость сервиса ServiceABC в файл _ViewImports.cshtml
@using ConfigurationTemplate_1.Services

Изменим Index.cshtml для отображение параметра Parameter1 на странице.
@model ServiceABC@{    ViewData["Title"] = "Home Page";}    <div class="text-center">        <h1>Десериализация конфигурации в ASP.NET Core</h1>        <h4>Классическая работа с конфигурацией</h4>    </div><div>            <p>Сервис ServiceABC, отображение Заголовка        из параметра Parameter1 = @Model.Title</p></div>

Запустим приложение:


Итог


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

Использование IConfiguration как Singleton


GitHub: ConfigurationTemplate_2
Второй вариант заключается в помещение IConfiguration(как Singleton) в сервисы. В результате IConfiguration может вызываться из контроллеров и других сервисов. При использовании AddSingleton сервис создается один раз и при использовании приложения обращение идет к одному и тому же экземпляру. Использовать этот способ нужно особенно осторожно, так как возможны утечки памяти и проблемы с многопоточностью.
Заменим код из предыдущего примера в Startup.cs на новый, где
services.AddSingleton<IConfiguration>(Configuration);
добавляет IConfiguration как Singleton в сервисы.
public void ConfigureServices(IServiceCollection services){  //Доступ к IConfiguration из других контроллеров и сервисов  services.AddSingleton<IConfiguration>(Configuration);  //Добавление сервиса "ServiceABC"                            services.AddScoped<ServiceABC>();  //next  services.AddControllersWithViews();}

Изменим конструктор сервиса ServiceABC для принятия IConfiguration
public class ServiceABC{          private readonly IConfiguration _configuration;  public string Title => _configuration["AppSettings:Parameter1"];          public ServiceABC(IConfiguration Configuration)    {      _configuration = Configuration;    }  public ServiceABC()    { }}

Как и в предыдущем варианте добавим сервис в конструктор и добавим ссылку на пространство имен
Для использования сервиса в контроллерах добавим его в конструктор, файл HomeController.cs
public class HomeController : Controller{  private readonly ILogger<HomeController> _logger;  private readonly ServiceABC _serviceABC;  public HomeController(ILogger<HomeController> logger, ServiceABC serviceABC)    {      _logger = logger;      _serviceABC = serviceABC;    }  public IActionResult Index()    {      return View(_serviceABC);    }

Добавим видимость сервиса ServiceABC в файл _ViewImports.cshtml:
@using ConfigurationTemplate_2.Services;

Изменим Index.cshtml для отображения параметра Parameter1 на странице.
@model ServiceABC@{    ViewData["Title"] = "Home Page";}<div class="text-center">    <h1>Десериализация конфигурации в ASP.NET Core</h1>    <h4>Использование IConfiguration как Singleton</h4></div><div>    <p>        Сервис ServiceABC, отображение Заголовка        из параметра Parameter1 = @Model.Title    </p></div>


Запустим приложение:


Сервис ServiceABC добавленный в контейнер с помощью AddScoped означает, что экземпляр класса будет создаваться при каждом запросе страницы. В результате экземпляр класса ServiceABC будет создаваться при каждом http запросе вместе с перезагрузкой конфигурации IConfiguration, и новые изменения в appsettings.json будут применяться.
Таким образом, если во время работы приложения изменить параметр Parameter1 на NEW!!! Parameter1 ABC, то при следующем обращение к начальной странице отобразится новое значение параметра.

Обновим страницу после изменения файла appsettings.json:


Итог


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

Десериализация конфигурации с валидацией (вариант IOptions)


GitHub: ConfigurationTemplate_3
Почитать про Options по ссылке.
В этом варианте необходимость использования ServiceABC отпадает. Вместо него используется класс AppSettings, который содержит параметры из конфигурационного файла и объект ClientConfig. Объект ClientConfig после изменения конфигурации требуется инициализировать, т.к. в контроллерах используется готовый объект. ClientConfig это некий класс, взаимодействующий с внешними системами, код которого нельзя изменять. Если выполнить только десериализацию данных класса AppSettings, то ClientConfig будет в состояние null. Поэтому необходимо подписаться на событие чтения конфигурации, и в обработчике инициализировать объект ClientConfig.
Для передачи конфигурации не в виде пар ключ-значение, а как объекты определенных классов, будем использовать интерфейс IOptions. Дополнительно IOptions в отличие от ConfigurationManager позволяет десерилизовать отдельные секции. Для создания объекта ClientConfig потребуется использовать IPostConfigureOptions, который выполняется после обработки всех конфигурации. IPostConfigureOptions будет выполняться каждый раз после чтения конфигурации, самым последним.
Создадим ClientConfig.cs:
public class ClientConfig{  private string _parameter1;  private string _parameter2;  public string Value => _parameter1 + " " + _parameter2;  public ClientConfig(ClientConfigOptions configOptions)    {      _parameter1 = configOptions.Parameter1;      _parameter2 = configOptions.Parameter2;    }}

В качестве конструктора будет принимать параметры в виде объекта ClientConfigOptions:
public class ClientConfigOptions{  public string Parameter1;  public string Parameter2;} 

Создадим класс настроек AppSettings, и определим в нем метод ClientConfigBuild(), который создаст объект ClientConfig.
Файл AppSettings.cs:
public class AppSettings{          public string Parameter1 { get; set; }  public string Parameter2 { get; set; }          public ClientConfig clientConfig;  public void ClientConfigBuild()    {      clientConfig = new ClientConfig(new ClientConfigOptions()        {          Parameter1 = this.Parameter1,          Parameter2 = this.Parameter2        }        );      }}

Создадим обработчик конфигурации, который будет отрабатываться последним. Для этого он должен быть унаследован от IPostConfigureOptions. Вызываемый последним метод PostConfigure выполнит ClientConfigBuild(), который как раз и создаст ClientConfig.
Файл ConfigureAppSettingsOptions.cs:
public class ConfigureAppSettingsOptions: IPostConfigureOptions<AppSettings>{  public ConfigureAppSettingsOptions()    { }  public void PostConfigure(string name, AppSettings options)    {                  options.ClientConfigBuild();    }}

Теперь осталось внести изменения только в Startup.cs, изменения коснутся только функции ConfigureServices(IServiceCollection services).
Сначала прочитаем секцию AppSettings в appsettings.json
// configure strongly typed settings objectsvar appSettingsSection = Configuration.GetSection("AppSettings");services.Configure<AppSettings>(appSettingsSection);

Далее, для каждого запроса будет создаваться копия AppSettings для возможности вызова постобработки:
services.AddScoped(sp => sp.GetService<IOptionsSnapshot<AppSettings>>().Value);

Добавим в качестве сервиса, постобработку класса AppSettings:
services.AddSingleton<IPostConfigureOptions<AppSettings>, ConfigureAppSettingsOptions>();

Добавленный код в Startup.cs
public void ConfigureServices(IServiceCollection services){  // configure strongly typed settings objects  var appSettingsSection = Configuration.GetSection("AppSettings");  services.Configure<AppSettings>(appSettingsSection);  services.AddScoped(sp => sp.GetService<IOptionsSnapshot<AppSettings>>().Value);                                      services.AddSingleton<IPostConfigureOptions<AppSettings>, ConfigureAppSettingsOptions>();              //next  services.AddControllersWithViews();}

Для получения доступа к конфигурации, из контроллера достаточно будет просто внедрить AppSettings.
Файл HomeController.cs:
public class HomeController : Controller{  private readonly ILogger<HomeController> _logger;  private readonly AppSettings _appSettings;  public HomeController(ILogger<HomeController> logger, AppSettings appSettings)    {      _logger = logger;      _appSettings = appSettings;    }

Изменим Index.cshtml для вывода параметра Value объекта СlientConfig
@model AppSettings@{    ViewData["Title"] = "Home Page";}<div class="text-center">    <h1>Десериализация конфигурации в ASP.NET Core</h1>    <h4>Десериализация конфигурации с валидацией (вариант IOptions)</h4></div><div>    <p>        Класс ClientConfig, отображение Заголовка         = @Model.clientConfig.Value    </p></div>

Запустим приложение:


Если во время работы приложения изменить параметр Parameter1 на NEW!!! Parameter1 ABC и Parameter2 на NEW!!! Parameter2 ABC, то при следующем обращении к начальной странице отобразится новое свойства Value:



Итог


Данный подход позволяет десериализовать все значения конфигурации без ручного перебирания параметров. Каждый запрос http работает со своим экземпляром AppSettings и СlientConfig, что исключает ситуацию возникновения коллизий. IPostConfigureOptions гарантирует выполнение в последнюю очередь, когда все параметры будут перечитаны. Недостатком решения является постоянное создание экземпляра СlientConfig для каждого запроса, что является непрактичным т.к. по сути СlientConfig должен пересоздаваться только после изменения конфигурации.

Десериализация конфигурации с валидацией (без использования IOptions)


GitHub: ConfigurationTemplate_4
Использование подхода с использованием IPostConfigureOptions приводит к созданию объекта ClientConfig каждый раз при получении запроса от клиента. Это недостаточно рационально т.к. каждый запрос работает с начальным состоянием ClientConfig, которое меняется только при изменение конфигурационного файла appsettings.json. Для этого откажемся от IPostConfigureOptions и создадим обработчик конфигурации который будет вызваться только при изменении appsettings.json, в результате ClientConfig будет создаваться только один раз, и далее на каждый запрос будет отдаваться уже созданный экземпляр ClientConfig.
Создадим класс SingletonAppSettings конфигурации(Singleton) с которого будет создаваться экземпляр настроек для каждого запроса.
Файл SingletonAppSettings.cs:
public class SingletonAppSettings{  public AppSettings appSettings;    private static readonly Lazy<SingletonAppSettings> lazy = new Lazy<SingletonAppSettings>(() => new SingletonAppSettings());  private SingletonAppSettings()    { }  public static SingletonAppSettings Instance => lazy.Value;}

Вернемся в класс Startup и добавим ссылку на интерфейс IServiceCollection. Он будет использоваться в методе обработки конфигурации
public IServiceCollection Services { get; set; }

Изменим ConfigureServices(IServiceCollection services) и передадим ссылку на IServiceCollection:
Файл Startup.cs:
public void ConfigureServices(IServiceCollection services){  Services = services;  //Считаем секцию AppSettings из конфигурации  var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();  appSettings.ClientConfigBuild();

Создадим Singleton конфигурации, и добавим его в коллекцию сервисов:
SingletonAppSettings singletonAppSettings = SingletonAppSettings.Instance;singletonAppSettings.appSettings = appSettings;services.AddSingleton(singletonAppSettings);     

Добавим объект AppSettings как Scoped, при каждом запросе будет создаваться копия от Singleton:
services.AddScoped(sp => sp.GetService<SingletonAppSettings>().appSettings);

Полностью ConfigureServices(IServiceCollection services):
public void ConfigureServices(IServiceCollection services){  Services = services;  //Считаем секцию AppSettings из конфигурации  var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();  appSettings.ClientConfigBuild();  SingletonAppSettings singletonAppSettings = SingletonAppSettings.Instance;  singletonAppSettings.appSettings = appSettings;  services.AddSingleton(singletonAppSettings);               services.AddScoped(sp => sp.GetService<SingletonAppSettings>().appSettings);  //next  services.AddControllersWithViews();}

Теперь добавить обработчик для конфигурации в Configure(IApplicationBuilder app, IWebHostEnvironment env). Для отслеживания изменения в файле appsettings.json используется токен. OnChange вызываемая функция при изменении файла. Обработчик конфигурации onChange():
ChangeToken.OnChange(() => Configuration.GetReloadToken(), onChange);

Вначале читаем файл appsettings.json и десериализуем класс AppSettings. Затем из коллекции сервисов получаем ссылку на Singleton, который хранит объект AppSettings, и заменяем его новым.
private void onChange(){                          var newAppSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();  newAppSettings.ClientConfigBuild();  var serviceAppSettings = Services.BuildServiceProvider().GetService<SingletonAppSettings>();  serviceAppSettings.appSettings = newAppSettings;  Console.WriteLine($"AppSettings has been changed! {DateTime.Now}");}

В контроллер HomeController внедрим ссылку на AppSettings, как в предыдущем варианте (ConfigurationTemplate_3)
Файл HomeController.cs:
public class HomeController : Controller{  private readonly ILogger<HomeController> _logger;  private readonly AppSettings _appSettings;  public HomeController(ILogger<HomeController> logger, AppSettings appSettings)    {      _logger = logger;      _appSettings = appSettings;    }

Изменим Index.cshtml для вывода параметра Value объекта СlientConfig:
@model AppSettings@{    ViewData["Title"] = "Home Page";}<div class="text-center">    <h1>Десериализация конфигурации в ASP.NET Core</h1>    <h4>Десериализация конфигурации с валидацией (без использования IOptions)</h4></div><div>    <p>        Класс ClientConfig, отображение Заголовка        = @Model.clientConfig.Value    </p></div>


Запустим приложение:


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


И новые значения:


Итог


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

Добавление значений по умолчанию и валидация конфигурации


GitHub: ConfigurationTemplate_5
В предыдущих примерах при отсутствии файла appsettings.json приложение выбросит исключение, поэтому сделаем файл конфигурации опциональным и добавим настройки по умолчанию. При публикации приложения проекта, созданного из шаблона в Visula Studio, файл appsettings.json будет располагаться в одной и той же папке вместе со всеми бинарными файлами, что неудобно при развертывание в Docker. Файл appsettings.json перенесем в папку config/:
.AddJsonFile("config/appsettings.json")

Для возможности запуска приложения без appsettings.json изменим параметр optional на true, который в данном случае означает, что наличие appsettings.json является необязательным.
Файл Startup.cs:
public Startup(IConfiguration configuration){  var builder = new ConfigurationBuilder()     .AddJsonFile("config/appsettings.json", optional: true, reloadOnChange: true);  configuration = builder.Build();  Configuration = configuration;}

Добавим в public void ConfigureServices(IServiceCollection services) к строке десериализации конфигурации случай обработки отсутствия файла appsettings.json:
 var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();

Добавим валидацию конфигурации, на основе интерфейса IValidatableObject. При отсутствующих параметрах конфигурации, будет применяться значение по умолчанию. Наследуем класс AppSettings от IValidatableObject и реализуем метод:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)

Файл AppSettings.cs:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext){  List<ValidationResult> errors = new List<ValidationResult>();  if (string.IsNullOrWhiteSpace(this.Parameter1))    {      errors.Add(new ValidationResult("Не указан параметр Parameter1. Задано " +        "значение по умолчанию DefaultParameter1 ABC"));      this.Parameter1 = "DefaultParameter1 ABC";    }    if (string.IsNullOrWhiteSpace(this.Parameter2))    {      errors.Add(new ValidationResult("Не указан параметр Parameter2. Задано " +        "значение по умолчанию DefaultParameter2 ABC"));      this.Parameter2 = "DefaultParameter2 ABC";    }    return errors;}

Добавим метод вызова проверки конфигурации для вызова из класса Startup
Файл Startup.cs:
private void ValidateAppSettings(AppSettings appSettings){  var resultsValidation = new List<ValidationResult>();  var context = new ValidationContext(appSettings);  if (!Validator.TryValidateObject(appSettings, context, resultsValidation, true))    {      resultsValidation.ForEach(        error => Console.WriteLine($"Проверка конфигурации: {error.ErrorMessage}"));      }    }

Добавим вызов метода валидации конфигурации в ConfigureServices(IServiceCollection services). Если файла appsettings.json отсутствует, то требуется инициализировать объект AppSettings со значениями по умолчанию.
Файл Startup.cs:
var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();

Проверка параметров. В случае использования значения по умолчанию в консоль будет выведено сообщение с указанием параметра.
 //Validate            this.ValidateAppSettings(appSettings);            appSettings.ClientConfigBuild();

Изменим проверку конфигурации в onChange()
private void onChange(){                          var newAppSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();  //Validate              this.ValidateAppSettings(newAppSettings);              newAppSettings.ClientConfigBuild();  var serviceAppSettings = Services.BuildServiceProvider().GetService<SingletonAppSettings>();  serviceAppSettings.appSettings = newAppSettings;  Console.WriteLine($"AppSettings has been changed! {DateTime.Now}");}

Если из файла appsettings.json удалить ключ Parameter1, то после сохранения файла в окне консольного приложения появится сообщение об отсутствие параметра:


Итог


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

Все шаблоны конфигураций доступны по ссылке.

Литература:
  1. Корректный ASP.NET Core
  2. METANIT Конфигурация. Основы конфигурации
  3. Singleton Design Pattern C# .net core
  4. Reloading configuration in .NET core
  5. Reloading strongly typed Options on file changes in ASP.NET Core RC2
  6. Конфигурация ASP.NET Core приложения через IOptions
  7. METANIT Передача конфигурации через IOptions
  8. Конфигурация ASP.NET Core приложения через IOptions
  9. METANIT Самовалидация модели
Подробнее..
Категории: C , Net , Net core , Configuration , Asp , Asp.net

Из песочницы Как выбрать инструмент для бизнес-анализа

08.10.2020 18:22:32 | Автор: admin

Какой у Вас выбор?


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

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

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

Приведенные особенности BI-систем заставляют задуматься о подборе альтернативы. Далее я предлагаю сравнить решение стандартного набора задач при подготовке отчетности с помощью Power BI и Excel.

Power BI или Excel?


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

А как решается эта задача с помощью Power BI?

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

Какие преимущества применения Power BI по сравнению с традиционным подходом можно заметить в приведенном примере?

1 Автоматизация процедуры получения данных и подготовка их к анализу.
2 Построение бизнес-модели.
3 Невероятная визуализация.
4 Разграниченный доступ к отчетам.

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

1 Для подготовки данных к построению отчета, нужно единожды определить процедуру, выполняющую подключение к данным и их обработку и каждый раз, когда понадобится получить отчет за другой период, Power BI будет пропускать данные через созданную процедуру. Таким образом автоматизируется большая часть работы по подготовки данных к анализу. Но дело в том, что Power BI осуществляет процедуру подготовки данных с помощью инструмента, который доступен в классической версии Excel, и называется он Power Query. Он позволяет выполнить поставленную задачу в Excel абсолютно тем же способом.

2 Здесь та же ситуация. Инструмент Power BI для построения бизнес-модели имеется и в Excel это Power Pivot.

3 Как Вы, наверное, уже догадались, с визуализацией дело обстоит подобным образом: расширение Excel Power View справляется с этой задачей на ура.

4 Остается разобраться с доступом к отчетам. Тут не так все радужно. Дело в том, что Power BI это облачный сервис, доступ к которому осуществляется через персональную учетную запись. Администратор сервиса распределяет пользователей по группам и задает для этих групп различный уровень доступа к отчетам. Этим достигается разграничение прав доступа между сотрудниками компании. Таким образом, аналитики, менеджеры и директора заходя на одну и туже страницу видят отчет в доступном для них представлении. Может быть ограничен доступ к определенному набору данных, либо к отчету целиком. Однако, если отчет находится в файле формата Excel, то усилиями системного администратора можно попытаться решить задачу с доступом, но это будет уже не то. Я еще вернусь к рассмотрению этой задачи, когда буду описывать особенности корпоративного портала.

Стоит отметить, что, как правило, потребность компании в сложных и красивых дашбордах не велика и часто, для анализа данных в Excel, после построения бизнес-модели не прибегают к возможностям Power View, а пользуются сводными таблицами. Они предоставляют OLAP-функциональность, которой вполне достаточно чтобы решить большинство бизнес-аналитических задач.

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

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

ETL и DWH


В ранее рассматриваемых подходах к построению бизнес-отчетов, загрузка и подготовка данных к анализу осуществлялась с помощью технологии Power Query. Этот способ остается вполне оправданным и эффективным до тех пор, пока источников данных не много: одна учетная система и справочники из Excel-таблиц. Однако, с увеличением числа учетных систем, решение этой задачи посредством Power Query становится очень громоздким, трудным для поддерживания и развития. В таких случаях на помощь приходят инструменты для ETL.

С их помощью осуществляется выгрузка данных из источников (Extract), их преобразование (Transform), что подразумевает очистку и сопоставление, и загрузка в хранилище данных (Load). Хранилище данных (DWH Data Warehouse) это, как правило, реляционная база данных, расположенная на сервере. Эта база содержит данные, пригодные для анализа. По расписанию запускается ETL-процесс, который обновляет данные хранилища до актуальных. Кстати говоря, всю эту кухню прекрасно обслуживает Integration Services, входящие в состав MS SQL Server.

Далее, как и раньше для построения бизнес-модели данных и визуализации можно воспользоваться Excel, Power BI, либо другими аналитическими инструментами, такими как Tableau или Qlik Sense. Но прежде, мне бы хотелось обратить Ваше внимание еще на одну возможность, о которой Вы могли не знать, несмотря на то, что она Вам давно доступна. Речь идет о построении бизнес-моделей с помощью аналитических служб MS SQL Server, а именно Analysis Services.

Модели данных в MS Analysis Services


Этот раздел статьи будет более интересен тем, кто уже использует MS SQL Server в своей компании.

На данный момент службы Analysis Services предоставляют два вида моделей данных это многомерная и табличная модели. Кроме того, что данные в этих моделях связаны, значения показателей модели предварительно агрегируются и хранятся в ячейках OLAP кубов, доступ к которым осуществляется MDX, либо DAX запросами. За счет такой архитектуры хранения данных, запрос, который охватывает миллионы записей, возвращается за секунды. Такой способ доступа к данным необходим компаниям, таблицы транзакций которых содержат от миллиона записей (верхний придел не ограничен).

Excel, Power BI и многие другие солидные инструменты умеют подключаться к таким моделям и визуализировать данные их структур.

Если Вы пошли продвинутым путем: автоматизировали процесс ETL и построили бизнес-модели при помощи служб MS SQL Server, то Вы достойны иметь свой собственный корпоративный портал.

Корпоративный портал


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

Однако, пока не понятно, как будет организовано отображение отчетов на странице портала. Чтобы ответить на этот вопрос, сначала нужно определиться с технологией, на основе которой будет строиться портал. Я предлагаю взять за основу один из фреймворков: ASP.NET MVC/Web Forms/Core, либо Microsoft SharePoint. Если в Вашей компании имеется хотя бы один .NET разработчик, то выбор не составит труда. Теперь можно подбирать встраиваемый в приложение OLAP-клиент, способный подключаться к многомерным или табличным моделям служб Analysis Services.

Выбор OLAP-клиента для визуализации


Сравним несколько инструментов по уровню сложности встраивания, функциональности и цене: Power BI, компоненты Telerik UI for ASP.NET MVC и компоненты RadarCube ASP.NET MVC.

Power BI


Чтобы организовать доступ сотрудников компании к отчетам Power BI на странице своего портала, нужно воспользоваться функцией Power BI Embedded.

Сразу скажу, что потребуется лицензия Power BI Premium и дополнительная выделенная емкость. Наличие выделенной емкости позволяет публиковать дашборды и отчеты для пользователей в организации без необходимости приобретать лицензии для них.

Сначала отчет, сформированный в Power BI Desktop, публикуется на портале Power BI и потом, с помощью не простой настройки, встраивается в страницу web-приложения.

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

Компоненты Telerik и RadarCube


Для встраивания компонентов Telerik и RadarCube достаточно владеть программными технологиями на базовом уровне. Поэтому профессиональных навыков одного программиста из IT-отдела будет вполне достаточно. Все что нужно, это разместить компонент на web-странице и настроить их под свои нужды.

Компонент PivotGrid из набора Telerik UI for ASP.NET MVC встраивается на страницу в изящной манере Razor и предоставляет самые необходимые OLAP-функции. Однако, если требуется более гибкие настройки интерфейса и развитый функционал, то лучше использовать компоненты RadarCube ASP.NET MVC. Большое количество настроек, богатый функционал с возможностями его переопределения и расширения, позволят создать OLAP-отчет любой сложности.

Ниже приведу таблицу сравнения характеристик рассматриваемых инструментов по шкале Низкий-Средний-Высокий.

Power BI Telerik UI for ASP.NET MVC RadarCube ASP.NET MVC
Визуализация Высокий Низкий Средний
Набор OLAP-функций Высокий Низкий Высокий
Гибкость настройки Высокий Высокий Высокий
Возможность переопределения функций - - +
Программная кастомизация - - +
Уровень сложности встраивания и настройки Высокий Низкий Средний
Минимальная стоимость Power BI Premium EM3

190 000 руб./месяц
Лицензия на одного разработчика

90 000 руб.

Лицензия на одного разработчика

25 000 руб.


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

Условия выбора Power BI


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

Условия выбора компонентов Telerik


  • Нужен простой OLAP-клиент для Ad hock анализа.
  • В штате компании имеется .NET разработчик начального уровня.
  • Небольшой бюджет на разовую покупку лицензии и дальнейшее ее продление со скидкой менее 20%.

Условия выбора компонентов RadarCube


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

Заключение


Правильный выбор инструмента для бизнес-аналитики позволит полностью отказаться от формирования отчетности в Excel. Ваша компания сможет постепенно и безболезненно перейти к использованию передовых технологий в области BI и автоматизировать работу аналитиков всех отделов.
Подробнее..

Перевод Локализация в ASP.NET Core Razor Pages Культуры

26.10.2020 20:10:07 | Автор: admin

Привет, хабр! Прямо сейчас OTUS открывает набор на новый поток курса "C# ASP.NET Core разработчик". В связи с этим традиционно делимся с вами полезным переводом и приглашаем записаться на день открытых дверей, в рамках которого можно будет подробно узнать о курсе, а также задать эксперту интересующие вас вопросы.


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

Глобализация в ASP.NET Core

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

Приведенные ниже шаги добавляют базовую локализацию к Razor Pages приложению, которое создается из стандартного шаблона веб-приложения ASP.NET Core 3.0 без настроенной аутентификации. Я назвал свое приложение Localisation. Свое вы можете назвать как вам угодно, но в таком случае не забудьте про пространства имен, если будете копировать код из этой статьи.

1. Начните с открытия файла Startup.cs и добавления туда следующих using директив:

using System.Globalization;using Microsoft.AspNetCore.Localization;using Microsoft.Extensions.Options;

2. Локализация - это дополнительная фича. По умолчанию она не включена. Измените метод ConfigureServices, включив AddLocalization, что сделает различные вспомогательные сервисы локализации доступными для системы инжекции зависимостей. Затем добавьте следующий код для конфигурации RequestLocalizationOptions в приложении.

services.Configure<RequestLocalizationOptions>(options =>{   var supportedCultures = new[]    {        new CultureInfo("en"),        new CultureInfo("de"),        new CultureInfo("fr"),        new CultureInfo("es"),        new CultureInfo("ru"),        new CultureInfo("ja"),        new CultureInfo("ar"),        new CultureInfo("zh"),        new CultureInfo("en-GB")    };    options.DefaultRequestCulture = new RequestCulture("en-GB");    options.SupportedCultures = supportedCultures;    options.SupportedUICultures = supportedCultures;});

Вам необходимо указать языки или культуры, которые вы планируете поддерживать в своем приложении. Культуры представлены в .NET классом CultureInfo, который содержит информацию о форматировании чисел и дат, календарях, системах письма, порядках сортировки и других вопросах, зависящих от местности проживания конечного пользователя. Перегрузка конструктора класса CultureInfo, используемого здесь, принимает строку, представляющую имя языка и региональных параметров (культуры). Допустимые значения - это коды ISO 639-1, которые представляют язык (например, en для английского языка), с необязательным кодом субкультуры ISO 3166, который представляет страну или диалект (например, en-GB для Великобритании или en-ZA для Южной Африки). В приведенном выше примере поддерживается несколько языков, включая одну субкультуру - британский английский, который был установлен в качестве культуры по умолчанию.

3. Теперь, когда RequestLocalizationOptions настроены, их можно применить к промежуточной прослойке локализации запросов, которую необходимо добавить в метод Configure после app.UseRouting():

app.UseHttpsRedirection();app.UseStaticFiles();app.UseRouting();var localizationOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>().Value;app.UseRequestLocalization(localizationOptions);

Установка культуры запроса

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

  • QueryStringRequestCultureProvider, который получает культуру из строки запроса

  • CookieRequestCultureProvider, который получает культуру из файла cookie

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

1. Первый шаг - создать папку с именем Models и добавить в нее файл класса с именем CultureSwitcherModel.cs.

using System.Collections.Generic;using System.Globalization; namespace Localisation.Models{    public class CultureSwitcherModel    {        public CultureInfo CurrentUICulture { get; set; }        public List<CultureInfo> SupportedCultures { get; set; }    }}

2. Добавьте в проект папку с именем ViewComponents и в нее добавьте новый файл класса C# с именем CultureSwitcherViewcomponent.cs. Затем замените содержимое на следующий код:

using Localisation.Models;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Localization;using Microsoft.AspNetCore.Mvc;using Microsoft.Extensions.Options;using System.Linq; namespace Localisation.ViewComponents{    public class CultureSwitcherViewComponent : ViewComponent    {        private readonly IOptions<RequestLocalizationOptions> localizationOptions;        public CultureSwitcherViewComponent(IOptions<RequestLocalizationOptions> localizationOptions) =>            this.localizationOptions = localizationOptions;         public IViewComponentResult Invoke()        {            var cultureFeature = HttpContext.Features.Get<IRequestCultureFeature>();            var model = new CultureSwitcherModel            {                SupportedCultures = localizationOptions.Value.SupportedUICultures.ToList(),                CurrentUICulture = cultureFeature.RequestCulture.UICulture            };            return View(model);        }    }}

3. Добавьте новую папку в папку Pages и назовите ее Components. Внутри добавьте еще одну папку с именем CultureSwitcher. Затем добавьте Razor View в default.cshtml и замените существующее содержимое следующим:

@model CultureSwitcherModel <div>    <form id="culture-switcher">        <select name="culture" id="culture-options">            <option></option>            @foreach (var culture in Model.SupportedCultures)            {                <option value="@culture.Name" selected="@(Model.CurrentUICulture.Name == culture.Name)">@culture.DisplayName</option>            }        </select>    </form></div>  <script>    document.getElementById("culture-options").addEventListener("change", () => {        document.getElementById("culture-switcher").submit();    });</script>

Компонент представления - это простой select элемент, заполненный поддерживаемыми культурами, которые были настроены в Startup. Представление, в котором он находится, использует дефолтный get метод, что означает, что отправленное значение появится в строке запроса с именем culture. QueryStringRequestCultureProvider предназначен для поиска элемента в строке запроса по ключа culture (и/или ui-culture).

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

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

@using Localisation@using Localisation.Models@using System.Globalization@namespace Localisation.Pages@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers@addTagHelper *, Localisation

5. Включите компонент переключателя культуры на страницу макета, используя подход с tag-хелпером, как показано здесь в последней строке.

<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">    <ul class="navbar-nav flex-grow-1">        <li class="nav-item">            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>        </li>        <li class="nav-item">            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>        </li>    </ul></div><vc:culture-switcher/>

6. Измените Index.cshtml, включив код в блок кода и HTML-код для таблицы, которая отображает различные биты данных:

@page@using Microsoft.AspNetCore.Localization@model IndexModel@{    ViewData["Title"] = "Home page";    var requestCultureFeature = HttpContext.Features.Get<IRequestCultureFeature>();    var requestCulture = requestCultureFeature.RequestCulture;} <div class="text-center">    <h1 class="display-4">Welcome</h1>    <p>Learn about <a href="http://personeltest.ru/aways/docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>     <table class="table culture-table">        <tr>            <td style="width:50%;">Culture</td>            <td>@requestCulture.Culture.DisplayName {@requestCulture.Culture.Name}</td>        </tr>        <tr>            <td>UI Culture</td>            <td>@requestCulture.UICulture.Name</td>        </tr>        <tr>            <td>UICulture Parent</td>            <td>@requestCulture.UICulture.Parent</td>        </tr>        <tr>            <td>Date</td>            <td>@DateTime.Now.ToLongDateString()</td>        </tr>        <tr>            <td>Currency</td>            <td>                @(12345.00.ToString("c"))            </td>        </tr>        <tr>            <td>Number</td>            <td>                @(123.45m.ToString("F2"))            </td>        </tr>    </table></div>

При первом запуске приложения культура для запроса задается AcceptHeadersCultureRequestProvider. Когда вы используете раскрывающийся список для выбора разных культур, культура задается QueryStringCultureRequestProvider. Попробуйте добавить ключ ui-culture в строку запроса с другим значением ключа culture (например, https://localhost:xxxxx/?culture=es&ui-culture=de), чтобы посмотреть, что произойдет.

Резюме

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

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


Подробнее о курсе.


Читать ещё:

Подробнее..

Как создать простое Rest API на .NET Core

03.12.2020 12:07:50 | Автор: admin

Введение

Всем привет, в данной статье будет рассказано, как с использованием технологии C# ASP.NET Core написать простое Rest Api. Сделать Unit-тесты на слои приложений. Отправлять Json ответы. Также покажу, как выложить данное приложение в Docker.

В данной статье не будет описано, как делать клиентскую (далее Front) часть приложения. Здесь я покажу только серверную (далее Back).

Что используем?

Писать код я буду в Visual Studio 2019.

Для реализации приложения, я буду использовать такие библиотеки NuGet:

  1. Microsoft.EntityFrameworkCore

  2. Microsoft.EntityFrameworkCore.SqlServer

  3. Microsoft.EntityFrameworkCore.Tools

Для тестов вот эти библиотеки:

  1. Microsoft.NET.Test.Sdk

  2. Microsoft.NETCore.App

  3. Moq

  4. xunit

  5. xunit.runner.visualstudio

Для установки пакетов нужно зайти в обозреватель пакетов NuGet, сделать это можно, нажав ПКМ по проекту, и выбрав там пункт управление пакетам NuGet

Что программировать?

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

Настройка Базы Данных

Для настройки базы данных нужен класс ApplicationContext (реализация будет далее) и строка подключения, которая храниться в файле appsettings.json. В этом классе будут прописаны все зависимости для генерации миграций. Строка подключения нужна для того, чтобы приложение знало в какую БД ей обращаться и с какими параметрами.

Чтобы добавить строку подключения, достаточно зайти в файл appsettings.json и прописать следующие строки:

"ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=testdb;Trusted_Connection=True;" },

Описание слоев приложения

Модели

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

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

Первая модель, которая понадобиться для описания сервиса по ремонту - модель сотрудника. Что она будет из себя представлять?

  • Уникальный идентификатор сотрудника

  • Имя сотрудника

  • Должность сотрудника

  • Номер телефона для связи с сотрудником

Следующая модель для описания сервиса - автомобили, которые будут поступать на ремонт.

  • Уникальный идентификатор автомобиля

  • Название автомобиля

  • Номер автомобиля

И последняя модель, которую мы уже будем отсылать - документ (выписка) по ремонту.

  • Уникальный идентификатор документа

  • Сотрудник, который обслуживал автомобиль

  • Автомобиль, который был на ремонте

Чтобы модели попали в базу данных, необходимо создать миграцию. Миграция - описание того, как и что будет записано в базу данных. С помощью Entity Framework миграции можно генерировать автоматически. Для этого в пакетном менеджере надо прописать команду "Add-Migration". После этого Entity Framework сгенерирует миграцию по вашим моделям, которые указаны в классе DbContext. Чтобы применить миграцию, используем команду "Update-Database", после этого ваши данные попадут в базу данных (как это применять будет описано далее).

Контроллеры

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

Для возвращаемого значения в контроллерах будут использоваться тип Json. Для этого достаточно в return прописать

new JsonResult(Ваш объект)

В данном примере, я покажу как сделать методы для GET, POST, PUT и DELETE запросов. В GET-запросе я буду выбирать все существующие документы и передавать их на Front, а в POST-запросе я буду вызывать сервис по ремонту автомобиля и возвращать выписку по ремонту, PUT будет отвечать за обновление существующего документа и DELETE за удаление документа.

DAO (Репозитории)

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

В своем приложении я сделал репозиторий, который может принимать любую модель, и выполнять такие действия как get, get all, update, create, delete.

Сервисы

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

В качестве примера сервиса, я сделал класс, всего с одним методом Work. Этот метод имитирует работу моего сервиса по починке машин. В этом методе нанимается рабочий, заводится автомобиль и заполняется документ о его починке.

Реализация

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

Создание проекта

При создании нового проекта, я выбрал веб-приложение ASP.NET Core, далее прописал его название (RestApi) и выбрал папку, где оно будет храниться. На экране выбора шаблона выбрал API.

Выбор шаблона приложенияВыбор шаблона приложения

Далее приступим к самому приложению.

Структура

Я разделил все приложение по папкам (также Unit-тесты в отдельном проекте) и получил вот такую структуру мое приложения:

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

Модели

Для реализации моделей я сделал абстрактный класс BaseModel. Он понадобиться в будущем для корректного наследования, а также в нем прописан Id каждой, модели (это помогает не дублировать код):

 public abstract class BaseModel { public Guid Id { get; set; } }

Далее вышеописанные модели:

 public class Car : BaseModel { public string Name { get; set; } public string Number { get; set; } }
 public class Document : BaseModel { public Guid CarId { get; set; } public Guid WorkerId { get; set; } public virtual Car Car { get; set; } public virtual Worker Worker { get; set; } }
 public class Worker : BaseModel { public string Name { get; set; } public string Position { get; set; } public string Telephone { get; set; } }

Репозиторий

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

Интерфейс:

public interface IBaseRepository<TDbModel> where TDbModel : BaseModel    {        public List<TDbModel> GetAll();        public TDbModel Get(Guid id);        public TDbModel Create(TDbModel model);        public TDbModel Update(TDbModel model);        public void Delete(Guid id);    }

Реализация:

    public class BaseRepository<TDbModel> : IBaseRepository<TDbModel> where TDbModel : BaseModel    {        private ApplicationContext Context { get; set; }        public BaseRepository(ApplicationContext context)        {            Context = context;        }        public TDbModel Create(TDbModel model)        {            Context.Set<TDbModel>().Add(model);            Context.SaveChanges();            return model;        }        public void Delete(Guid id)        {            var toDelete = Context.Set<TDbModel>().FirstOrDefault(m => m.Id == id);            Context.Set<TDbModel>().Remove(toDelete);            Context.SaveChanges();        }        public List<TDbModel> GetAll()        {            return Context.Set<TDbModel>().ToList();        }        public TDbModel Update(TDbModel model)        {            var toUpdate = Context.Set<TDbModel>().FirstOrDefault(m => m.Id == model.Id);            if (toUpdate != null)            {                toUpdate = model;            }            Context.Update(toUpdate);            Context.SaveChanges();            return toUpdate;        }        public TDbModel Get(Guid id)        {            return Context.Set<TDbModel>().FirstOrDefault(m => m.Id == id);        }    }

Сервис

Сервис также как и репозиторий имеет интерфейс и его реализацию.

Интерфейс:

public interface IRepairService    {        public void Work();    }

Реализация:

public class RepairService : IRepairService    {        private IBaseRepository<Document> Documents { get; set; }        private IBaseRepository<Car> Cars { get; set; }        private IBaseRepository<Worker> Workers { get; set; }        public void Work()        {            var rand = new Random();            var carId = Guid.NewGuid();            var workerId = Guid.NewGuid();            Cars.Create(new Car            {                Id = carId,                Name = String.Format($"Car{rand.Next()}"),                Number = String.Format($"{rand.Next()}")            });            Workers.Create(new Worker            {                Id = workerId,                Name = String.Format($"Worker{rand.Next()}"),                Position = String.Format($"Position{rand.Next()}"),                Telephone = String.Format($"8916{rand.Next()}{rand.Next()}{rand.Next()}{rand.Next()}{rand.Next()}{rand.Next()}{rand.Next()}")            });            var car = Cars.Get(carId);            var worker = Workers.Get(workerId);            Documents.Create(new Document {                CarId = car.Id,                WorkerId = worker.Id,                Car = car,                Worker = worker            });        }    }

Контроллер

У меня в приложении всего один контроллер, но по его шаблону можно сделать сколько угодно контроллеров. Когда приложение запущено, для того чтобы обратиться к методу контроллера с Front части приложения, достаточно передать запрос, который выглядит примерно вот так:

ДоменноеИмя/НазваниеКонтроллера/НазваниеМетода?Параметры(если есть)

Пути гибко настраиваются с помощью специальных атрибутов (о них не в этой статье).

Мой MainController:

[ApiController]    [Route("[controller]")]    public class MainController : ControllerBase    {        private IRepairService RepairService { get; set; }        private IBaseRepository<Document> Documents { get; set; }        public MainController(IRepairService repairService, IBaseRepository<Document> document )        {            RepairService = repairService;            Documents = document;        }        [HttpGet]        public JsonResult Get()        {            return new JsonResult(Documents.GetAll());        }        [HttpPost]        public JsonResult Post()        {            RepairService.Work();            return new JsonResult("Work was successfully done");        }        [HttpPut]        public JsonResult Put(Document doc)        {            bool success = true;            var document = Documents.Get(doc.Id);            try            {                if (document != null)                {                    document = Documents.Update(doc);                }                else                {                    success = false;                }            }            catch (Exception)            {                success = false;            }            return success ? new JsonResult($"Update successful {document.Id}") : new JsonResult("Update was not successful");        }        [HttpDelete]        public JsonResult Delete(Guid id)        {            bool success = true;            var document = Documents.Get(id);            try            {                if (document != null)                {                    Documents.Delete(document.Id);                }                else                {                    success = false;                }            }            catch (Exception)            {                success = false;            }            return success ? new JsonResult("Delete successful") : new JsonResult("Delete was not successful");        }    }

Application Context

ApplicationContext класс, который унаследован от класса DbContext. В нем прописываются все DbSet. С их помощью приложение знает, какие модели должны быть в базе данных, а какие нет.

public class ApplicationContext: DbContext    {        public DbSet<Car> Cars { get; set; }        public DbSet<Document> Documents { get; set; }        public DbSet<Worker> Workers { get; set; }        public ApplicationContext(DbContextOptions<ApplicationContext> options): base(options)        {            Database.EnsureCreated();        }    }

Настройка зависимостей и инжектирования

А теперь немного про инжектирование. Правильная настройка зависимостей проекта Asp.net core позволяет упростить его работу и избежать лишнего написания кода. Все зависимости прописываются в файле Startup.cs.

Что я связывал? Я связывал интерфейс репозитория с репозиторием каждой модели (далее будет видно, что имеется ввиду), также я связал интерфейс сервиса с его реализацией.

Также в этом же файле прописываются настройки для базы данных. Помните про строку подключения из начала статьи? Так вот сейчас мы ее и используем для настройки БД.

Вот как выглядит мой файл Startup.cs:

public Startup(IConfiguration configuration)        {            Configuration = configuration;        }        public IConfiguration Configuration { get; }        // This method gets called by the runtime. Use this method to add services to the container.        public void ConfigureServices(IServiceCollection services)        {            string connection = Configuration.GetConnectionString("DefaultConnection");            services.AddMvc();            services.AddDbContext<ApplicationContext>(options =>                options.UseSqlServer(connection));            services.AddTransient<IRepairService, RepairService>();            services.AddTransient<IBaseRepository<Document>, BaseRepository<Document>>();            services.AddTransient<IBaseRepository<Car>, BaseRepository<Car>>();            services.AddTransient<IBaseRepository<Worker>, BaseRepository<Worker>>();        }        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)        {            if (env.IsDevelopment())            {                app.UseDeveloperExceptionPage();            }            app.UseHttpsRedirection();            app.UseRouting();            app.UseAuthorization();            app.UseEndpoints(endpoints =>            {                endpoints.MapControllers();            });        }

Не забудьте создать БД перед запуском приложения. Для этого в Консоле диспетчера пакетов нужно прописать следующие команды:

Add-Migration init (или любое другое имя)

Update-Database

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

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

Здесь я покажу как создать UNIT-тесты для контроллера и сервиса. Для тестов я сделал отдельный проект (библиотека классов .Net Core).

Тест для контроллера

public class MainControllerTests    {        [Fact]        public void GetDataMessage()        {            var mockDocs = new Mock<IBaseRepository<Document>>();            var mockService = new Mock<IRepairService>();            var document = GetDoc();            mockDocs.Setup(x => x.GetAll()).Returns(new List<Document> { document });            // Arrange            MainController controller = new MainController(mockService.Object, mockDocs.Object);            // Act            JsonResult result = controller.Get() as JsonResult;            // Assert            Assert.Equal(new List<Document> { document }, result?.Value);        }        [Fact]        public void GetNotNull()        {            var mockDocs = new Mock<IBaseRepository<Document>>();            var mockService = new Mock<IRepairService>();            mockDocs.Setup(x => x.Create(GetDoc())).Returns(GetDoc());            // Arrange            MainController controller = new MainController(mockService.Object, mockDocs.Object);            // Act            JsonResult result = controller.Get() as JsonResult;            // Assert            Assert.NotNull(result);        }        [Fact]        public void PostDataMessage()        {            var mockDocs = new Mock<IBaseRepository<Document>>();            var mockService = new Mock<IRepairService>();            mockDocs.Setup(x => x.Create(GetDoc())).Returns(GetDoc());            // Arrange            MainController controller = new MainController(mockService.Object, mockDocs.Object);            // Act            JsonResult result = controller.Post() as JsonResult;            // Assert            Assert.Equal("Work was successfully done", result?.Value);        }        [Fact]        public void UpdateDataMessage()        {            var mockDocs = new Mock<IBaseRepository<Document>>();            var mockService = new Mock<IRepairService>();            var document = GetDoc();            mockDocs.Setup(x => x.Get(document.Id)).Returns(document);            mockDocs.Setup(x => x.Update(document)).Returns(document);            // Arrange            MainController controller = new MainController(mockService.Object, mockDocs.Object);            // Act            JsonResult result = controller.Put(document) as JsonResult;            // Assert            Assert.Equal($"Update successful {document.Id}", result?.Value);        }        [Fact]        public void DeleteDataMessage()        {            var mockDocs = new Mock<IBaseRepository<Document>>();            var mockService = new Mock<IRepairService>();            var doc = GetDoc();            mockDocs.Setup(x => x.Get(doc.Id)).Returns(doc);            mockDocs.Setup(x => x.Delete(doc.Id));            // Arrange            MainController controller = new MainController(mockService.Object, mockDocs.Object);            // Act            JsonResult result = controller.Delete(doc.Id) as JsonResult;            // Assert            Assert.Equal("Delete successful", result?.Value);        }        public Document GetDoc()        {            var mockCars = new Mock<IBaseRepository<Car>>();            var mockWorkers = new Mock<IBaseRepository<Worker>>();            var carId = Guid.NewGuid();            var workerId = Guid.NewGuid();            mockCars.Setup(x => x.Create(new Car()            {                Id = carId,                Name = "car",                Number = "123"            }));            mockWorkers.Setup(x => x.Create(new Worker()            {                Id = workerId,                Name = "worker",                Position = "manager",                Telephone = "89165555555"            }));            return new Document            {                Id = Guid.NewGuid(),                CarId = carId,                WorkerId = workerId            };        }    }

В данных тестах проверяется работа каждого метода контроллера на их корректное выполнение.

Тест для сервиса

public class RepairServiceTests    {        [Fact]        public void WorkSuccessTest()        {            var serviceMock = new Mock<IRepairService>();            var mockCars = new Mock<IBaseRepository<Car>>();            var mockWorkers = new Mock<IBaseRepository<Worker>>();            var mockDocs = new Mock<IBaseRepository<Document>>();            var car = CreateCar(Guid.NewGuid());            var worker = CreateWorker(Guid.NewGuid());            var doc = CreateDoc(Guid.NewGuid(), worker.Id, car.Id);            mockCars.Setup(x => x.Create(car)).Returns(car);            mockDocs.Setup(x => x.Create(doc)).Returns(doc);            mockWorkers.Setup(x => x.Create(worker)).Returns(worker);            serviceMock.Object.Work();            serviceMock.Verify(x => x.Work());        }        private Car CreateCar(Guid carId)        {            return new Car()            {                Id = carId,                Name = "car",                Number = "123"            };        }        private Worker CreateWorker(Guid workerId)        {            return new Worker()            {                Id = workerId,                Name = "worker",                Position = "manager",                Telephone = "89165555555"            };        }        private Document CreateDoc(Guid docId, Guid workerId, Guid carId)        {            return new Document            {                Id = docId,                CarId = carId,                WorkerId = workerId            };        }    }

В тесте для сервиса есть всего один тест для метода Work. Тут проверяется отработал этот метод или нет.

Запуск тестов

Чтобы запустить тесты достаточно зайти во вкладку Тест и нажать выполнить все тесты.

Выкладываем в Docker

В финале я покажу, как выложить данное приложение в Docker Hub. В Visual Studio 2019 это сделать крайне просто. Учтите, что у вас уже должен быть профиль в Docker и создан репозиторий в Docker Hub.

Нажимаете ПКМ на ваш проект и выбираете пункт опубликовать.

Там выбираем Docker Container Registry

На следующем окне, надо выбрать Docker Hub

Далее введите свои учетные данный Docker.

Если все прошло успешно, то осталось сделать последнюю вещь, нажать кнопку Опубликовать.

Готово, вы опубликовали свое приложение в Docker Hub!

Заключение

В данной статье я показал, как использовать возможности C# ASP.NET Core для создания простого Rest API. Показал, как создавать модели, записывать их в БД, как создать свой репозиторий, как использовать сервисы и как создавать контроллеры, которые будут отправлять JSON ответы на ваш Front. Также показал, как сделать Unit-тесты для слоев контроллеров и сервисов. И в финале показал, как выложить приложение в Docker.

Надеюсь, что данная статья будет вам полезна!

Подробнее..
Категории: C , Net , Asp.net core , Asp , Rest api

Как готовить Cake, используя только Frosting

26.12.2020 10:13:54 | Автор: admin

Итак, Cake. Многие слышали, многие хотели попробовать, но откладывали. Конечно, если ты все время работал на TeamCity или на Jenkins и продолжаешь, то зачем переизобретать то, что уже отлично работает? Люби свою жизнь и радуйся. Но вот, допустим, в твоей любимой жизни появился новый проект, новый дедлайн, минимум сторипойнтов до релиза, а опыта с новым сборщиком нет? Мне в этом случае и пригодился Cake.

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

На что похож Cake? Наверное, любой разработчик, не погрязший в мире .Net, найдет свою аналогию: gradle, gulp, golang make. Make-системы не откровение в 2020 году. Это всегда было удобно, а значит нужно и правильно. Мир .Net долгое время был обделен такими средствами. Фактически был и есть до сих пор MSBuild, но у него есть очень-очень много недостатков. Основной - кто вообще умеет им пользоваться из рядовых разработчиков? И какова целесообразность его освоения? Какие-то базовые и нужные всем вещи явно проще делать на билд-сервере. Наверное, кому-то он и удобен, но я уверен, что значимая часть коммьюнити предпочтет MSBuild'у освоить новый билд-сервер. Один раз написать конфиг и забыть как страшный сон.

А что если бы существовала make-система с DSL на C#, автокомплитом и прочими фишками типизированного языка? Да, я про Cake. В частности сейчас пойдет разговор про библиотеку Cake.Frosting, являющуюся одним из раннеров make-системы.

Подробней про доступные раннеры можно прочитать тут: Cake Runners

С Frosting все привычно самодокументирующийся Api с которым почти сразу находишь общий язык. Методы расширения, загружаемые из Nuget на любой случай жизни, структура проекта, похожая на смесь тестов или бенчмарков и хоста Asp. Все решения угадываются сразу, все как дома.

Frosting от остальных раннеров Cake отличается тем, что существует не в виде тулза, а в виде отдельного проекта, который можно докинуть в solution и хранить вместе с ним в репозитории. Это невероятно упрощает работу с системой. Фактически стоит просто создать новый проект, подключить к нему зависимость Cake.Frosting, сконфигурировать Build-хост и можно запускать этот проект командой.

dotnet run

Чтобы нам стало еще проще, существует темплейт проекта. К нему в комплект даже идут шелл-скрипты для Mac OS, Linux и Windows, подгружающие SDK, если его нет в окружении. Через них стоит вызывать сборку вместо dotnet CLI, если в этом есть необходимость.

Тут можно почитать подробнее об этом: Frosting Bootstraping

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

Вторая интересующая нас часть проекта папка Tasks. Тут хранятся все шаги сборки в виде классов -наследников от FrostingTask<Context>.

Задачи автоматически регистрируются в IoC контейнере, как мы привыкли в Asp. Более того, Frosting реализует точно такой же паттерн с DI через IServiceCollection, к которому мы все привыкли.

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

[Dependency(typeof(MyPreviousTask))]

Где MyPreviousTask это задача, которая должна завершиться ранее помеченной.


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

  1. Восстановление пакетов.

  2. Билд.

  3. Прогон unit-тестов.

  4. Publish.

  5. Поставка артефактов.

В качестве поставки артефактов мы можем делать как привычную нам архивацию и отправку в среду исполнения приложения, так и упаковку приложения в образ docker, словом все, что мы можем написать на C#.

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

Единственный минус такого похода захламление IntelliSense окна чудовищным количеством методов, но когда это нас останавливало?

По случаю хотелось бы напомнить про относительно свежую фичу .Net core self-contained приложения. В этом способе публикации надо явно задать версию рантайма, в результате чего формируется не библиотека, исполняемая в контексте dotnet, а запускаемое приложение, содержащее рантайм, так сказать, в себе. Она может пригодиться при упаковке в образ без установленного рантайма, если по каким-то причинам установить последний нельзя. Нет никаких причин не делать этого в Cake.

Когда все готово, настроено и залито в репозиторий, мы делаем в TS или Jenkins всего одну команду

dotnet run ./Build/Build.csproj

Путь до проекта у вас будет свой (Ваш Кэп) и смотрим, как происходит медитативный процесс сборки. Frosting пишет события сборки в стандартный вывод, который читает билд-сервер, так что никакие данные не пропадут.

Конечно, это минимальный и самый простой сценарий использования системы. Билд-сервер может передать в dotnet аргументы при вызове билда Frosting. Например, чтобы в дальнейшем установить версию сборки или образа или тип рантайма или папки назначения для публикации.

В общем полный простор фантазии.

Мотивация

  1. Это удобно. Вы пишете на своем основном языке и не зависите от выразительности скриптов и настроек/плагинов билд-сервера;

  2. Это мобильно. Вы заливаете код в репозиторий и он универсально запускается на любом билд-сервере. И никакого вендор-лока.

  3. Это версионно. Код сборки хранится в репозитории. Вместе с самим релизом.

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

  5. Это легко. IntelliSense, автокомплит, разберется даже обленившийся senior.

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

Бонусом пример использования Cake.Frosting на github. Для затравки так сказать: Link

Ссылка на сайт проекта Cake

Подробнее..

Простое и удобное журналирование ошибок для сайтов на .NET Core

29.12.2020 10:06:55 | Автор: admin

Возможно, многим знакома библиотека ELMAH (Error Logging Modules and Handlers), которая позволяет организовать простое журналирование ошибок для любого сайта, созданного с помощью .NET Framework.



Этот простой и проверенный временем инструмент выручал меня во многим проектах.
Несколько лет назад, создавая свой новый проект под .NET Core я с досадой обнаружил, что ELMAH не работает под .NET Core.


Но это же opensource проект! Несколько выходных в работе над форком, и вот готова первая версия ELMAH работающая под .NET Core.


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


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


  • Тип и информация об исключении, стек вызова
  • Информация об HTTP запросе: данные шапки запроса (header), параметры запроса, cookies, данные о подключении пользователя
  • Информация о текущем пользователе
  • Информация о текущей сессии на сервере
  • Переменные среды сервера

В новой версии я добавил регистрацию всех сообщений, созданных через Microsoft.Extensions.Logging (ILogger) в привязке к контексту HTTP запроса.
Вся эта информация может быть сохранена:


  • в памяти сервера
  • в XML файлах в папке на сервере
  • в СУБД, сейчас поддерживаются MSSQL, MySQL, PostgreSQL

Подключение данной библиотеки максимально простое:


  1. Добавить в проект nuget-пакет elmahcore.
  2. Добавить следующие строчки в Startup.cs:

services.AddElmah(); // в метод ConfigureServices app.UseElmah(); // в начале метода Configure

Для доступа к журналу ошибок библиотека предоставляет программный и пользовательский интерфейс.
Интерфейс пользователя, по умолчанию, доступен по пути ~/elmah.
В новой версии я существенно переработал UI, реализовав его на VUE.js



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


services.AddElmah(options =>{   options.SourcePaths = new []   {      @"D:\tmp\ElmahCore.DemoCore3",      @"D:\tmp\ElmahCore.Mvc",      @"D:\tmp\ElmahCore"   };});

На закладке Log отображается журнал сообщений Microsoft.Extensions.Logging в контексте HTTP запроса в котором была зарегистрирована ошибка.



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


services.AddElmah(options =>{        options.OnPermissionCheck = context => context.User.Identity.IsAuthenticated;});

При этом вызов UseElmah, должен быть позже UseAuthentication и UseAuthorization


app.UseAuthentication();app.UseAuthorization();app.UseElmah();

Можно организовать фильтрацию регистрируемых ошибок с помощь фильтров, реализованных в коде (реализующих интерфейс IErrorFilter) или в xml-файле конфигурации (https://elmah.github.io/a/error-filtering/examples/).


services.AddElmah<XmlFileErrorLog>(options =>{    options.FiltersConfig = "elmah.xml";    options.Filters.Add(new MyFilter());})

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


services.AddElmah<XmlFileErrorLog>(options =>{    options.Notifiers.Add(new ErrorMailNotifier("Email",emailOptions));});

Надеюсь, что эта бесплатная библиотека будет полезна в ваших проектах.
Подробнее с библиотекой можно познакомиться здесь: https://github.com/ElmahCore/ElmahCore

Подробнее..
Категории: C , Open source , Net , Net core , Asp , Asp.net , Error handling , Error_reporting

6 вещей, которые не стоит делать в ASP.NET контроллерах

26.04.2021 02:09:13 | Автор: admin

ASP.NET контроллеры должны быть тонкими

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

Почему они должны быть тонкими? Какой в этом плюс? Как сделать их тонкими, если они сейчас не такие? Как сохранить их тонкими?

Правильные (и частые) вопросы. Обсуждение части этих вопросов можно найти в моих ранних статьях, поэтому сейчас мы посмотрим на проблему с другой стороны.

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

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

1. Маппинг объектов передачи данных (DTO)

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

Вы понимаете, это что-то вроде:

public IActionResult CheckOutBook([FromBody]BookRequest bookRequest){    var book = new Book();    book.Title = bookRequest.Title;    book.Rating = bookRequest.Rating.ToString();    book.AuthorID = bookRequest.AuthorID;    //...}

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

2. Валидация

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

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

Вот такому коду нет оправданий!

public IActionResult Register([FromBody]AutomobileRegistrationRequest request){    // Проверяем, что VIN номер был заполнен...    if (string.IsNullOrEmpty(request.VIN))    {        return BadRequest();    }        //...}

3. Бизнес-логика

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

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

4. Авторизация

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

Аналогично в случае с валидацией, ASP.NET предлагает множество путей для выноса авторизации (ПО промежуточного слоя и фильтры, например).

Если вы проверяете свойства объекта Userвнутри контроллера, чтобы разрешить/запретить ему что-то, то, кажется, что у вас есть кое-что для рефакторинга.

5. Обработка ошибок

Это больно, это БОЛЬНО!

public IActionResult GetBookById(int id){    try    {      // Важный код, который должен выполнять шеф-повар...    }    catch (DoesNotExistException)    {      // Код, который должен выполнять ассистент...    }    catch (Exception e)    {      // Пожалуйста, только не это...    }}

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

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

6. Сохранение/получение данных

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

У меня даже есть статья, показывающая использование контроллеров таким образом.

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

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

public IActionResult CheckOutBook(BookRequest request){    var book = _bookRepository.GetBookByTitleAndAuthor(request.Title, request.Author);    // Если у вас уже есть логика получения книги, то вы скорее    // всего захотите добавить сюда и логику выдачи этой книги  // ...return Ok(book);}

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

Именно здесь мне нравится использовать некий сервис (спрятанный за интерфейсом) для обработки запроса или делегирования какому-нибудь CQRS объекту.

На этом всё!


Статья закончилась, а у вас есть ещё примеры, которые не были освещены? Не согласны с каким-то из пунктов? Или просто хотите задать вопрос? Добро пожаловать в комментарии!

Перевод статьи подготовлен в преддверии старта курса "C# ASP.NET Core разработчик".

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

Ссылка для записи на вебинар

Подробнее..

Партнерские отношения, каналы распространения и продажи продукта Курс Создание программного продукта и управление его

22.04.2021 14:23:53 | Автор: admin
Привет, Хабр! Сегодня в продолжение темы продакт-менеджмента мы поговорим о выстраивании партнерских отношений, поиске каналов и, конечно, о святая святых любого бизнеса о продажах. В этом посте вы найдете информацию о работе с дистрибьюторами, выстраивании стратегии продаж, примеры win-loss анализа и многое другое. Так что всех заинтересованных прошу под кат!



Оглавление курса


1. Роль менеджера по продукту и фреймворк
2. Сегментирование рынка и конкурентный анализ
3. Пользовательские персоны
4. Проверка гипотез
5. Позиционирование продукта
6. Дорожная карта продукта
7. Составление требований для разработки
8. Бизнес-модель и Бизнес-план
9. Финансовый план и ценообразование
<a href=habr.com/ru/company/acronis/blog/522124>10. Запуск ИТ-продукта и проведение маркетинговой кампании
<a href=???>11 Партнерства. Каналы дистрибуции. Продажи < Вы здесь
Продолжение следует

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



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

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



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



В вопросах продвижения продукта можно выделить 4 основных направления. Именно так сделал Эдмунд МакКарти в своей книге 1964 года (да-да, с тех пор почти ничего не изменилось). Он сформулировал концепцию четырех P, которые позволяют добиться цели то есть заработать на своем продукте.



Product необходимо наличие самого продукта
Price нужно определить правильную цену
Promotion требуется рассказать о продукте людям
Place нужно организовать место (в широком смысле слова), где люди могут приобрести ваш продукт.

Именно для достижения этих целей выстраиваются отношения с партнерами. И первым делом речь идет о дистрибуции и методах продаж.



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

Главное, чтобы ваш продукт стал доступен самому широкому спектру потребителей. Сотрудничество с партнерами позволяет увеличить охват рынка, а также повысить эффективность своей работы за счет сокращения количества контактов (ведь их берут на себя партнеры).
При этом создание канала дистрибьюции позволяет одновременно решить массу сопутствующих задач:
  • Транспортировку может взять на себя один из партнеров;
  • Хранение может быть организовано на партнерском складе;
  • Продажи могут вести партнеры разных уровней;
  • Масштабирование продукта можно делать вместе с партнерами;
  • Реклама может быть запущена вместе с партнерами;
  • Time-to-market сокращается, если вы пользуетесь готовой цепочкой поставок;
  • Финансовая поддержка может стать одним из факторов сотрудничества, если партнера заинтересовал ваш продукт;
  • Вы можете получить маркетинговую поддержку от партнера, если у него уже выделен бюджет на маркетинг.



Продажи


Однако кроме партнерских отношений вам нужно выстроить саму схему продаж. И сделать это должен именно CEO. А поскольку Product Manager это mini-CEO, который курирует свой продукт, навыки продаж ему просто необходимы.

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



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



В реальности все иначе. Потому что нет лучшего продажника (сейла), чем основатель и идеолог продукта. Ведь у вас уже есть уникальные преимущества, которые могут сделать из вас очень сильного продажника:
  • Вера в продукт;
  • Ваша страсть;
  • Отраслевой опыт.



Ведь если задуматься, ключевая роль фаундера не только создавать продукт, но и разговаривать с пользователями, чтобы понимать их нужды и развивать продукт в правильную сторону. Если задуматься, каждую секунду своего рабочего дня ты занимаешь только двумя задачами:
  1. Создаешь продукт и Разговариваешь с пользователями;
  2. Создаешь продукт и Разговариваешь с пользователями;
  3. Создаешь продукт и Разговариваешь с пользователями.

Но общение с пользователями это и есть продажи! Слушать пользователей это и есть большая часть процесса, который называется продавать.



А теперь очень важная и секретная информация!



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



Найдите свои 2,5%!


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



В своей книге Диффузия инноваций, Эверетт Роджерс подчеркивает, что людей, готовых на это лишь 2,5%!



Только после первой пробы инноваторами продуктом могут начать пользоваться early-adopters. Поэтому для начала вам во что бы то ни стало нужно найти свою первую сотню потенциальных клиентов, и если два из них захотят купить продукт это успех! В вопросах продвижения чего-то нового большую роль играет закон больших чисел, и здесь оказываются полезны все возможные контакты.

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

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





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

Холодные email


Несмотря на негативное отношение большинства к рассылкам, холодные email оказываются очень результативными для установления контактов. Нет, это не спам и не шаблонные рассылки. Холодные письма нужно писать, придерживаясь четких правил:
  • Персонализированные вы должны знать, кому вы пишете
  • Короткие буквально несколько предложений
  • Конкретные нужно четко продумать, что вы хотите сообщить
  • Содержащие call to action не просто информация, а предложение пообщаться
  • И главное, их цель выйти на разговор и выслушать (а не продать)


Вот пример неплохого холодного email

Тимофей,
Меня зовут Василий и я основатель компании Мегакорп.

Моя компания разрабатывает новую технологию, которая позволит снизит затраты на бухгалтерскую отчетность на 80%.

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

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

У тебя будет 20 минут на этой неделе? Я доступен во вторник в 11 или 12, если это удобное время для тебя.
Василий


Откуда брать адреса?


Допустим, вы придумали, что будете писать в холодных email. Возникает проблема поиска адресов. Повторюсь, безликие базы и листы рассылок не подходят!



Вместо этого вы можете:
  • Обмениваться визитками на конференциях
  • Находить контакты в LinkedIn и других соцсетях
  • Собирать контакты на веб-сайте в обмен на бонус информацию, исследование, какие-то спецпредложения

При этом не стоит думать, что вы получите сразу большой отклик. Согласно воронке продаж, на первом этапе из 100% потребителей (то есть всего рынка) вашими реальными заказчиками могут стать только 2%!



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



Нужно узнать, в чем проблема заказчика? Какая у него потребность? Как можно решить актуальную задачу? Как здесь может помочь наш продукт? Почему человек вообще захотел встречаться/говорить с вами чего он ищет?

При этом каждый лид нужно доводить до конца (делать follow up). Ниже приведена схема обработки клиента, которая длилась 3 месяца. В результате был заключен контракт на 1,5 миллиона рублей. Но если бы продажник бросил это дело на каком-то этапе, этих денег бы не было!



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



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



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



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

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


  • Учитывайте личные качества сотрудников отдела продаж
  • Одни сами разберутся и научатся, а также уговорят любого без подсказок
  • Другим нужны материалы: презентации, скрипты, battlecard



Анализ Win-loss


Последняя тема, на которой мне хотелось бы остановиться сегодня это win-loss анализ или, говоря другими словами, ревизия результатов рыночный борьбы вашего продукта с конкурентными решениями. Такой анализ открывает путь к повышению выручки (revenue growth) и удержанию выручки (revenue retention).



В ходе win-loss анализа обычно проводят следующие выкладки:
Closed Lost analysis
  • Какие общие паттерны у сделок которые мы проиграли?
  • Что нам надо добавить или сделать, чтобы их выиграть?
Closed Win analysis
  • Что общего у сделок, которые мы выиграли?
  • Как нам лучше использовать наши сильные стороны и выиграть еще больше сделок?


Для того, чтобы провести Closed Lost Analysis нужно:

  • Собрать все вводные детали о сделке
  • Зафиксировать их в инструментах для продаж (например, в Salesforce)
  • Получить обратную связь от клиентов
  • Оценить недополученную выручку
  • Провести опрос внутри отдела продаж, расставить приоритет и определить вес каждой причины
  • Подготовить результат анализа и передать его менеджеру продукта
  • Модифицировать дорожную карту (roadmap) по итогам анализа, чтобы до 70% фичей в роадмапе были из этого источника




При этом каждая из причин поражения должна стать толчком для изменений.
Клиент оставил текущее решение Спросите, какое? Почему он так решил?
У клиента нет бюджета Но решение интересно? Тогда обратитесь через год!
Клиенты не отвечают на предложения Значит нужно лучше прорабатывать лиды, целевую аудиторию, усиливать маркетинг
Выбраны продукты конкурентов Какие? Почему? Как мы можем от них отстроиться?
Высокая цена Были ли скидки для этих клиентов? Что мы можем сделать с ценовой политикой?
Не подошло лицензирование Может быть стоит пересмотреть нашу политику?
Не хватает функционала Сделать запрос на разработку и обновить RoadMap
Подвело качество или надежность Провести анализ слабых мест
Продукт показался сложным Поставить задачу для UX\UI исследования и доработки интерфейса

Заключение

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

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

Видео-запись всех лекций курса доступна на YouTube

Лекция про партнерские отношения, дистрибуцию и продажи:

Подробнее..
Категории: Acronis , Asp

Категории

Последние комментарии

  • Имя: Макс
    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