﻿<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
  <channel><title>d2funlife | Блог о программировании</title>
<description>Блог о программировании. Программирование веб, код .NET и все, все, все. ASP.NET Core, ASP.NET MVC, C#, .NET Framework, .NET Core, Docker, Teamcity.</description>
<generator>d2funlife</generator>
<link>https://d2funlife.com</link>
<item>
  <title>Видосики #1</title>
  <link>https://d2funlife.com/vidosiki1</link>
  <description>&lt;p&gt;&lt;strong&gt;Hidden gems in .NET Core 3 - David Fowler &amp;amp; Damian Edwards&lt;/strong&gt;&lt;/p&gt;
&lt;div class="video-container"&gt;&lt;iframe width="560" height="315" frameborder="0" allowfullscreen="allowfullscreen" src="https://www.youtube.com/embed/xdSSH63IZZc"&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;Релиз .NET Core 3 готовит огромную пачку улучшений и изменений. Данный доклад в сжатом формате представляет основные нововведения, изменения. Изменений очень много. Таймкоды для быстрой навигации:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=66s" target="_blank" rel="nofollow noopener"&gt;1:06&lt;/a&gt; ограничения для GC (сборщика мусора)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=105s" target="_blank" rel="nofollow noopener"&gt;1:45&lt;/a&gt; пример графика с новыми ограничениями GC&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=140s" target="_blank" rel="nofollow noopener"&gt;2:20&lt;/a&gt; AssemblyLoadContext API, новая возможность для плагинов, библиотеки можно зрагрузить и выгрузить в отдельном контексте.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=439s" target="_blank" rel="nofollow noopener"&gt;7:19&lt;/a&gt; Startup hooks - код, который будет выполняться перед методом Main&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=565s" target="_blank" rel="nofollow noopener"&gt;9:25&lt;/a&gt; новые кроссплатформенные инструменты для диагностики приложений&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=1138s" target="_blank" rel="nofollow noopener"&gt;18:58&lt;/a&gt; 3 новых опции для публикации приложений&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=1676s" target="_blank" rel="nofollow noopener"&gt;27:56&lt;/a&gt; улучшения взаимодействия с нативным кодом&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=1786s" target="_blank" rel="nofollow noopener"&gt;29:46&lt;/a&gt; IAsyncDisposable - неблокирующее выполнение dispose объекта&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=1854s" target="_blank" rel="nofollow noopener"&gt;30:54&lt;/a&gt; распределенная трассировка запросов пользователя (пример: &lt;a href="https://zipkin.io/" target="_blank" rel="nofollow noopener"&gt;https://zipkin.io/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=2310s" target="_blank" rel="nofollow noopener"&gt;38:30&lt;/a&gt; определение функций среды во время выполнения кода&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=2357s" target="_blank" rel="nofollow noopener"&gt;39:17&lt;/a&gt; улучшения threadpool&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=2409s" target="_blank" rel="nofollow noopener"&gt;40:09&lt;/a&gt; Process.Kill, BitOperations, Microsoft.VisualBasic, DbCommand async&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=2473s" target="_blank" rel="nofollow noopener"&gt;41:13&lt;/a&gt; UnixDomainSocket - отказ от библиотеки libuv в Kestrel&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=2520s" target="_blank" rel="nofollow noopener"&gt;42:00&lt;/a&gt; Интеграция статический данных (скриптов, картинок и т.д.) в представления Razor&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=2590s" target="_blank" rel="nofollow noopener"&gt;43:10&lt;/a&gt; переменные IIS сервера теперь доступны для ASP.NET Core приложений&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=2596s" target="_blank" rel="nofollow noopener"&gt;43:16&lt;/a&gt; System.IO.Pipelines&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=2662s" target="_blank" rel="nofollow noopener"&gt;44:22&lt;/a&gt; Новые сетевые абстракции. Kestrel теперь имеет изолированый транспортный API. API можно использовать для написания собственных клиетов и серверов.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=2905s" target="_blank" rel="nofollow noopener"&gt;48:25&lt;/a&gt; LOL 😂&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=3206s" target="_blank" rel="nofollow noopener"&gt;53:26&lt;/a&gt; HostMatcherPolicy, HostAttribute. Теперь есть возможность действие контроллера привязать к домену или порту приложения!&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=3398s" target="_blank" rel="nofollow noopener"&gt;56:38&lt;/a&gt; TrailingHeaders - возможность отправить заголовок после отправки тела ответа. Используется для отправки служебной информации, метрик. Добавлено для полной поддержки gRPC&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=3440s" target="_blank" rel="nofollow noopener"&gt;57:20&lt;/a&gt; улучшена страница ошибки для разработчика. Теперь поддерживается отправка данных без HTML.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=3485s" target="_blank" rel="nofollow noopener"&gt;58:05&lt;/a&gt; улучшения компрессии данных&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=3529s" target="_blank" rel="nofollow noopener"&gt;58:49&lt;/a&gt; интеграция systemd. Возможность запускать демоном свое приложение под linux ос.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=3535s" target="_blank" rel="nofollow noopener"&gt;58:55&lt;/a&gt; IConfigurationRoot.GetDebugView - получение снимка все конфигурационных парамеров&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=3543s" target="_blank" rel="nofollow noopener"&gt;59:03&lt;/a&gt; SystemProviderOPtions.ValidateOnBuild - возможность проверить, что зависимости (DI) настроены корректно&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=3561s" target="_blank" rel="nofollow noopener"&gt;59:21&lt;/a&gt; валидация options - проверка что все объекты конфигураций корректы&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=3583s" target="_blank" rel="nofollow noopener"&gt;59:43&lt;/a&gt; LoggerFactory.Create - теперь более легко создать фабрику логирования для использования в приложениях без DI&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xdSSH63IZZc&amp;amp;t=3590s" target="_blank" rel="nofollow noopener"&gt;59:50&lt;/a&gt; добавили возможность получать конфигурации из потоковых источников. Существующие файловые методы конфигураций перевели на потоковые.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Отличный доклад от очень крутых программистов. Интересная подача, лайвкодинг.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Как добавить свою оптимизацию в JIT, Егор Богатов&lt;/strong&gt;&lt;/p&gt;
&lt;div class="video-container"&gt;&lt;iframe width="560" height="315" frameborder="0" allowfullscreen="allowfullscreen" src="https://www.youtube.com/embed/cto4gttyxHI"&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;Доклад приотрывает детали устройства компиляторов. Показаны точки расширения для применения оптимизаций. Интересные примеры во что может быть трансформирован ваш код и как, поменяв строки местами, можно уменьшить количесвто сгенерированных инструкций.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IDE wars: мы, наши друзья, наши соперники и наши...партнёры (Кирилл Скрыган)&lt;/strong&gt;&lt;/p&gt;
&lt;div class="video-container"&gt;&lt;iframe width="560" height="315" frameborder="0" allowfullscreen="allowfullscreen" src="https://www.youtube.com/embed/LfuvTI0uL6A"&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;История становления JetBrains, на каком этапе развития IDE как таковой мы находимся. Почему Visual Studio Code не сможет обогнать IDE от JetBrains. И как же языки программирования и экосистемы для программистов влияют на огромный бизнес и большие корпорации. Так же рассказано как конкурирует Microsoft и JetBrains.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Be More Productive with JetBrains Rider&lt;/strong&gt;&lt;/p&gt;
&lt;div class="video-container"&gt;&lt;iframe width="560" height="315" frameborder="0" allowfullscreen="allowfullscreen" src="https://www.youtube.com/embed/mTW_BUUKKRM"&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;Если по воле обстоятельств или нелепой случайности вы не пробовали в деле JetBrains Rider, данное видео может быть коротким экскурсом в основные особенности этой IDE. Продемонстрированны особенности редактирования кода, а так же обвязка дополнительных инструментов, которые делают Rider отличным швейцарским ножом.&lt;/p&gt;</description>
  <author>d2funlife@gmail.com (Danil Pavlov)</author>
  <category>видосики</category>
  <guid isPermaLink="false">6bc6dc47-ffbd-4d79-a5e7-fec62ebef1f4</guid>
  <pubDate>Tue, 17 Sep 2019 10:55:42 GMT</pubDate>
</item>
<item>
  <title>TeamCity, Docker и два бранча!</title>
  <link>https://d2funlife.com/teamcity-docker-n-two-branches</link>
  <description>&lt;p&gt;Для многих контейнеризация стала стандартом распространения сервисов. TeamCity же один из лучших инструментов для организации CI\CD. Что же, давайте сольем все в одну емкость и получим прекрасный коктейль, который будет помогать и радовать нас. По пути разберемся, как использовать параметры и шаблоны конфигураций. Для реализации данных сценариев я буду использовать проект движка блога (вы сейчас его читаете). Что мы имеем и что хотим в итоге получить: сборка asp.net core проекта в docker образ, публикация образа в репозиторий, запуск сценария обновления на удаленном сервере. Опционально можно так же добавить проставление тега на последний коммит обработанных изменений. На уровне "Root project" добавляем проект:&lt;/p&gt;
&lt;p&gt;&lt;a class="b-single-post__gallery" href="#gallery-popup1"&gt; &lt;img src="images/tlPqYvG.png" alt="Стартовая страница TeamCity" /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;div id="gallery-popup1" class="b-gallery hidden"&gt;&lt;a class="b-gallery__link" title="Стартовая страница TeamCity" href="images/tlPqYvG.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Создание нового проекта" href="images/Lkp0W0d.png"&gt;link&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;В репозитории имеется две ветки - master и develop, давайте сделаем публикацию develop на тестовый домашний сервер. Позже добавим публикацию master на production сервер. Настроим как первый источник ветку develop. В настройках проекта раздел "Version Control Settings":&lt;/p&gt;
&lt;p&gt;&lt;img class="lozad" alt="Настройка контроля версий проекта" width="870" height="857" src="./images/VTgKYeB.png" /&gt;&lt;/p&gt;
&lt;p&gt;Теперь TeamCity&amp;nbsp;мониторит все изменения ветки develop и можно настроить тригер для запуска всего цикла обработки на изменения кода. Так же можно указать более сложные правила для срабатывания тригера, тут есть широкий вариант настроек. Например, можно указать маску директории и тригер будет срабатывать только если изменения были в файлах этой директории.&lt;/p&gt;
&lt;p&gt;&lt;img class="lozad" alt="Триггеры проекта" width="870" height="575" src="./images/7AMgVEp.png" /&gt;&lt;/p&gt;
&lt;p&gt;Для более простого понимания устройства&amp;nbsp;TeamCity: все проекты &amp;mdash; это дерево с общим корнем в виде "Root project" и дочерние проекты, которые наследуют все параметры у родителей. Таким образом можно описывать в родительских проектах общие параметры конфигурации и расширять конфигурацию собственными параметрами в дочерних проектах. Каждый проект состоит из конфигураций, от одной и более. Конфигурация состоит из build step&amp;rsquo;ов, которые описывают действия и выстраиваются в определенный порядок &amp;ndash; цепочка действий, которая приводит к результату конфигурации. Создадим первый build step для построения docker образа проекта:&lt;/p&gt;
&lt;p&gt;&lt;img class="lozad" alt="Docker build step" width="870" height="722" src="./images/XcLIuQD.png" /&gt;&lt;/p&gt;
&lt;p&gt;После получения образа, нам необходимо его опубликовать в репозитории. Для этого нам необходимо залогиниться в нем, добавим новый build step:&lt;/p&gt;
&lt;p&gt;&lt;img class="lozad" alt="Docker login step" width="870" height="570" src="./images/e54clWx.png" /&gt;&lt;/p&gt;
&lt;p&gt;Внимательный читатель обратит внимание, что указаны переменные конфигурации (%dockerLogin% и %dockerPassword%), а мы их еще не добавляли. Давайте же сделаем это. Так, как я уже описывал, TeamCity &amp;ndash; дерево, и мы можем добавлять параметры конфигурации, которые будут доступны для всех дочерних уровней от корневого. Если у вас используется какой-то конкретный аккаунт для проекта, то можно указать его в параметрах проекта, если же параметры будут общими и для других проектов, то можем их указать на уровне "Root project" и они будут доступны везде.&lt;/p&gt;
&lt;p&gt;&lt;a class="b-single-post__gallery" href="#gallery-popup2"&gt; &lt;img src="images/BOPvNkq.png" alt="TeamCity добавление параметра" /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;div id="gallery-popup2" class="b-gallery hidden"&gt;&lt;a class="b-gallery__link" title="TeamCity добавление параметра" href="../images/BOPvNkq.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Редактирование свойст параметра" href="images/deo1GEY.png"&gt;link&lt;/a&gt;&lt;a class="b-gallery__link" title="Задание значения параметра" href="images/Jh2EHzu.png"&gt;link&lt;/a&gt;&lt;a class="b-gallery__link" title="Общее значение параметра" href="images/nO2ETRW.png"&gt;link&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;Помечая параметры как hidden + password мы обеспечиваем защиту и скрытие отображения значений этих параметров в логах при билде проекта. После того&amp;nbsp;как мы залогинились, мы можем публиковать наш образ, для этого добавим еще один build step:&lt;/p&gt;
&lt;p&gt;&lt;img class="lozad" alt="TeamCity docker push" width="870" height="577" src="./images/uWX8CDH.png" /&gt;&lt;/p&gt;
&lt;p&gt;И остается финальный build step, который будет запускать цикл обновления нашего сервиса на удаленном сервере. Путей решения этого вопроса просто уйма, я же буду запускать скрипт на сервере через SSH:&lt;/p&gt;
&lt;p&gt;&lt;img class="lozad" alt="TeamCity ssh exec" width="870" height="732" src="./images/rvnZnCV.png" /&gt;&lt;/p&gt;
&lt;p&gt;Готово! Теперь мы получили полностью готовый цикл, который будет обновлять сервис при изменениях в ветке develop. "Хорошо, для master ветки копируем и все работает, все понятно, мы читать дальше не будем" &amp;ndash; так могли подумать вы...&amp;nbsp; и это может быть нормальный вариант. Но я предложу чуточку шагнуть дальше и создать шаблон конфигурации. Шаблон конфигурации используется, если у вас есть разные конфигурации по параметрам, но одинаковые по сути. И редактирование множества конфигураций сводится к редактированию шаблона. К примеру, у вас поменялся сценарий публикации приложения, необходимо добавить еще один build step, но если конфигурации копировались отдельно, то отдельно придется и настраивать дополнительный шаг везде. Шаблон же позволяет сделать это в одном месте, конфигурация унаследует шаблон, наполняя его только своими параметрами.&lt;/p&gt;
&lt;p&gt;Для того, чтоб добавить шаблон у нас сейчас есть два варианта: сгенерировать шаблон из конфигурации develop и потом только выделить параметры, либо создать все с нуля и привязать к конфигурациям. Я сгенерирую шаблон и вычленю наши ключевые конфигурации в переменные:&lt;/p&gt;
&lt;p&gt;&lt;a class="b-single-post__gallery" href="#gallery-popup3"&gt; &lt;img src="images/FRbxAzt.png" alt="TeamCity добавление параметра" /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;div id="gallery-popup3" class="b-gallery hidden"&gt;&lt;a class="b-gallery__link" title="TeamCity генирация щаблона" href="images/FRbxAzt.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Результат генирации" href="images/X1UgpjJ.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Начало редактирования шаблона" href="images/mcdVAt3.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Выделение параметра теги для docker образа" href="images/1jIV2zH.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Указание параметра тегов для docker образа в build step'e" href="images/qMIgVgB.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Выделение параметра ip сервера" href="images/Ws0pkxb.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Выделение параметра логин пользователя сервера" href="images/NEzUGnQ.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Выделение параметра пароль пользователя сервера" href="images/gKRXoiB.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Указание всех параметров в build step'e" href="images/KzFW8CZ.png"&gt;link&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;Так же необходимо будет убрать из шаблона привязку к vcs root, да бы независимо настраивать это в конфигурациях. Теперь давайте добавим новую конфигурацию на основе шаблона для обновления сервиса на продакшн сервере из ветки master:&lt;/p&gt;
&lt;p&gt;&lt;a class="b-single-post__gallery" href="#gallery-popup4"&gt; &lt;img src="../../../images/mgxZh7J.png" alt="TeamCity добавление новой конфигурации на основе шаблона" /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;div id="gallery-popup4" class="b-gallery hidden"&gt;&lt;a class="b-gallery__link" title="TeamCity добавление новой конфигурации на основе шаблона" href="../../../images/mgxZh7J.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Указание параметров конфигурации" href="../../../images/cAg1oLn.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Выбор VCS ветки для мониторинга" href="../../../images/fqHWSMJ.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Привязка конфигурации к шаблону" href="../../../images/FUqkT9O.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Выбор шаблона конфигурации для привязки" href="../../../images/pRzreRn.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Параметры шаблона, которые необходимо заполнить для конфигурации" href="../../../images/wUVKOGx.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Указание параметров шаблона для существующей конфигурации ветки development" href="../../../images/WnhQsp7.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Результирующий вид для конфигурации" href="../../../images/RCUZkhq.png"&gt;link&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;Вот так мы можем настроить TeamCity для тесного содружества с docker и обновлять сервера без проблем. И да, опциональное проставление тегов на последний коммит в изменениях. Для чего нужно - если у вас идет очень много изменений, то да бы иметь историю версий прямо в гите, рекомендуется проставлять теги. Мы можем сделать это автоматически, для этого нам необходимо зайти в конфигурацию и в настройках Build Features добавить VCS labeling:&lt;/p&gt;
&lt;p&gt;&lt;img class="lozad" alt="Добавление тега к коммиту" width="870" height="501" src="https://d2funlife.com/images/KNwrXit.png" /&gt;&lt;/p&gt;
&lt;p&gt;Теперь точно все, если у вас есть вопросы, замечания, велкам в комменты!&lt;/p&gt;</description>
  <author>d2funlife@gmail.com (Danil Pavlov)</author>
  <category>непрерывная интеграция</category>
  <category>docker</category>
  <guid isPermaLink="false">f66af6a3-166c-4b60-9db0-2c9318e3c7c9</guid>
  <pubDate>Tue, 16 Jul 2019 23:47:41 GMT</pubDate>
</item>
<item>
  <title>Создаем скриншот и pdf документ из веб страницы</title>
  <link>https://d2funlife.com/screenshooter</link>
  <description>&lt;p&gt;Все мы любим фоточки, а прикиньте фоточки нужной веб страницы в полный рост! А если это все в pdf по формату читалки?! Да это же крутяк! Можно накидать статей в читалку, очистить список непрочитанного в pocket. Делаем! Нужен фотик для всего этого, и тут вариантов для реализации много, да вот только не все они хорошие.&amp;nbsp;&lt;a title="PhantomJS" href="http://phantomjs.org/" target="_blank" rel="noopener"&gt;PhantomJS&lt;/a&gt; - для сложных страниц не подходит, хоть и идея хороша, к тому же проект, судя по всему, уже все, &lt;a title="phantomjs is dead" href="https://medium.com/devschacht/phantomjs-is-over-df065e5b23bf" target="_blank" rel="noopener"&gt;того&lt;/a&gt;... Для нормального отображения веб страницы нужен "полноценный" браузер. И тут очень в тему будет &lt;a title="SeleniumHQ" href="https://www.seleniumhq.org/" target="_blank" rel="noopener"&gt;SeleniumHQ&lt;/a&gt;&amp;nbsp;- инструмент автоматизации браузера, как заявляют на главной. Отлично, фото в полный рост будет, а что насчёт pdf? Тут все как обычно - куча платных библиотек, есть и бесплатные, но со звездочкой, есть и без звездочек, но они существуют как отдельное приложение. Как вариант, я выбрал &lt;a title="iTextSharp" href="https://github.com/itext/itextsharp" target="_blank" rel="noopener"&gt;iTextSharp&lt;/a&gt;. Для opensource проектов они предоставляют бесплатное использование - нам подходит. А теперь собирем все вместе и да начнется фотосет!&lt;/p&gt;
&lt;p&gt;Для получение скриншота страницы нам нужен SeleniumHQ + Chrome. Так как мы уже &lt;a title="Docker в быту разработчика" href="../../../docker-for-everybody" target="_blank" rel="noopener"&gt;прошареные&lt;/a&gt; ребята, то знаем как можно получить желаемое максимально быстро. Да-да,&amp;nbsp;&lt;a title="Selenium Docker" href="https://hub.docker.com/r/selenium/standalone-chrome/" target="_blank" rel="noopener"&gt;Selenium Docker&lt;/a&gt;&amp;nbsp;и всех делов. Для запуска нам нужно всего-то выполнить одну команду для запуска контейнера.&lt;/p&gt;
&lt;pre class="language-bash"&gt;&lt;code&gt;docker run -d -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-chrome:3.14.0-curium&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Теперь наведем объектив нашего браузера на нужную страницу и начнем снимать. Для подключения к браузеру мы будем использовать&amp;nbsp;&lt;a title="Selenium WebDriver" href="https://www.nuget.org/packages/Selenium.WebDriver" target="_blank" rel="noopener"&gt;Selenium WebDriver&lt;/a&gt;.&amp;nbsp;Всю документацию можно найти на странице &lt;a title="SeleniumHQ" href="https://www.seleniumhq.org/docs/03_webdriver.jsp#setting-up-a-selenium-webdriver-project" target="_blank" rel="noopener"&gt;инструмента&lt;/a&gt;.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;var driver = new RemoteWebDriver(new Uri("http://localhost:4444/"), new ChromeOptions());
driver.Navigate().GoToUrl("https://google.com");

//Устанавливаем размер окна
driver.Manage().Window.Size = new Size(1024, 768);

//Перемещаем скрол на нужную позицию
driver.Manage().Window.Position = new Point(0, 0);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Общая схема создания скриншота такова - мы определяем "пазлы", которые формируют всю страницу, каждый пазл имеет свои размеры и свои координаты. Имея весь набор пазлов, мы проходимся в цикле по кажому и делаем скриншоты страницы. Каждый скриншот переносим в общее полотно изображения всей страницы.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;private Image GetFullPageImage(IWebDriver driver, ShotOptions options)
{
    ResizeBrowser(driver,
                  options.Width + ScrollWidthOffset,
                  options.StepHeight + MenuHeightOffset);

    var jsExecutor = (IJavaScriptExecutor) driver;

    LoadAllPageHeight(jsExecutor);

    var pageSizes = GetWebPageSizes(jsExecutor);
    if (pageSizes.TotalWidth &amp;gt; pageSizes.ViewportWidth)
    {
        ResizeBrowser(driver,
                      options.Width + ScrollWidthOffset,
                      options.StepHeight + MenuHeightOffset + ScrollWidthOffset);
    }

    var rectangles = new List&amp;lt;Rectangle&amp;gt;();
    for (var yScroll = 0; yScroll &amp;lt; pageSizes.TotalHeight; yScroll += pageSizes.ViewportHeight)
    {
        var rectangleHeight = pageSizes.ViewportHeight;
        if (yScroll + pageSizes.ViewportHeight &amp;gt; pageSizes.TotalHeight)
        {
            rectangleHeight = pageSizes.TotalHeight - yScroll;
        }

        var currRect = new Rectangle(0, yScroll, options.Width, rectangleHeight);
        rectangles.Add(currRect);
    }

    var takerScreenshot = (ITakesScreenshot) driver;
    var fullPageImage = new Bitmap(options.Width, pageSizes.TotalHeight);
    var graphics = Graphics.FromImage(fullPageImage);

    var yPosition = 0;
    for (var rectangleIndex = 0; rectangleIndex &amp;lt; rectangles.Count; rectangleIndex++)
    {
        jsExecutor.ExecuteScript($"window.scroll(0, {yPosition.ToString()})");

        if (rectangleIndex &amp;gt; 0 ||
            options.HideOverlayElementsImmediate)
        {
            HideFloatingElements(jsExecutor);
        }

        var rectangle = rectangles[rectangleIndex];
        var screenshot = takerScreenshot.GetScreenshot();
        var screenshotImage = ScreenshotToImage(screenshot);

        var sourceRectangle = new Rectangle(0,
                                            pageSizes.ViewportHeight - rectangle.Height,
                                            rectangle.Width,
                                            rectangle.Height);

        graphics.DrawImage(screenshotImage, rectangle, sourceRectangle, GraphicsUnit.Pixel);

        yPosition = rectangle.Bottom;
    }

    return fullPageImage;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;А когда у нас есть полная картинка мы можем делать уже все, что нам вздумается. А вздумается нам получить pdf документ для электронной читалки. Для полного удовлетворения потребностей нам нужно перевести изображения в черно-белый цвет и минимизировать. Каждая страница документа представляет из себя изображение на весь размер. Формирование pdf документа:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;private PdfShot GetPdfDocument(Image fullPageImage, ShotOptions options, String title)
{
    var magicImage = new MagickImage(fullPageImage.ToMemoryStream());
    magicImage.Strip();

    if (options.IsGrayscale)
    {
        magicImage.Grayscale(PixelIntensityMethod.Lightness);
        magicImage.Contrast();
    }
    
    var partsCount = Math.Ceiling((Decimal) magicImage.Height / options.StepHeight);
    partsCount = Math.Ceiling((magicImage.Height + options.OverlaySize * partsCount) / options.StepHeight);
    
    var pageImageParts = new List&amp;lt;Byte[]&amp;gt;();
    for (var i = 0; i &amp;lt; partsCount; i++)
    {
        var y = i * options.StepHeight - i * options.OverlaySize;
        var images = magicImage.CropToTiles(new MagickGeometry(0,
                                                               y,
                                                               options.Width,
                                                               options.StepHeight))
                               .ToList();
        pageImageParts.Add(images.First().ToByteArray());
    }
    
    var pdfBytes = _pdfCreator.CreateDocument(options.Width, options.StepHeight, pageImageParts);
    return new PdfShot(pdfBytes, GetFileName(title));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Формирование документа pdf вынесено в абстракцию, можно подставить необходимую вам реализацию и не зависеть от той или иной библиотеки. В репозитории готового проекта представлена реализация с использованием ITextSharp. Так же там есть и консолька, которая может экспортировать список ссылок в pdf файлы.&lt;/p&gt;
&lt;h2&gt;Примечания&lt;/h2&gt;
&lt;p&gt;Я добавил два метода для получения более чистых скриншотов: прогрузка всей страницы, скрытие плавающих элементов. Не секрет, что сейчас множество веб страниц используют различные оптимизации, для нас самая критичная - ленивая подгрузка ресурсов. Ведь если размер страницы изменился, а мы уже обсчитали пазлы для съемки, то будет либо не вся страница, либо наложение контента. Для решения этой проблемы перед съемкой делается проход по всей странице с малыми паузами и только после этого производится обсчет пазлов. Так же, если на странице присутствуют плавающие элементы, то они будут на каждом скриншоте, это просто не допустимо, поэтому мы их скрываем перед съемкой.&lt;/p&gt;
&lt;p&gt;Из неприятного, но факт: при некорректном использовании веб драйвера, вы можете получать ошибки по типу "session does not exist". Еще имеется открытый &lt;a title="&amp;quot;Port&amp;quot; leaking in C# WebDriver.dll" href="https://github.com/SeleniumHQ/selenium/issues/4162" target="_blank" rel="noopener"&gt;issue&lt;/a&gt; на github, который указывает на проблему использования подключений. При интенсивной нагрузке подключения множатся, драйвер работает в этом плане неверно.&lt;/p&gt;
&lt;p&gt;Исходный код:&amp;nbsp;&lt;a title="Screen Shooter 🔫" href="https://github.com/d2funlife/screenshooter" target="_blank" rel="noopener"&gt;https://github.com/d2funlife/screenshooter&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Примечания по запуску под Linux:&amp;nbsp;&lt;a href="https://www.hanselman.com/blog/HowDoYouUseSystemDrawingInNETCore.aspx"&gt;https://www.hanselman.com/blog/HowDoYouUseSystemDrawingInNETCore.aspx&lt;/a&gt;&lt;/p&gt;</description>
  <author>d2funlife@gmail.com (Danil Pavlov)</author>
  <category>docker</category>
  <category>.net core</category>
  <guid isPermaLink="false">69f1b0cd-f6e9-456f-b28a-cae993e4ddc9</guid>
  <pubDate>Fri, 21 Sep 2018 06:12:55 GMT</pubDate>
</item>
<item>
  <title>Appveyor + Docker = ❤</title>
  <link>https://d2funlife.com/appveyor-love-docker</link>
  <description>&lt;p&gt;Не так давно Appveyor добавили поддержку Linux агентов сборки, а это значит, что пришло новое счастье для множества open source проектов. Ведь раньше для сборки проекта под Linux необходимо было использовать что-то другое, например Travis CI, и выглядело это все как-то так:&lt;/p&gt;
&lt;p&gt;&lt;img src="images/F3zsxjJ.png" alt="Старая схема CI " width="870" height="373" /&gt;&lt;/p&gt;
&lt;p&gt;И все бы хорошо, но скорость Travis CI для открытых проектов оставляет желать лучшего. Сейчас же мы можем построить полный цикл неприрывной интеграции (CI) на Appveyor. Я хочу рассказать, как настроить сборку docker образа, публикацию образа в репозиторий, а так же запуск скрипта для обновления docker контейнеров. Все примеры подразумевают использование open source проектов и не требуют оформления подписки. Если интересно, как скрыть приватные файлы конфигураций в открытом проекте, а так же не иметь проблем с доступом к этим файлам уже в docker контейнере, пишите комментарии, опишу отдельно этот вопрос.&lt;/p&gt;
&lt;p&gt;Appveyor поддерживает конфигурацию в 2х режимах - через визуальную админ часть или через конфигурационные файлы *.yml. Оба подхода имеют одну и ту же суть, разница в том, что админ часть создает конфигурационный файл на сервере и после использует его, а ручной вы можете создать в любом текстовом редакторе и использовать его, храня у себя в исходном коде. По умолчанию используется серверная конфигурация, но если добавить конфигурационный файл в репозиторий вашего проекта, то будет использоваться он. Я покажу всю настройку на примере собственного файла. В качестве подопытного выступает непосредственно этот блог. Для него сформирована следующая конфигурация:&lt;/p&gt;
&lt;pre class="language-yaml"&gt;&lt;code&gt;# Версия, которая будет присваиваться каждой сборке. Переменная build - инкремент номера сбороки.
version: 1.0.0.{build}
# Образ виртуальной машины на которой будет происходить сборка.
image: ubuntu

# Глубина клонирования репозитория. По умолчанию клонируется вся история. Флаг 1 указывает клоинирование без истории.
clone_depth: 1

# Номер сборки не будет инкрементироваться при создании pull request'а.
pull_requests:
  do_not_increment_build_number: true

# Переменные окружения.
environment:
  DOCKER_PASS:
    secure: tyCs62GdU59M1vNsnAwKrw==
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
  DOTNET_CLI_TELEMETRY_OPTOUT: true

# Параметр указывает, что сборка будет производиться только при изменении master ветки. По умолчанию сборка производится на всех ветках.
branches:
  only:
    - master

skip_commits:
  files:
    - README.md

install:
- docker version

build_script:
- ps: .\build.ps1

# Этап выполнения тестов отключен
test: off

# Используется потому как не поддерживается скрипт и провайдер одновременно в deploy
before_deploy:
- ps: .\deploy.ps1

deploy:
- provider: Webhook
  url:
    secure: bwtxM3PwlQqNCxXgKZ7Aul9FWEx9OTMjV4suCKf0E6vOI
  authorization:
    secure: xFex7sajn/RKjS/cZrqSJnEjrhRO4535ALSzvC+OjzY7xVlq==
  on:
    branch: master&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;К каждому пункту конфигурации добавлены комментарии, так будет проще и быстрее понять общий сценарий. Отдельно отмечу официальную документацию &lt;a href="https://www.appveyor.com/docs/"&gt;https://www.appveyor.com/docs/&lt;/a&gt;&amp;nbsp;в ней расписаны детально все ньюансы, но только на английском языке. Давайте посмотрим непосредственно на скрипты build.ps1 и deploy.ps1. Они запускаются в соответствующие названию этапы обработки. К каждому этапу можно добавить сразу несколько скриптов.&lt;/p&gt;
&lt;p&gt;build.ps1 -&amp;nbsp;скрипт запускает сборку docker образа и присваивает ему сразу несколько тегов.&lt;/p&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;$Version=$env:APPVEYOR_BUILD_VERSION
Write-Host Starting build $Version

docker build -t d2funlife/blog:latest -t d2funlife/blog:$Version .&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;deploy.ps1 - скрипт логинится в docker hub репозиторий и выполняет push образа, который мы собрали на предыдущем этапе.&lt;/p&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;$Version=$env:APPVEYOR_BUILD_VERSION
Write-Host Starting deploy $Version

docker images

Write-Host Docker login

$env:DOCKER_PASS | docker login --username d2funlife --password-stdin

Write-Host Push to docker hub

docker push d2funlife/blog&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;И когда образ опубликован, остается только один вопрос - как же обновить его на нашем хосте. Список доступных вариантов деплоя представлен в документации &lt;a href="https://www.appveyor.com/docs/deployment/"&gt;https://www.appveyor.com/docs/deployment/&lt;/a&gt;. К сожалению, не поддерживается выполнение ssh команды на удаленном сервере. С использованием docker контейнеров мы можем запустить такой сценарий, но это что-то из разряда костылей и подпорок + необходимо быть внимательным к вопросу безопасности. Хорошей альтернативой является Webhook. Это веб-запрос на указаный вами url, с параметрами авторизации (для безопасности). Приняв запрос на сервере, вы можете выполнить необходимую команду для обновления docker контейнеров. Я сделал простой веб-сервис на .NET Core, который принимает запрос и запускает shell скрипт обновления.&amp;nbsp;Таким образом мы получаем полностью настроенную непрерывную интегарцию с помощью Appveyor.&lt;/p&gt;</description>
  <author>d2funlife@gmail.com (Danil Pavlov)</author>
  <category>непрерывная интеграция</category>
  <category>docker</category>
  <guid isPermaLink="false">2d3255b8-f85c-4737-9eac-10cde6f240e1</guid>
  <pubDate>Tue, 10 Jul 2018 12:39:10 GMT</pubDate>
</item>
<item>
  <title>Docker в быту разработчика</title>
  <link>https://d2funlife.com/docker-for-everybody</link>
  <description>&lt;p&gt;В последнее время сталкиваюсь с такими двумя фразами, относительно docker, первая - "Зачем мне нужен docker, у меня монолитное приложение, менять ничего не нужно, все уже отлажено", и вторая - "Я видел пару статей про docker, интересно будет разобраться, но потом как-то выделю время на это". Постараюсь дать свои комментарии на обе фразы.&lt;/p&gt;
&lt;p&gt;Чем же docker будет лучше всеми привычных виртуальных машин? На этот вопрос уже отвечали множество спикеров, книг и статей, не буду много расписывать. Docker, по своей сути, создает изолированное пространство с наименьшими затратами ресурсов. Вам не нужно иметь поверх основной операционной системы приложение виртуализации, виртуальные операционные системы. Поверх основной операционной системы вы можете создавать множество изолированных контейнеров.&lt;/p&gt;
&lt;p&gt;&lt;img class="lozad" alt="Сравнение docker и виртуальных систем" width="839" height="489" src="./images/GtmPJpC.png" /&gt;&lt;/p&gt;
&lt;p&gt;Docker закрывает множество проблем инфраструктурного характера. Вы можете решить проблемы с ограничением ресурсов для своего приложения, создать без особых проблем кластер и многое другое. Ко всему этому, вы получаете возможность "портативного" использования своих контейнеров вне зависимости от вашей операционной системы. Так, к примеру, у разработчика есть виртуальная машина, где уже все настроено как ему удобно и нужно, где провел немало часов. И вдруг нужно перенести виртуалку на другой компьютер, и хорошо, если операционные системы совпадают, можно решить проблему копированием огромных образов и установкой нужного софта, а если вы работали на windows под Hyper-V, а теперь вам нужно получить тоже самое для Mac OS или Linux, то обычным копированием влоб не удастся решить эту проблему. В свою очередь, docker позволил бы решить эту проблему обычным копированием скрипта запуска контейнера и переносом данных. Многие .NET разработчики использовали MS SQL Server на своих компьютерах и знают, что сам процесс установки его вручную - боль еще та, и к тому же, по итогу получаем кучу фоновых сервисов, которые работают всегда, а если это компьютер личного пользования, то сервисы работают даже тогда, когда в них нет нужды. Подобную проблему очень легко решить с помощью docker, ведь мы получаем полностью изолированную &amp;ldquo;песочницу&amp;rdquo;, в которой можем делать все, что захотим. Когда вам нужно, можно просто остановить контейнер с приложением и расхода ресурсов нет, ваша основная операционная система чиста. Для SQL Server Microsoft предоставили официальный docker образ, который можно использовать в своих нуждах. Что же такое контейнер и образ? Образ приложения - готовый пакет софта и данных, который вы можете запустить, а вот когда вы его запустите, то получите контейнер. По сути контейнер - рабочий экземпляр (instance) вашего софта. Для нетерпеливых новичков приведу примеры готовых скриптов для запуска популярных приложений.&lt;/p&gt;
&lt;p&gt;Установка docker под разные платформы описана здесь:&amp;nbsp;&lt;a title="EN + Windows" href="https://docs.docker.com/docker-for-windows/install/#where-to-go-next" target="_blank" rel="noopener noreferrer"&gt;EN + Windows&lt;/a&gt;&amp;nbsp;&lt;a title="RU + Ubuntu" href="https://www.digitalocean.com/community/tutorials/docker-ubuntu-16-04-ru" target="_blank" rel="noopener noreferrer"&gt;RU + Ubuntu&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Запустим MS SQL Server в контейнере. Для этого нам нужно будет выполнить следующую команду:&lt;/p&gt;
&lt;pre class="language-bash"&gt;&lt;code&gt;docker run --name mssql -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=givemedata' -p 1433:1433 -d --restart unless-stopped -v mssql-data:/var/opt/mssql/data -v C://docker//backups:/var/backups microsoft/mssql-server-linux:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Отмечу, что "C://docker//backups" - путь директории, которая будет общей с вашим контейнером, куда вы можете забросить нужные вам резервные копии баз данных для восстановления или откуда вы можете забрать созданные копии баз.&lt;/p&gt;
&lt;p&gt;Запустим RabbitMQ:&lt;/p&gt;
&lt;pre class="language-bash"&gt;&lt;code&gt;docker run --name rabbit -d --restart unless-stopped -p 8004:15672 -p 8001:5672 -v rabbitmq:/var/lib/rabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=pass rabbitmq:3-management&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;"-p 8004:15672" - это указание связи порта, который будет в вашей операционной системе и на который будет направляться запрос внутрь контейнера. Порт можно изменить с 8004 на любой свободный\желаемый.&lt;/p&gt;
&lt;p&gt;Для запуска "пар" контейнеров можно использовать команду "&lt;strong&gt;docker-compose up -d&lt;/strong&gt;". Отличие в том, что нам необходимо описать docker compose файл с указанием необходимых сервисов и их параметров. Приведу docker compose файл для запуска postgresql + pgAdmin. Отмечу, что разработчики pgAdmin в последней версии (4 v3.0)&amp;nbsp;добавили официальный образ docker. Для запуска рекомендую создать отдельную директорию и в нее поместить файл&amp;nbsp;docker-compose.yml со следующим содержанием.&lt;/p&gt;
&lt;p&gt;Docker compose для Postgres SQL + pgAdmin 4:&lt;/p&gt;
&lt;pre class="language-yaml"&gt;&lt;code&gt;version: '3.1'

services:
    db:
        image: postgres
        volumes:
            - db-data:/var/lib/postgresql/data
            - C://docker//backups:/var/lib/postgresql/backups
        ports:
            - 8002:5432
        environment:
            - POSTGRES_PASSWORD=givemedata
        restart: unless-stopped
        networks:
            - net

    admin:
        image: dpage/pgadmin4
        volumes:
            - admin-data:/var/lib/pgadmin
            - C://docker//backups:/var/lib/pgadmin/storage/admin/backups
        ports:
            - 8003:80
        environment:
            - PGADMIN_DEFAULT_EMAIL=admin
            - PGADMIN_DEFAULT_PASSWORD=admin
        restart: unless-stopped
        networks:
            - net

volumes:
    db-data:
    admin-data:

networks: 
    net:&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Все что остается, выполнить команду&amp;nbsp;&lt;strong&gt;docker-compose up -d&lt;/strong&gt; в доступном CLI инструменте (powershell/shell/bash).&lt;/p&gt;
&lt;p&gt;Приведу&amp;nbsp;репозиторий &lt;a title="docker-infrastructure" href="https://github.com/d2funlife/docker-infrastructure" target="_blank" rel="noopener noreferrer"&gt;docker-infrastructure&lt;/a&gt;, где можно будет забрать остальные docker compose файлы, файл с общими командами для запуска инструментов.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title="PostgreSQL + pgAdmin 4" href="https://raw.githubusercontent.com/d2funlife/docker-infrastructure/master/postgres/docker-compose.yml" target="_blank" rel="noopener noreferrer"&gt;PostgreSQL + pgAdmin 4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a title="MongoDB + Mongo express" href="https://raw.githubusercontent.com/d2funlife/docker-infrastructure/master/mongo/docker-compose.yml" target="_blank" rel="noopener noreferrer"&gt;MongoDB + Mongo express&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a title="TeamCity + TeamCity agent" href="https://raw.githubusercontent.com/d2funlife/docker-infrastructure/master/teamcity/docker-compose.yml" target="_blank" rel="noopener noreferrer"&gt;TeamCity + TeamCity agent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a title="Elasticsearch + dejavu" href="https://raw.githubusercontent.com/d2funlife/docker-infrastructure/master/elasticsearch/docker-compose.yml" target="_blank" rel="noopener noreferrer"&gt;Elasticsearch + dejavu&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Для более наглядного управления и понимания состояния всего, что связано с docker, рекомендую использовать &lt;a title="Portainer" href="https://hub.docker.com/r/portainer/portainer/" target="_blank" rel="noopener noreferrer"&gt;Portainer&lt;/a&gt;. Он имеет очень большой функционал и позволяет уйти от командной строки + отобразить все, что происходит и хранится в вашем docker. Особенно полезно, если места на диске мало, а неиспользуемых образов скопилось&amp;nbsp;много. C Portainer можно быстро выбрать и удалить не нужные образы, сохранить место на диске.&lt;/p&gt;
&lt;p&gt;&lt;a class="b-single-post__gallery" href="#gallery-popup"&gt; &lt;img src="images/BlUoWMi.png" alt="Главный экран" /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;div id="gallery-popup" class="b-gallery hidden"&gt;&lt;a class="b-gallery__link" title="Главный экран" href="images/BlUoWMi.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Шаблоны приложений" href="images/JqLGO8z.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Стэки" href="images/0DyxK89.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Сервисы" href="images/kWjhviq.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Образы" href="images/mMmmeVq.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Контейнеры" href="images/QX72aZ0.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Кластер" href="images/9MYUxmh.png"&gt;link&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;Запуск Portainer:&lt;/p&gt;
&lt;pre class="language-bash"&gt;&lt;code&gt;docker run --name portainer -d --restart unless-stopped -p 8000:9000 -v portainer:/data -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;В целом, это лишь трейлер к данному инструменту, популярность которого растет очень большими темпами. Docker позволяет решить множество потребностей в разработке приложений, поэтому считаю, что изучать его более глубоко необходимо, а данная статья пусть будет затравочкой и быстрыми рецептами для старта изучения.&lt;/p&gt;</description>
  <author>d2funlife@gmail.com (Danil Pavlov)</author>
  <category>docker</category>
  <guid isPermaLink="false">ccd2804f-14ea-4dd3-9e6f-8964a49d7572</guid>
  <pubDate>Sun, 10 Jun 2018 18:34:54 GMT</pubDate>
</item>
<item>
  <title>ASP.NET MVC жизненный цикл запроса</title>
  <link>https://d2funlife.com/asp-net-mvc-request-life-cycle</link>
  <description>&lt;p&gt;Как бы смешным не казалось, но эту статью можно начать философским вбросом про смысл жизни, куда мы движемся, и то, что движемся по спирали, и все события повторяются. Ведь посудите сами &amp;ndash; у всего есть жизненный цикл, многие процессы имеют некую "циклическую" составляющую. И я хочу вам, мои друзья, рассказать про тот самый смысл жизни и типичную жизнь тривиального запроса в ASP.NET MVC приложении. Основой классического ASP.NET MVC приложения является IIS он и является той самой отправной точкой на длинном пути запроса "от появления на сервере до отправки ответа". Картина прихожей для запросов выглядит следующим образом:&lt;/p&gt;
&lt;p&gt;&lt;img class="lozad" alt="Жизненный цикл запроса в IIS" width="870" height="824" src="https://d2funlife.com/images/wknf5s4.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Подробное описание устройства и взаимодействия всех частей цепочки описано в статье на &lt;a title="хабрахабре" href="https://goo.gl/BgLvV5" target="_blank" rel="noopener noreferrer"&gt;хабрахабре&lt;/a&gt;. Отдельно хочу отметить список источников, приведенный в статье. Цель данной статьи рассказать, что именно происходит между 6м и 7м шагом на данной схеме, рассказать про устройство и всю цепочку работы ASP.NET MVC приложения. Путешествие с запросом происходит по следующей цепочке действий:&lt;/p&gt;
&lt;p&gt;&lt;img class="lozad" alt="жизненный цикл запроса в HTTP приложении" width="870" height="933" src="https://d2funlife.com/images/CrvGuYY.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Здесь видно два "острова", которые соединены мостами. Один из них &amp;mdash; это платформа HTTP приложения, если предметнее: открыв Global.asax, мы увидим, что ключевой класс, где происходит конфигурация нашего приложения, наследуется от &lt;a title="HttpApllication" href="https://goo.gl/WDCEXR" target="_blank" rel="noopener noreferrer"&gt;HttpApllication&lt;/a&gt;&amp;nbsp;(System.Web). HttpApplication и дает базу для нашего приложения, в том числе и события из списка выше, которые можно использовать из Global.asax.cs, либо подключаться к событиям, реализуя свой HTTP модуль (&lt;a title="IHttpModule" href="https://goo.gl/xNg7wz" target="_blank" rel="noopener noreferrer"&gt;IHttpModule&lt;/a&gt;). Пример всех этих точек расширения HttpApplication можно посмотреть тут - &lt;a title="исходный код" href="https://goo.gl/gkan7r" target="_blank" rel="noopener noreferrer"&gt;исходный код&lt;/a&gt;. Используя данный пример, можно в режиме отладки пройти все шаги один за одним и проверить правильность порядка и существование точки расширения в принципе. Уровень HTTP приложения и интеграция его с MVC приложением будет рассмотрен в отдельной статье. В данной статье будет рассмотрен исключительно второй "остров" нашей карты, с его остановками в виде маршрутизации (routing), инициализации контроллера (controller initialization), выполнением действия контроллера (action excecution) и выполнением результата действия (result execution).&lt;/p&gt;
&lt;h2&gt;MVC цикл&lt;/h2&gt;
&lt;p&gt;Ранее был представлен общий план цикла запроса. Более детальный цикл уровня MVC приложения выглядит следующим образом:&lt;/p&gt;
&lt;p&gt;&lt;img class="lozad" alt="жизненный цикл запроса в ASP.NET MVC приложении" width="870" height="736" src="https://d2funlife.com/images/kIEyWmJ.jpg" /&gt;&lt;/p&gt;
&lt;h3&gt;Routing (Маршрутизация)&lt;/h3&gt;
&lt;p&gt;Первой остановкой запроса является подсистема маршрутизации, ведь необходимо определить, какой цели должен достигнуть наш запрос, какое действие и какого контроллера является для него желанным, а может быть запросу вовсе нужен статический файл, а не контроллер, в любом случае необходимо получить некую пару "ключ &amp;ndash; значение" для маршрута и единицы, которая будет его обрабатывать дальше. Эта единица называется "обработчик запроса" (route handler) и ее задача вернуть объект &lt;a title="HttpHandler" href="https://goo.gl/DVGt6e" target="_blank" rel="noopener noreferrer"&gt;HttpHandler&lt;/a&gt;, который будет выполнять всю дальнейшую цепочку обработки запроса. Неопытный программист задастся вопросом "какой route handler? Ведь мы при старте приложения просто указываем общий шаблон в текстовом варианте и все. Там нет указания каких-либо route handler, только имя контроллера и действия по умолчанию". И здесь тот момент, когда оптимизации и улучшения сыграли злую шутку с программистом и он не знает внутренних тонкостей из-за того, что их скрыли от него. Пример объявления маршрутизации по умолчанию:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;// пример регистрации маршрута по умолчанию
routes.MapRoute(name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Все красиво, все просто: передаем имя маршрута, шаблон url запроса, анонимный объект, который указывает названия контроллера и действия по умолчанию. Но MapRoute &amp;ndash; это расширяющий метод, который скрывает за собой реальную логику добавления маршрута в таблицу маршрутизации.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//метод расширения для добавления маршрута в таблицу
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces)
{
    if (routes == null)
    {
        throw new ArgumentNullException("routes");
    }
    if (url == null)
    {
        throw new ArgumentNullException("url");
    }

    Route route = new Route(url, new MvcRouteHandler())
    {
        Defaults = CreateRouteValueDictionaryUncached(defaults),
        Constraints = CreateRouteValueDictionaryUncached(constraints),
        DataTokens = new RouteValueDictionary()
    };

    ConstraintValidation.Validate(route);

    if ((namespaces != null) &amp;amp;&amp;amp; (namespaces.Length &amp;gt; 0))
    {
        route.DataTokens[RouteDataTokenKeys.Namespaces] = namespaces;
    }

    routes.Add(name, route);

    return route;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Пример добавления маршрута в таблицу маршрутизации без использования расширяющих методов:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Добавление маршрута в таблицу маршрутизации без расширяющих методов
var myRoute = new Route("{controller}/{action}/{id}",
              new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" }, { "id", "1" } },
              new MvcRouteHandler());
routes.Add(myRoute);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Вот тут уже более наглядно будет видно, что за route handler и как они вообще попадают в маршрутизацию. Сам класс обработчика маршрута реализует интерфейс &lt;a title="IRouteHandler" href="https://goo.gl/FFy1yZ" target="_blank" rel="noopener noreferrer"&gt;IRouteHandler&lt;/a&gt; у которого есть единственный метод GetHttpHandler, возвращающий HttpHandler. HttpHandler &amp;ndash; это тот объект который берет на себя ответственность за генерацию ответа на HTTP запрос, класс этого объекта реализует интерфейс IHttpHandler. Этот интерфейс содержит свойство IsReusable и метод ProcessRequest. IsReusable &amp;nbsp;- флаг, указывающий может ли другой запрос использовать этот же обработчик, иными словами нужно ли ему создавать каждый раз новый экземпляр или можно создать один раз и использовать его для всех последующих запросов. ProcessRequest &amp;ndash; метод, который выполняет обработку запроса. По умолчанию используется &lt;a title="MvcRouteHandler" href="https://goo.gl/ZxW4Wi" target="_blank" rel="noopener noreferrer"&gt;MvcRouteHandler&lt;/a&gt; и &lt;a title="MvcHandler" href="https://goo.gl/XRwb9k" target="_blank" rel="noopener noreferrer"&gt;MvcHandler&lt;/a&gt;. Исходный код открывает все тонкости и оптимизации, которые заложили архитекторы. В реальной жизни очень мала вероятность , что придется создавать собственный HttpHandler, так как стандартные средства ASP.NET MVC покрывают большинство задач. Приведу пример маленького HttpHandler`а, который поздоровается с вами:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Пример обработчика
public class HelloHandler : IHttpHandler
{
    public bool IsReusable
    {
        get { return false; }
    }
    public void ProcessRequest(HttpContext context)
    {
        context.Response.Write("&amp;lt;p&amp;gt;Hello world&amp;lt;/p&amp;gt;");
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Получить данный обработчик мы можем только из RouteHandler, поэтому сделаем его и зарегистрируем по адресу "home/hello".&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Обработчик маршрута
public class HelloRouteHandler : IRouteHandler
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new HelloHandler();
    }
}

//Регистрация обработчика маршрута на определенный урл
routes.Add(new Route("home/hello", new HelloRouteHandler()));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Таким образом мы получим приветствие, которого ждали, но это лирическое отступление с демонстрацией точки расширения в ASP.NET MVC, где вы можете управлять процессом обработки запроса. В стандартном приложении мы получаем &lt;a title="MvcRouteHandler" href="https://goo.gl/yF9rog" target="_blank" rel="noopener noreferrer"&gt;MvcRouteHandler&lt;/a&gt;, который отдает &lt;a title="MvcHandler" href="https://goo.gl/5uK7Cf" target="_blank" rel="noopener noreferrer"&gt;MvcHandler&lt;/a&gt;, а тот в свою очередь запускает нужный контроллер, нужное действие и отдает им нужные данные.&lt;/p&gt;
&lt;h3&gt;Controller initialization (Инициализация контроллера)&lt;/h3&gt;
&lt;p&gt;Как было уже сказано ранее, по умолчанию мы получаем MvcRouteHandler, он в свою очередь отдает MvcHandler. MvcHandler в методе ProcessRequest начинает процесс создания нужного контроллера и исполнение нужного действия.&amp;nbsp;&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Запуск процеса обработки запроса
private void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory)
{
    HttpContext currentContext = HttpContext.Current;
    if (currentContext != null)
    {
        bool? isRequestValidationEnabled = ValidationUtility.IsValidationEnabled(currentContext);
        if (isRequestValidationEnabled == true)
        {
            ValidationUtility.EnableDynamicValidation(currentContext);
        }
    }

    AddVersionHeader(httpContext);
    RemoveOptionalRoutingParameters();

	//Получение имени контроллера
    string controllerName = RequestContext.RouteData.GetRequiredString("controller");

	//Получение фабрики контроллера
    factory = ControllerBuilder.GetControllerFactory();
	
	//Создание объекта контроллера
    controller = factory.CreateController(RequestContext, controllerName);
    if (controller == null)
    {
        throw new InvalidOperationException(
            String.Format(
                CultureInfo.CurrentCulture,
                MvcResources.ControllerBuilder_FactoryReturnedNull,
                factory.GetType(),
                controllerName));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;В MVCHandler мы получаем объект фабрики контроллеров (controller factory), который ответственен за получение контроллера и реализует интерфейс &lt;a title="IControllerFactory" href="https://goo.gl/icTch6" target="_blank" rel="noopener noreferrer"&gt;IControllerFactory&lt;/a&gt;. По умолчанию &lt;a title="ControllerBuilder" href="https://goo.gl/3VQZLX" target="_blank" rel="noopener noreferrer"&gt;ControllerBuilder&lt;/a&gt; отдает объект класса &lt;a title="DefaultControllerFactory" href="https://goo.gl/y1e4LK" target="_blank" rel="noopener noreferrer"&gt;DefaultControllerFactory&lt;/a&gt;. Увидеть это мы можем, посмотрев исходный код:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Получение фабрики контроллеров в ControllerBuilder
public IControllerFactory GetControllerFactory()
{
    return _serviceResolver.Current;
}

//Конструктор ControllerBuilder
internal ControllerBuilder(IResolver&amp;lt;IControllerFactory&amp;gt; serviceResolver)
{
    _serviceResolver = serviceResolver ?? new SingleServiceResolver&amp;lt;IControllerFactory&amp;gt;(
                                              () =&amp;gt; _factoryThunk(),
                                              new DefaultControllerFactory { ControllerBuilder = this },
                                              "ControllerBuilder.GetControllerFactory");
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;В методе CreateController используется объект &lt;a title="IControllerActivator" href="https://goo.gl/ysU8RF" target="_blank" rel="noopener noreferrer"&gt;IControllerActivator&lt;/a&gt; в методе GetControllerInstance, такое разграничение дополнительным уровнем дает возможность использовать разрешение зависимостей при создании контроллеров.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Создания контроллера
public virtual IController CreateController(RequestContext requestContext, string controllerName)
{
    if (requestContext == null)
    {
        throw new ArgumentNullException("requestContext");
    }

    if (String.IsNullOrEmpty(controllerName) &amp;amp;&amp;amp; !requestContext.RouteData.HasDirectRouteMatch())
    {
        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName");
    }

	//Получение типа контроллера
    Type controllerType = GetControllerType(requestContext, controllerName);
	
	//Получение объекта контроллера
    IController controller = GetControllerInstance(requestContext, controllerType);
    return controller;
}

//Получение контроллера используя активатор контроллеров
protected internal virtual IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
    if (controllerType == null)
    {
        throw new HttpException(404,
                                String.Format(
                                    CultureInfo.CurrentCulture,
                                    MvcResources.DefaultControllerFactory_NoControllerFound,
                                    requestContext.HttpContext.Request.Path));
    }
    if (!typeof(IController).IsAssignableFrom(controllerType))
    {
        throw new ArgumentException(
            String.Format(
                CultureInfo.CurrentCulture,
                MvcResources.DefaultControllerFactory_TypeDoesNotSubclassControllerBase,
                controllerType),
            "controllerType");
    }
    return ControllerActivator.Create(requestContext, controllerType);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Активатор контроллера по умолчанию реализован в классе &lt;a title="DefaultControllerActivator" href="https://goo.gl/C1Pb8G" target="_blank" rel="noopener noreferrer"&gt;DefaultControllerActivator&lt;/a&gt;&amp;nbsp;и реализация получения объекта с использованием разрешения зависимостей (dependency resolver) выглядит следующим образом:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;private class DefaultControllerActivator : IControllerActivator
{
    private Func&amp;lt;IDependencyResolver&amp;gt; _resolverThunk;

    public DefaultControllerActivator()
        : this(null)
    {
    }

    public DefaultControllerActivator(IDependencyResolver resolver)
    {
        if (resolver == null)
        {
            _resolverThunk = () =&amp;gt; DependencyResolver.Current;
        }
        else
        {
            _resolverThunk = () =&amp;gt; resolver;
        }
    }

    public IController Create(RequestContext requestContext, Type controllerType)
    {
        try
        {
            return (IController)(_resolverThunk().GetService(controllerType) ?? Activator.CreateInstance(controllerType));
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(
                String.Format(
                    CultureInfo.CurrentCulture,
                    MvcResources.DefaultControllerFactory_ErrorCreatingController,
                    controllerType),
                ex);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;_resolveThunk по умолчанию получает объект класса &lt;a title="DefaultDependencyResolver" href="https://goo.gl/zkRKxM" target="_blank" rel="noopener noreferrer"&gt;DefaultDependencyResolver&lt;/a&gt;, который является оберткой для System.Activator, но если вы подключите собственный dependency resolver, то использоваться будет именно он. Используя различные контейнеры инверсии зависимостей (&lt;a title="IoC containers" href="https://goo.gl/h8X1PQ" target="_blank" rel="noopener noreferrer"&gt;IoC containers&lt;/a&gt;) + внедрение зависимостей (DI), мы расширяем наши контроллеры и можем дополнительно управлять жизненным циклом объектов. Интеграция контейнера присходит посредством класса &lt;a title="DependencyResolver" href="https://goo.gl/8FuWbQ" target="_blank" rel="noopener noreferrer"&gt;DependencyResolver&lt;/a&gt;, он статичен и имеет метод SetResolver, где и будет установлен наш IoC контейнер, который, в свою очередь, будет получен в активаторе контроллера в конструкторе (тот самый _resolveThunk). И вот, когда контроллер получен, MvcHandler может вызывать метод Execute у контроллера, передав объект &lt;a title="RequestContext" href="https://goo.gl/f7XE6y" target="_blank" rel="noopener noreferrer"&gt;RequestContext&lt;/a&gt; (контекст запроса) в качестве аргумента. Схематически, в сжатом виде, инициализация контроллера выглядит следующим образом:&lt;/p&gt;
&lt;p&gt;&lt;img class="lozad" alt="Выполнение контроллера MVC" width="870" height="226" src="https://d2funlife.com/images/8yObuW6.jpg" /&gt;&lt;/p&gt;
&lt;h3&gt;Action execution (Выполнение действия)&lt;/h3&gt;
&lt;p&gt;При выполнении действий контроллера, так же выполняются фильтры. Фильтры &amp;mdash; это точки расширения, которые позволяют обогатить код дополнительной логикой. Существуют следующие типы фильтров authentication filters (фильтры аутентификации), authorization filters (фильтры авторизации), action filters (фильтры действий), result filters (фильтры результата действия), exception filters (фильтры исключений). Фильтры действий реализуют интерфейс &lt;a title="IActionFilter" href="https://goo.gl/g81jLh" target="_blank" rel="noopener noreferrer"&gt;IActionFilter&lt;/a&gt;, в котором определены два метода OnActionExecuting и OnActionExecuted, соответственно они выполняются перед выполнением действия и после выполнения действия. При выполнении цикла запроса фильтры исполняются в следующем порядке:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Фильтры аутентификации&lt;/li&gt;
&lt;li&gt;Фильтры авторизации&lt;/li&gt;
&lt;li&gt;Фильтры действий&lt;/li&gt;
&lt;li&gt;Фильтры результата действия&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Фильтры исключений выполняются на любом этапе, ведь ошибки нужно отлавливать и обрабатывать по всему приложению. &lt;br /&gt;В исходном коде класса Controller мы видим, что &lt;a title="Controller" href="https://goo.gl/pZ1Fsy" target="_blank" rel="noopener noreferrer"&gt;Controller&lt;/a&gt; наследует &lt;a title="ControllerBase" href="https://goo.gl/kvkWUW" target="_blank" rel="noopener noreferrer"&gt;ControllerBase&lt;/a&gt;, в ControllerBase реализован метод Execute и в нем уже вызывается ExecuteCore. ExecuteCore в свою очередь реализован в классе Controller, получив имя действия для выполнения, запускает его на выполнение с исполнением дополнительного уровня в виде &lt;a title="ActionInvoker" href="https://goo.gl/71nzQn" target="_blank" rel="noopener noreferrer"&gt;ActionInvoker&lt;/a&gt;. ActionInvocker в себе инкапсулирует выполнение дополнительных действий (привязку данных, выполнение всех фильтров, запуск на исполнение результата действия) и выполнение самого действия. Все происходит в следующем порядке внутри метода InvokeAction:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Фильтры аутентификации (метод InvokeAuthenticationFilters)&lt;/li&gt;
&lt;li&gt;Фильтры авторизации (метод InvokeAuthorizationFilters)&lt;/li&gt;
&lt;li&gt;Получение значений параметров, если формальнее "привязка модели" (метод GetParameterValues)&lt;/li&gt;
&lt;li&gt;Выполнение действия и фильтров действия (метод InvokeActionMethodWithFilters)&lt;/li&gt;
&lt;li&gt;Выполнение результата действия (метод InvokeActionResultWithFilters)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Выполнение действия контроллера
public virtual bool InvokeAction(ControllerContext controllerContext, string actionName)
{
    if (controllerContext == null)
    {
        throw new ArgumentNullException("controllerContext");
    }

    Contract.Assert(controllerContext.RouteData != null);
    if (String.IsNullOrEmpty(actionName) &amp;amp;&amp;amp; !controllerContext.RouteData.HasDirectRouteMatch())
    {
        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName");
    }

    ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext);
    ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName);

    if (actionDescriptor != null)
    {
        FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor);

        try
        {
            AuthenticationContext authenticationContext = InvokeAuthenticationFilters(controllerContext, filterInfo.AuthenticationFilters, actionDescriptor);

            if (authenticationContext.Result != null)
            {
                AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge(
                    controllerContext, filterInfo.AuthenticationFilters, actionDescriptor,
                    authenticationContext.Result);
					
                InvokeActionResult(controllerContext, challengeContext.Result ?? authenticationContext.Result);
            }
            else
            {
                AuthorizationContext authorizationContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor);
                if (authorizationContext.Result != null)
                {
                    AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge(
                        controllerContext, filterInfo.AuthenticationFilters, actionDescriptor,
                        authorizationContext.Result);
						
                    InvokeActionResult(controllerContext, challengeContext.Result ?? authorizationContext.Result);
                }
                else
                {
                    if (controllerContext.Controller.ValidateRequest)
                    {
                        ValidateRequest(controllerContext);
                    }

                    IDictionary&amp;lt;string, object&amp;gt; parameters = GetParameterValues(controllerContext, actionDescriptor);
                    ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters);

                    AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge(
                        controllerContext, filterInfo.AuthenticationFilters, actionDescriptor,
                        postActionContext.Result);
						
                    InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters,
                        challengeContext.Result ?? postActionContext.Result);
                }
            }
        }
        catch (ThreadAbortException)            
        {
            throw;
        }
        catch (Exception ex)
        {
            ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex);
            if (!exceptionContext.ExceptionHandled)
            {
                throw;
            }
            InvokeActionResult(controllerContext, exceptionContext.Result);
        }
		
        return true;
    }

    return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ActionInoker определяет, какое действие необходимо запустить, для этого берутся за ключевые фильтры: имя действия, параметры действия, атрибуты &lt;a title="ActionMethodSelectorAtribute" href="https://goo.gl/fVVW4f" target="_blank" rel="noopener noreferrer"&gt;ActionMethodSelectorAtribute&lt;/a&gt; (если добавлены). Такими атрибутами являются &lt;a title="HttpGet" href="https://goo.gl/BUKEmL" target="_blank" rel="noopener noreferrer"&gt;HttpGet&lt;/a&gt;, &lt;a title="HttpPost" href="https://goo.gl/p9Vfjj" target="_blank" rel="noopener noreferrer"&gt;HttpPost&lt;/a&gt;, к примеру, вы можете добавить свой, для этого просто необходимо создать класс, который наследует ActionMethodSelectorAtribute и переопределяет метод IsValidForRequest. Пример атрибута, который уточнит выбор действия, определив является ли клиент мобильным браузером или нет:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Дополнительный параметр выборки действия
public class IsMobile : ActionMethodSelectorAttribute
{
    public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
    {
        return controllerContext.HttpContext.Request.Browser.IsMobileDevice;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Если при выборке необходимого действия было найдено более одного или ни одного, то будет ошибка выполнения, ведь явное одно действие не было выбрано. Отдельно хочу отметить, что этап привязки данных к модели описан в статье ASP.NET MVC привязка данных, тут можно подчерпнуть информацию и посмотреть примеры, как мы можем влиять на привязку модели.&lt;/p&gt;
&lt;h3&gt;Result execution (Выполнение результата действия)&lt;/h3&gt;
&lt;p&gt;В результате выполнения всей цепочки мы получаем объект ActionResult, который вместе с фильтрами результата выполняется в методе InvokeActionResultWithFilters. Данный метод вызывает InvokeActionResultFilterRecursive в котором рекурсивно выполняются фильтры результата и выполняется сам результат. Что значит выполняется сам результат? Объект &lt;a title="ActionResult" href="https://goo.gl/JN5tvf" target="_blank" rel="noopener noreferrer"&gt;ActionResult&lt;/a&gt; записывает свои данные в ответ запросу. Если действие возвращает JsonResult, то в ответ будет записан сериализованный в json объект, который отдан в результат действия. Если же результатом нашего действия является представление или частичное представление, то будет выполнена следующая цепочка действий:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public override void ExecuteResult(ControllerContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException("context");
    }
    if (String.IsNullOrEmpty(ViewName))
    {
        ViewName = context.RouteData.GetRequiredString("action");
    }

    ViewEngineResult result = null;

    if (View == null)
    {
        result = FindView(context);
        View = result.View;
    }

    TextWriter writer = context.HttpContext.Response.Output;
    ViewContext viewContext = new ViewContext(context, View, ViewData, TempData, writer);
    View.Render(viewContext, writer);

    if (result != null)
    {
        result.ViewEngine.ReleaseView(context, View);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Как видно, тут происходит следующее:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;В базовом классе ViewResultBase вызывается метод ExecuteResult&lt;/li&gt;
&lt;li&gt;Происходит поиск представления для рендера (реализован в классе &lt;a title="ViewResult" href="https://goo.gl/NxkAJ5" target="_blank" rel="noopener noreferrer"&gt;ViewResult&lt;/a&gt;) по всем движкам представлений (ViewEngines)&lt;/li&gt;
&lt;li&gt;Найденное представление отправляется на рендер&lt;/li&gt;
&lt;li&gt;Результат рендера записывается в ответ на запрос&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Есть в этой цепочке один нюанс, мы можем определить свой ViewEngine с путями поиска представлений и добавить его к списку стандартных. Таким образом мы сможем расширить правила поиска представлений. Ключевой момент: при поиске представления, проходит цикл по всем зарегистрированным ViewEngines и первый, который сможет найти представление возвращает результат, то есть для указания более частного случая необходимо свой ViewEngine регистрировать в списке на более ранних позициях.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//код движка представления
public class ThemeViewEngine : RazorViewEngine
{
    public ThemeViewEngine()
    {
        ViewLocationFormats = new string[] { "~/Themes/{1}/{0}.cshtml" };
        PartialViewLocationFormats = new string[] { "~/Themes/{1}/{0}.cshtml" };
    }
}

//регистрация движка представления
protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
    ViewEngines.Engines.Insert(0, new ThemeViewEngine());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Данный пример объясняет цепочку вызовов, когда результатом является представление, если у нас более простой ответ, к примеру, ContentResult, то вся цепочка выглядит куда проще:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public class ContentResult : ActionResult
{
    public string Content { get; set; }

    public Encoding ContentEncoding { get; set; }

    public string ContentType { get; set; }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }

        HttpResponseBase response = context.HttpContext.Response;

        if (!String.IsNullOrEmpty(ContentType))
        {
            response.ContentType = ContentType;
        }
        if (ContentEncoding != null)
        {
            response.ContentEncoding = ContentEncoding;
        }
        if (Content != null)
        {
            response.Write(Content);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Вот так мы и подошли к концу цикла запроса в MVC, в следующей статье завершим детальный обзор цикла HTTP приложения, получив полную картину, как работает само приложение, как обрабатываются запросы от пользователей. Если вы захотите сами побродить по просторам MVC приложения, то это можно сделать, скачав код тут : &lt;a title="https://aspnetwebstack.codeplex.com/" href="https://aspnetwebstack.codeplex.com/" target="_blank" rel="noopener noreferrer"&gt;https://aspnetwebstack.codeplex.com&lt;/a&gt;&amp;nbsp;или тут : &lt;a title="https://github.com/ASP-NET-MVC/aspnetwebstack" href="https://github.com/ASP-NET-MVC/aspnetwebstack" target="_blank" rel="noopener noreferrer"&gt;https://github.com/ASP-NET-MVC/aspnetwebstack&lt;/a&gt;. Сodeplex уходит в прощальную гастроль, но официальный репозиторий пока только там, на github не завезли еще, только не официальное зеркало. Так же посмотрите подробные диаграмы жизненного цикла от Microsoft&amp;nbsp;&lt;a title="Lifecycle of an ASP.NET MVC 5 Application" href="https://goo.gl/o7Rd2b" target="_blank" rel="noopener noreferrer"&gt;Lifecycle of an ASP.NET MVC 5 Application&lt;/a&gt;&lt;/p&gt;</description>
  <author>d2funlife@gmail.com (Danil Pavlov)</author>
  <category>asp.net mvc</category>
  <guid isPermaLink="false">1046d9f8-e218-4efd-a442-86e04e468337</guid>
  <pubDate>Wed, 20 Sep 2017 20:05:25 GMT</pubDate>
</item>
<item>
  <title>ASP.NET Core обработка ошибок</title>
  <link>https://d2funlife.com/asp-net-core-exception</link>
  <description>&lt;blockquote&gt;Единственная настоящая ошибка &amp;mdash; не исправлять своих прошлых ошибок. Конфуций.&lt;/blockquote&gt;
&lt;p&gt;Ошибки в приложении неизбежны, но есть те, которые мы "поджидаем", а есть зайцы, которые проскакивают сквозь дыру в заборе. Как сделать так, чтоб незамеченным не остался ни один заяц, и чтоб пользователь был доволен всегда будет рассказано в этой статье.&lt;/p&gt;
&lt;p&gt;Для обработки ошибки выделим три ключевых шага, которые необходимо выполнить:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Перехватить ошибку.&lt;/li&gt;
&lt;li&gt;Обработать ошибку (добавить информацию в логи или установить сообщение об ошибке пользователю, выполнить обработку ошибочной операции).&lt;/li&gt;
&lt;li&gt;Отобразить приемлемый результат пользователю.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Ключевой особенностью ASP.NET Core MVC приложений, по сравнению с ASP.NET MVC, является то, что в основу архитекторы заложили принцип объединения в цепочку отдельных функциональных частей (middleware).&lt;/p&gt;
&lt;p&gt;&lt;img src="images/tVC2ddP.jpg" alt="Middleware цепочка" width="870" height="463" /&gt;&lt;/p&gt;
&lt;p&gt;Соответственно MVC является одним из элементов данной цепочки, и запрос проходит по данной цепочке до обработчика и обратно. Каждый элемент вызывает последующий, если тот существует. И Каждый элемент возвращает результат вызова следующего. А это дает следующий факт - обработать ошибку можно, используя следующие точки влияния:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Try catch в своем коде.&lt;/li&gt;
&lt;li&gt;Фильтры исключений MVC.&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Try catch&amp;rdquo; на уровне middleware.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Обработка на уровне своего кода предельно ясна и ее рассматривать мы не будем. Для MVC мы можем использовать фильтр исключения. Для этого нам нужно создать фильтр исключения и зарегистрировать его для всех действий контроллеров. Код фильтра:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Класс фильтра ошибок
public class GlobalExceptionFilter : IExceptionFilter
{
    private readonly ILogger&amp;lt;GlobalExceptionFilter&amp;gt; _logger;

    public GlobalExceptionFilter(ILogger&amp;lt;GlobalExceptionFilter&amp;gt; logger)
    {
        _logger = logger;
    }
    public void OnException(ExceptionContext context)
    {
        _logger.LogError(0, context.Exception, context.Exception.Message);

        context.ExceptionHandled = true;
        context.Result = new ViewResult {ViewName = "Error-500"};
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;В фильтре мы логируем нашу ошибку и возвращаем страницу ошибки сервера "Error-500". Что бы добавить фильтр, мы должны зарегистрировать его в глобальные фильтры MVC, то есть фильтры, которые выполняются для всех действий всех контроллеров.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Добавляем глобальный фильтр, который будет получать информацию об ошибке
services.AddMvc(options =&amp;gt;
{
    options.Filters.Add(typeof(GlobalExceptionFilter));
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;В данном случае мы обработаем все наши Exception и наследуемые от него, но как же отобразить пользователю красивое окно 404, к примеру, когда он пытался посмотреть ресурс, которого нет. Для этого мы добавим обработку HTTP кодов. В компоненте StatusCodePagesMiddleware есть так же методы UseStatusCodePages, UseStatusCodePagesWithRedirects и UseStatusCodePagesWithReExecute. Объясню разницу между 3мя методами.&lt;/p&gt;
&lt;p&gt;UseStatusCodePages &amp;ndash; использует текстовое представление HTTP кода ошибки, и все что вы можете изменить, это формат и текст сообщения, который увидете в браузере.&lt;/p&gt;
&lt;p&gt;&lt;img src="images/RUKFskX.jpg" alt="Пример страницы с ошибкой" width="685" height="161" /&gt;&lt;/p&gt;
&lt;p&gt;UseStatusCodePagesWithRedirects &amp;ndash; может перенаправить вас на страницу ошибки, но ключевое слово перенаправит, то есть браузер, пытаясь получить не существующий ресурс, получит HTTP код 302 (ресурс перемещен) и будет выполнено перенаправление на страницу ошибки. Но по факту браузер будет думать, что ресурс существует, просто перемещен на страницу ошибки.&lt;/p&gt;
&lt;p&gt;UseStatusCodePagesWithReExecute &amp;ndash; самый оптимальный вариант, так как может вернуть и HTTP код и страницу ошибки в одном флаконе. Для использования мы зарегистрируем компонент:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Добавляем разденеие обработки ошибок
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseBrowserLink();
}
else
{
    app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;В строчном параметре {0} будет передано числовое отображение HTTP кода ошибки, с которым мы можем оперировать. Код контроллера с перенаправлением на страницы ошибок:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Контроллер и действие обработки ошибки
public class StatusCodeController : Controller
{
    private Int32[] _availableCodes = new Int32[] { 404, 500 };

    [Route("/StatusCode/{statusCode}")]
    public IActionResult Index(Int32 statusCode)
    {
        if (!_availableCodes.Contains(statusCode))
            statusCode = 500;
        return View($"Error-{statusCode}");
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;И последняя точка для обработки исключений &amp;ndash; обработчик Middleware. Для чего это может быть нужно? Если у нас не используется MVC, а мы написали свой middleware для обработки запроса, то функциональность фильтров из MVC нам не будет доступна, поэтому и используем отдельный middleware для обработки ошибок:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Middleware для обработки ошибок
public sealed class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger&amp;lt;ExceptionHandlerMiddleware&amp;gt; _logger;
    public ExceptionHandlerMiddleware(RequestDelegate next, ILogger&amp;lt;ExceptionHandlerMiddleware&amp;gt; logger)
    {
        _next = next;
        _logger = logger;
    }
    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception exception)
        {
            try
            {
                //Обработка ошибки, к примеру, просто можем логировать сообщение ошибки
            }
            catch (Exception innerException)
            {
                _logger.LogError(0, innerException, "Ошибка обработки исключения");
            }
            // Если в коде обработки ошибки мы снова получили ошибку, то пробрасываем ее выше по цепочке Middleware
            throw;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Регистрируем наш middleware:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;app.UseMiddleware&amp;lt;ExceptionHandlerMiddleware&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ключевой момент &amp;ndash; обработчик должен быть зарегистрирован раньше всех остальных элементов цепочки, потому как, если произойдет исключение в цепочке до обработчика, то он не будет выполнятся и информацию об ошибке не получит. В ASP.NET Core важен порядок регистрации элементов middleware. Ну и самый простой вариант, это, не создавая свой middleware, обернуть все регистрации в try catch и обработать ошибку. Принцип работы тот же, только менее красиво.&lt;/p&gt;</description>
  <author>d2funlife@gmail.com (Danil Pavlov)</author>
  <category>asp.net core</category>
  <guid isPermaLink="false">ab7fd85d-858b-4c23-a58c-6ea1bb85e0a7</guid>
  <pubDate>Sun, 27 Aug 2017 17:00:57 GMT</pubDate>
</item>
<item>
  <title>Windows 10 Mobile жив или того? Точка зрения разработчика</title>
  <link>https://d2funlife.com/win10m-dead</link>
  <description>&lt;p&gt;Хотелось бы поделиться своими мыслями по поводу мобильной платформы от Microsoft и что происходит сейчас, чего ждать дальше. Все описанное будет с точки зрения разработчика с небольшими углублениями в платформу и разъяснениями некоторых высказываний от официальных лиц.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Кто ты такой и почему я должен вообще это дальше читать?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Я разработчик на платформе .NET и имею определенный опыт в разработке Windows Phone приложений. Игру, которую сделали мы с моей женой можно посмотреть по ссылке &lt;strong&gt;: &lt;/strong&gt; &lt;a title="Магазин" href="https://goo.gl/XdGkSE" target="_blank" rel="noopener noreferrer"&gt;Магазин&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Общение с мобильными операционными системами из разряда &amp;ldquo;смартфон&amp;rdquo; у меня началось с далекого Symbian дальше у меня был Windows Phone 8.0... иии вот я был приверженцем Windows как мобильной, так и пк версии. В целом, полностью мое рабочее пространство составляет инфраструктура от Microsoft и сейчас.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Так что там с &lt;/strong&gt; &lt;strong&gt;Win&lt;/strong&gt; &lt;strong&gt;дой? &lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Мобильная OS была отличной базой и задумкой от Microsoft, но все пошло не так. Операционная система позиционировала себя как наиболее стабильная и производительная не зависимо от железа телефона. Можно купить бюджетный телефон или флагман и получить различия исключительно в железных возможностях, качестве материалов, камеры система же себя вела идентично круто и быстро. И это меня безумно радовало и давало надежду на успешное развитие платформы. Посудите сами: платформа не имеет взлома, а приложения если и будут идти в телефоны пользователей, то через официальный магазин с попутным отчислением денег разработчикам. Производительность отличная на разных девайсах - к сравнению я видел, как люди проверяли тупит или нет Android листая главный экран с иконками. Отличный дизайн системы, да и вообще задания тенденции всем остальным платформам по дизайну. Да-да именно Windows 8 задала направление к metro (тогда это называлось так) дизайну и оформлению приложений с использованием "плоского" дизайна, красивых шрифтов и логичного расположения элементов. Порог вхождения в разработку крайне мал и простое приложение можно сделать чуть ли не за 2 часа с минимальными знаниями C# и XAML. Возможности для разработки приложений API Windows дает достаточные для большинства приложений. НО... шло время, а развитие было, но как-то все сдержано и по чуть-чуть. Прям английский лорд, который сдерживается в эмоциях и выражениях. И я, как маленький ребенок перед новым годом, сидел и ждал каждую весну конференцию Build от Microsoft, чтоб увидеть, что же будет нового, чем же будет жить следующий год все сообщество .NET разработчиков. Но в этом и прошлом году произошли самое лучшее событие с Windows платформой и самое худшее, как по мне. Лучшее - объединение Windows Mobile\Phone и полноценной Windows (многие и сейчас не знают, как же на самом деле Phone\Mobile). Что это дает? Можно написать приложение, которое работает одинаково на телефоне и на компьютере. И это очень круто, потому как открываются возможности очень тесной интеграции своего пк с телефоном, к примеру: писал текст\сидел в браузере\слушал музыку и перешел на телефон, а у тебя уже есть все то же самое, как будто просто поменялся размер экрана и все. Я очень рад был этому и у меня уже начали чесаться руки написать что-то реально нужное мне и таким же, как я. Что-то, что будет иметь единый интерфейс и способ взаимодействия как на компьютере, так и на телефоне. Родилось множество идей, но больше всего загорелся идеей сделать читалку для книг, которая будет синхронизировать состояние прогресса чтения на телефоне и компьютере. Придя домой, вы можете продолжить читать с того же места, где закончили, пока читали с телефона в пути. И все как в фильме "Начало" - идея грызла и прожигала изнутри и хотелось, но... не моглось. И не потому, что я не знал, как все это написать и организовать, а потому, что платформа все так же является сдержанным лордом и не дает тебе поврываться и устроить реальный дебош. Если детальнее: Windows 10 имеет API для вывода pdf документов, но это вывод с использованием jpg изображения, а не рендера самой разметки документа. Мобильная реализация функционала оставляет желать лучшего, изображения, которые получаешь на выходе имеюту эффект замыливания и "нечеткости" вне зависимости от того, какие параметры размера изображения для рендера указываешь. К сожалению, в программировании на C++ я не силен и реализовать собственными силами хороший движок для отрисовки pdf документа я не имею возможности. Есть платные компоненты сторонних производителей, но и у них качество в основном оставляет желать лучшего, да и цена высока для меня (500 вечно зеленых и более). И выходит, что идея есть, польза была б многим, но реализовать у меня не входит. Что же плохого произошло? В этом году Build прошел без мобильной платформы Windows вообще. Доклады были конечно, но нового ничего нет. И официальные лица заявляют, что платформа будет продолжать развиваться, но в ней сейчас не так заинтересованы, поэтому ждите. И демки на пленарных докладах проводились на IPhone и Android. И как-то меня, как разработчика, такое положение дел опечалило... Вот почему:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Еще год ничего нового в Windows 10 Mobile не будет и это так, потому как Microsoft теперь задавило свое же производство мобильных телефонов и приказало "ждать новый Surface Phone", а он будет весной 2017-го.&lt;/li&gt;
&lt;li&gt;Анонс 10ки для телефонов был еще летом 2015, в феврале 2016-го все должны были получить ее на свои устройства, и обещали, что "кидалова не будет, все получат". Но февраль пролетел без выпуска, все находились какие-то ошибки, которые нужно было исправить. Обновление все время переносилось.&lt;/li&gt;
&lt;li&gt;Несмотря на обещания, некоторые флагманы не получили Windows 10. Меня возмутил тот момент что Lumia 930 и 1520 не получат обновление. Хотя это самые лучшие телефоны из всей серии. Я очень хотел получить 930-ю в свое время, видно не зря я тогда не решился на нее перейти.&lt;/li&gt;
&lt;li&gt;Когда в марте выкатили обновление, то я с радостью обновился (без превью версий и недоработанных бета). По итогу получил девайс, который дико садился при использовании Wifi, экран произвольно моргал, когда происходили глюки с датчиком освещения, переодически телефон просто брал вырубался и запускался заново без причин для этого (просто находясь в режиме ожидания). Так же плеер сходил с ума, если использовать кнопки гарнитуры для управления проигрыванием.&lt;/li&gt;
&lt;li&gt;Приложения все сырые и не реализованы с надлежащим качеством, как это было раньше. Элементарный ввод текста на стандартной клавиатуре заставлял задуматься мою Lumia 830 для выдачи слов из словаря и подстановки их в текст. Да что там говорить, вы и сейчас можете увидеть иконки от Windows XP\7 на последней Windows 10, и я не пойму, неужели это так сложно - собрать пак новых иконок и забыть про отголоски прошлого. Напрашивается простой вывод - "те, кто надо из Microsoft" сами не пользуются Windows.&lt;/li&gt;
&lt;li&gt;Новые телефоны на Windows 10 имеют плохое качество самого телефона и операционной системы. Новых выходить не будет, потому как теперь только Surface Phone. Для подтверждения моих слов о посредственности телефонов можно посмотреть на кучу акций у российского &lt;a title="n-store" href="https://microsoftstore.ru/382393/microsoft-lumia-950-xl-chernyy/" target="_blank" rel="noopener noreferrer"&gt;n-store&lt;/a&gt;, где за покупку флагмана тебе давали в подарок lumia 650 годовую подписку на office 365, бонусы для игры в WoT да и сама цена была снижена на 10 000 рублей, при стартовой в 50-60... Куча отзывов о том, что телефон не удался.&lt;/li&gt;
&lt;li&gt;Microsoft теперь дружит со всеми и это хорошо, но как следствие все приложения office пакета и skype реализованы и работают на много лучше на Android и iPhone, чем на Windows 10 Mobile. Обновление другие платформы так же получают первыми. К примеру: недавно было выпущена клавиатура для iPhone с интересным методом ввода: &lt;a href="https://goo.gl/GKyLq2" target="_blank" rel="noopener noreferrer"&gt;Источник&lt;/a&gt; . Windows 10 же не позволяет реализовать собственную клавиатуру ...&lt;/li&gt;
&lt;li&gt;Обновления переносятся на более поздний срок и сейчас. И новые сообщения, о грядущей тесной интеграции становятся смешными, потому как еще куча ошибок и работы в том, что уже есть, куда же еще дальше плодить функционал?!&lt;/li&gt;
&lt;li&gt;Официальные лица указывают, что развитие будет и далее. Да будет, но это развитие настольной версии и как следствие мобильной именно в этом порядке. Система объединилась и вскоре вообще приставки Mobile\Phone не будет. Но основной уклон все так же на персональных компьютерах.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Приведу статистику своей игры. Скажу сразу, что на рекламу и продвижение я не потратил денег вообще, все скачки - это поисковые запросы в официальном магазине. Продемонстрирую общее положение дел, и что сейчас происходит с того самого момента Build 2016, который был 30 марта. Графики скачек приложения с масштабированием от последних 30 дней до года:&lt;/p&gt;
&lt;p&gt;&lt;a class="b-single-post__gallery" href="#gallery-popup"&gt; &lt;img src="images/OxxvKhj.jpg" alt="30 дней" /&gt; &lt;/a&gt;&lt;/p&gt;
&lt;div id="gallery-popup" class="b-gallery hidden"&gt;&lt;a class="b-gallery__link" title="30 дней" href="images/Er0Tv12.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="3 месяца" href="images/57D843d.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="6 месяцев" href="images/HRCkW4R.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="12 месяцев" href="images/Nxo210q.png"&gt;link&lt;/a&gt; &lt;a class="b-gallery__link" title="Вся статистика" href="images/Lc91wyv.png"&gt;link&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;Пробел, который образовался в декабре связан с тем, что приложение просто изьяли из магазина при этом не предьявив не притензий, не уведомив о нарушении прав или еще чем-то в этом роде. Просто в поиске не увидить приложение и все, даже если ты введешь полное имя. Хочу сказать большое спасибо Михаилу Черномордикову, только с его помощью я смог решить эту проблему. Официальная поддержка порекомендовала мне делиться ссылкой на приложение в своих соц сетях... и все. Это бред чистой воды, как по мне.&lt;/p&gt;
&lt;p&gt;Если посмотреть статистику за год, и вспомнить еще раз, что это распространение приложения только через поиск пользователями, то можно увидеть, как аудитория растет, как и настроения Windows Phone разработчиков, а после 30-го марта скачкообразным образом аудитория падает, как и продажи телефонов, как и настроения разработчиков. Все чаще сейчас можно увидеть заголовки "Windows Mobile мертв, не стоит ждать ничего хорошего". И, к сожалению, я частично согласен с ними. У меня осталась надежда на то, что Surface Phone и Windows 10 покажут отличный тандем в 2017-м, но это будет через год. Сейчас же у меня один совет для Microsoft - используйте свои продукты сами, которые выпускаете. Относитесь к ним так, как будто бы вы и есть тот самый злостный троль и критик в одном лице, который будет писать вам злостные отзывы и рассказывать, как у вас все плохо. Вот именно тогда реализуется весь потенциал платформы, который был заложен изначально. Сейчас же я ушел на Android и пока почти доволен. "Почти" потому, что все тоже не идеально. Очень хотелось бы вернуться на платформу Windows в будущем и обрадоваться хорошему развитию и росту. Пока же буду жить хорошим сегодня, а не "прекрасным завтра".&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Отдельно хочу сказать спасибо всем, кто скачивал и играл в игру, кто оставлял отзывы и поддерживал материально! Каждый ваш отзыв был прочтен, все они были важны и сыграли определенную роль в моем развитии, как разработчика. Спасибо большое! В скором времени я выложу исходный код игры на Github, отблагодарив таким образом интернет комьюнити, которое дало мне очень много.&lt;/p&gt;</description>
  <author>d2funlife@gmail.com (Danil Pavlov)</author>
  <category>истории разработчика</category>
  <guid isPermaLink="false">b067f9a9-93f1-4412-8670-a694a49ed67d</guid>
  <pubDate>Sun, 08 May 2016 22:19:51 GMT</pubDate>
</item>
<item>
  <title>ASP.NET MVC лучшие практики</title>
  <link>https://d2funlife.com/asp-net-mvc-best-practices</link>
  <description>&lt;p&gt;Для начала скажу, что я .NET приверженец до мозга костей, не скажу, что остальные технологии чужды, но то самое ламповое и теплое я нашел для себя в .NET и в ASP.NET MVC в частности. Но как бы не была прекрасна технология, она лишь инструмент, который при грамотном использовании и модернизации становится действительно мощным и полезным. Так же с ASP.NET MVC &amp;ndash; существуют проблемы, которые не имеют решения &amp;laquo;из коробки&amp;raquo;. Именно такие проблемы я выделил и сгруппировал по своему усмотрению.&lt;/p&gt;
&lt;p&gt;&lt;img src="images/mpF4Tu6.jpg" alt="Проблемы ASP.NET MVC" width="870" height="508" /&gt;&lt;/p&gt;
&lt;p&gt;Это основной список задач, которые требуют особого внимания и практик решения. Именно о лучших практиках решения этих задач я расскажу подробнее.&lt;/p&gt;
&lt;h2&gt;Общие проблемы архитектуры&lt;/h2&gt;
&lt;p&gt;Данный вопрос включает в себя понимание многослойной архитектуры приложений и умение грамотно сформировать solution на уровне проектов и файлов. Пример хорошей организации:&lt;/p&gt;
&lt;p&gt;&lt;img src="images/C4cbzRe.jpg" alt="Архитектура простого приложения" width="870" height="432" /&gt;&lt;/p&gt;
&lt;p&gt;Данный пример показывает организацию solution'а в рамках проектов. Проект ядра содержит все доменные модели, интерфейсы, ключевые сущности. Данный проект составляет основу приложения. Проект бизнес логики содержит всю необходимую бизнес логику, которая инкапсулирована в менеджерах и которая не должна иметь платформенных зависимостей. Проект доступа к данным - рекомендую использовать паттерн репозиторий, который помогает инкапсулировать (скрывать) доступ к хранилищу данных. К примеру, вы можете реализовать работу с данными, используя различные ORM, либо использовать хранимые процедуры. Проект контейнера инверсии управления содержит непосредственно сам контейнер, а так же настройки зависимостей абстракций и реализаций (интерфейсов и реализаций интерфейсов). Проект веб проекта содержит все наши контроллеры, css, js код, вообщем основную ASP.NET MVC инфраструктуру приложения. Саб веб проект содержит модели представлений, дополнительные хелперы, сущности, которые расширяют функционал веб проекта. Для чего же это нужно? &amp;ndash; для обеспечения легкости основного веб проекта, ведь согласитесь, намного проще воспринимать десяток строк и пару папок, чем огромные неповоротливые файлы с ужасной структурой файлов.&amp;nbsp;Но данная архитектура и организация solution'а подходит для небольших проектов. Для больших систем больше подходит следующая схема:&lt;/p&gt;
&lt;p&gt;&lt;img src="images/UsmvKgc.jpg" alt="Схема больших проектов" width="770" height="851" /&gt;&lt;/p&gt;
&lt;p&gt;Большие системы имеют ядро - "точку входа" в ASP.NET MVC приложение, а остальная функциональность представлена в виде модулей, которые наращивают функционал. Очень важно анализировать требования проекта и подбирать хорошую архитектуру проекта, так как это очень большая часть хорошего проекта.&lt;/p&gt;
&lt;h2&gt;Толстые и/или кривые контроллеры&lt;/h2&gt;
&lt;p&gt;Толстые контроллеры &amp;ndash; это бич многих проектов, так как за частую сжатые сроки или неопытность разработчика подталкивают его к написанию всей бизнес логики в контроллер и как итог выходят большие файлы, которые сложно читать и понимать. Контроллер должен выполнять роль посредника, который получил запрос, провалидировал, отдал на обработку и вернул финальное значение. Ни больше ни меньше. В этом и есть его принцип &lt;a title="S.O.L.I.D." href="https://goo.gl/Py7NwS" target="_blank" rel="noopener noreferrer"&gt;S.O.L.I.D.&lt;/a&gt;&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;- единство ответственности.&amp;nbsp;Идеальная ситуация, когда у нас есть менеджеры, которые отвечают за бизнес логику и мы валидируем данные, отдаем менеджерам на обработку, после чего возвращаем результат.&amp;nbsp;Использование своего базового типа для контроллеров полезно когда нам необходимо инкапслуровать общее поведение для нескольких контроллеров, иными словами мы создаем базовый класс контроллера, который после наследуем и пропадает необходимость дублировать какой то общий код в наследуемых контроллерах.&lt;/p&gt;
&lt;h2&gt;Паттерн Post/Redirect/Get&lt;/h2&gt;
&lt;p&gt;Как гласит &lt;a title="Wikipedia" href="https://goo.gl/A7syCj" target="_blank" rel="noopener noreferrer"&gt;Wikipedia&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;Post/Redirect/Get (PRG) &amp;mdash; модель поведения веб-приложений, используемая разработчиками для защиты от повторной отправки данных веб-форм (от т. н. double submit problem). Модель PRG обеспечивает интуитивно понятное поведение веб-приложений при обновлении страниц в браузере и при использовании закладок в браузере.&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="images/VKI6Nxz.jpg" alt="Post Redirect Get паттерн" width="870" height="257" /&gt;&lt;/p&gt;
&lt;p&gt;Иными словами используйте отдельные типы запросов для отображения данных и для модификации их. Существуют такие ситуации, когда отправка данных формы POST запросом на сервер прошла успешно, вернулась та же страница редактирования и если пользователь просто обновит страницу ему выпадет предупреждение "Подтвердите повторную отправку формы". Данная проблема решается тем, что после модификации данных в качестве результата действия пользователь не должен получить представление, а должен быть перенаправлен на получение данных, как если бы он только заходил в редактирование сущности. Таким образом можно избежать ситуации, когда пользователь может дублировать отправку формы, даже если просто захочет обновить страницу. Данный подход хорош для классической схемы приложения ASP.NET MVC, при асинхронном взаимодействии с сервером схема не применяется.&lt;/p&gt;
&lt;h2&gt;Cache&lt;/h2&gt;
&lt;p&gt;&lt;a title="Кэширование" href="https://goo.gl/fN3vyH" target="_blank" rel="noopener noreferrer"&gt;Кэширование&lt;/a&gt;&amp;nbsp;это&amp;nbsp;сложный вопрос по своей сути. И полностью его раскрыть можно с помощью отдельных статей, но если вкратце, то использование кеширования оправдано на страницах где нет динамических данных или где нет необходимости поддерживать постоянную актуальность данных. К примеру, много проектов имеют статическую главную страницу, которая не меняется так часто. Используя кеширование, можно увеличить нагрузку, которую сможет держать сервер. Кеширование имеет много граней, к примеру - где хранятся данные: сервер/клиент и т.д. , где кешировать данные, ведь можно использовать возможности ASP.NET,&amp;nbsp;&lt;a title="IIS" href="https://goo.gl/TxfJGW" target="_blank" rel="noopener noreferrer"&gt;IIS&lt;/a&gt;,&amp;nbsp;&lt;a title="HTTP" href="https://goo.gl/BkkeJb" target="_blank" rel="noopener noreferrer"&gt;HTTP&lt;/a&gt;.&amp;nbsp;Простой пример использования кеширования в ASP.NET MVC - это создать Cache профиль и использовать его на необходимых действиях контроллера.&lt;/p&gt;
&lt;pre class="language-markup"&gt;&lt;code&gt;&amp;lt;caching&amp;gt;
      &amp;lt;outputCacheSettings&amp;gt;
        &amp;lt;outputCacheProfiles&amp;gt;
          &amp;lt;add name="AllEvents" duration="15" varyByParam="*" location="Client"/&amp;gt;
        &amp;lt;/outputCacheProfiles&amp;gt;
       &amp;lt;/outputCacheSettings&amp;gt;
&amp;lt;/caching&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;[HttpGet, OutputCache(CacheProfile = "AllEvents")]
public ActionResult All()
{
    var promoterId = this.User.Identity.GetUserId&amp;lt;long&amp;gt;();
    var events = this.eventManager.GetAll(promoterId);
    return this.View(events);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Использование action filter&lt;/h2&gt;
&lt;p&gt;Ну хорошо, рассказал, что контроллеры могут быть жирными, что необходимо выносить логику в менеджеры , что можно не дублировать код контроллеров, используя супер типы и кешировать нужные ответы сервера, что же еще скрывается за "толстые" и/или кривые контроллеры? А есть еще такая вещь, как&amp;nbsp;&lt;a title="action filters" href="https://goo.gl/UsMK6L" target="_blank" rel="noopener noreferrer"&gt;action filters&lt;/a&gt;, мощь и пользу которых мало кто из новичков ценит и понимает вообще. Очень распространенная практика, когда программист долго пишет, используя какой-то набор готовых action filtter : авторизации и тд., но своих фильтров не пишет и не задумывается об этом. Приведу пример, как можно, используя фильтры, реализовать задачу более круто, чем с подходом "в лоб". Такой задачей является создание транзакций данных. Для этого нам нужен менеджер транзакций который инкапсулирует работу с транзакциями, а так же создать наш action filter:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;[System.AttributeUsage(System.AttributeTargets.All, AllowMultiple = false, Inherited = true)]
public class TransactionAttribute : ActionFilterAttribute
{
    private static ITransactionManager TransactionManager
    {
        get
        {
            return ServiceLocator.Current.GetInstance&amp;lt;ITransactionManager&amp;gt;();
        }
    }
    
    
	public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        TransactionManager.Begin();
    }
	
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        try
        {
            if (TransactionManager.IsActive)
            {
                if (((filterContext.Exception != null) &amp;amp;&amp;amp; !filterContext.ExceptionHandled))
                {
                    TransactionManager.Rollback();
                }
                else
                {
                    TransactionManager.Commit();
                }
            }
        }
        finally
        {
            TransactionManager.Dispose();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;А использование фильтра предельно простое, добавив атрибут к действию контроллера, можно сразу же обеспечить функциональность без явного кода реализации.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;[Transaction]
public ActionResult DeleteAccount(string email)
{
    //Удаление элеммента
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Если внимательнее разобраться в сути action filter, то можно понять, что данный инструмент позволяет вклиниться в процесс выполнения действия контроллера и влиять на выполнение действия в различных точках жизненного цикла запроса.&lt;/p&gt;
&lt;h2&gt;DTO (Data Transfer Objects)&lt;/h2&gt;
&lt;p&gt;Мир доменных моделей представляет собой набор всех сущностей, что представляют предметную область проекта, в свою же очередь view model &amp;ndash; это мир представлений. Модели представлений должны полностью удовлетворять потребности представлений. В множествах источников рекомендуют использовать на каждое отдельное представление отдельную модель. С помощью view model мы можем передать представлению что-то сверх доменной модели. И все бы хорошо, но когда начинаешь использовать такой подход, то сталкиваешься с такими вещами как&amp;nbsp;&lt;a title="DTO" href="https://goo.gl/GEiMXz" target="_blank" rel="noopener noreferrer"&gt;DTO&lt;/a&gt;&amp;nbsp;(Data Transfer Objects) &amp;ndash; конвертация из доменной модели в view model и обратно. Для многих это становится микро адом. Проблема всем известна и решение в виде mapper&amp;rsquo;ов все знают, я же хочу показать несколько интересных подходов для решения этого вопроса. В первом варианте решения используется ручной код, тут предельно просто, у нас всю ответственность за DTO берет на себя view model:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public class IssueDetailsViewModel
{
	public int IssueID { get; set; }
       public DateTime CreatedAt { get; set; }
       public string CreatorUserName { get; set; }
       public string Subject { get; set; }
       public IssueType IssueType { get; set; }
       public string AssignedToUserName { get; set; }
       public string Body { get; set; }

       public IssueDetailsViewModel(Domain.Issue model)
    {
        this.IssueID = model.IssueID;
        this.CreatedAt = model.CreatedAt;
        this.CreatorUserName = model.Creator.UserName;
        this.AssignedToUserName = model.AssignedTo.UserName;
        this.IssueType = model.IssueType;
        this.Subject = model.Subject;
        this.Body = model.Body;
    }

    public Domain.Issue GetDomain()
    {
        return new Domain.Issue
        {
               IssueID = this.IssueID,
               CreatedAt = this.CreatedAt,
               Subject = this.Subject,
               Body = this.Body,
               IssueType = this.IssueType
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Второй вариант можно использовать с mapper&amp;rsquo;ами. Суть его в том, что мы используем реализацию двух интерфейсов для указания, от какой доменной модели &amp;laquo;пошла&amp;raquo; view model, и как реализовать сложный mapping сущности. Сами интерфейсы:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public interface IHaveCustomMappings
{
   void CreateMappings(IConfiguration configuration);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Реализации интерфейсов имеют следующий вид:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public class IssueDetailsViewModel : IMapFrom&amp;lt;Domain.Issue&amp;gt;
{
	public int IssueID { get; set; }
	public DateTime CreatedAt { get; set; }
	public string CreatorUserName { get; set; }
	public string Subject { get; set; }
	public IssueType IssueType { get; set; }
	public string AssignedToUserName { get; set; }
	public string Body { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; } 
}

public class AssignmentStatsViewModel : IHaveCustomMappings
{
	public string UserName { get; set; }
	public int Enhancements { get; set; }
	public int Bugs { get; set; }
	public int Support { get; set; }
	public int Other { get; set; }

	public void CreateMappings(IConfiguration configuration)
	{
		configuration.CreateMap&amp;lt;ApplicationUser, AssignmentStatsViewModel&amp;gt;()
			.ForMember(m =&amp;gt; m.Enhancements, opt =&amp;gt; 
				opt.MapFrom(u =&amp;gt; u.Assignments.Count(i =&amp;gt; i.IssueType == IssueType.Enhancement)))
			.ForMember(m =&amp;gt; m.Bugs, opt =&amp;gt;
				opt.MapFrom(u =&amp;gt; u.Assignments.Count(i =&amp;gt; i.IssueType == IssueType.Bug)))
			.ForMember(m =&amp;gt; m.Support, opt =&amp;gt;
				opt.MapFrom(u =&amp;gt; u.Assignments.Count(i =&amp;gt; i.IssueType == IssueType.Support)))
			.ForMember(m =&amp;gt; m.Other, opt =&amp;gt;
				opt.MapFrom(u =&amp;gt; u.Assignments.Count(i =&amp;gt; i.IssueType == IssueType.Other)));
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Регистрацию мапингов сущностей можно произвести вручную, по-старинке, либо используя LINQ&amp;nbsp;и рефлексию. Как реализовать автоматическую регистрацию:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public class AutoMapperConfig
{
	public void Register()
	{
		var types = Assembly.GetExecutingAssembly().GetExportedTypes();
           LoadStandardMappings(types);
           LoadCustomMappings(types);
	}

	private static void LoadCustomMappings(IEnumerable&amp;lt;Type&amp;gt; types)
	{
		var maps = (from t in types
					from i in t.GetInterfaces()
					where typeof(IHaveCustomMappings).IsAssignableFrom(t) &amp;amp;&amp;amp;
						  !t.IsAbstract &amp;amp;&amp;amp;
						  !t.IsInterface
					select (IHaveCustomMappings)Activator.CreateInstance(t)).ToArray();

		foreach (var map in maps)
		{
			map.CreateMappings(Mapper.Configuration);
		}
	}

	private static void LoadStandardMappings(IEnumerable&amp;lt;Type&amp;gt; types)
	{
		var maps = (from t in types
					from i in t.GetInterfaces()
					where i.IsGenericType &amp;amp;&amp;amp; i.GetGenericTypeDefinition() == typeof(IMapFrom&amp;lt;&amp;gt;) &amp;amp;&amp;amp;
						  !t.IsAbstract &amp;amp;&amp;amp;
						  !t.IsInterface
					select new
					{
						Source = i.GetGenericArguments()[0], 
						Destination=t
					}).ToArray();

		foreach (var map in maps)
		{
			Mapper.CreateMap(map.Source, map.Destination);
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Принцип предельно прост - мы проходим по сборке и забираем все типы, которые реализуют необходимые интерфейсы и для простого мапинга cопоставляем сущности, а для сложного - выполняем метод, который описан в интерфейсе IHaveCustomMappings.&lt;/p&gt;
&lt;h2&gt;Использование строковых значений&lt;/h2&gt;
&lt;p&gt;ASP.NET MVC содержит множество функций, которые строятся с использованием строковых значений. Вроде бы ничего плохого в этом нет, но строки не проверяются во время компиляции, и без спец средств понять сходу, что в коде есть ошибка в написании строкового значения сложно. Такие строки и называются магическими, вроде все хорошо, но работать может и не будет. Пример использования магический строк:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public ActionResult CompetencyFrameworkReport()
{
    return View("ReportContainer",
    new ReportViewModel
    {
        Params = "User.CompetencyFrameworkReport.Params",
        Result = "User.CompetencyFrameworkReport.Result"
    });
}

public ActionResult ViewProfileRole()
{
    return RedirectToAction("ReviewGroupNeeded", "Assessment");
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Как можем видеть проблемы проявляются, когда происходит перенаправление к определенному действию контроллера, когда используем маршруты в наших представлениях. Хорошей практикой для решения вопросов маршрутов является следующее:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public static class UrlHelper
{
    public static string Login(this System.Web.Mvc.UrlHelper helper)
    {
        return helper.Action("Login", "Account");
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTML helper, который будет инкапсулировать наши маршруты. И Использовать его будем так:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;//Было
&amp;lt;a href="@Url.Action("Login", "Account")"&amp;gt;Login&amp;lt;/a&amp;gt;

//Стало
&amp;lt;a href="@Url.Login()"&amp;gt;Login&amp;lt;/a&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Получение доступа к view model из скрипта&lt;/h2&gt;
&lt;p&gt;На практике рекомендуется использовать для отображения данных модели представления, которые позволят строго типизировать представления и дать больше возможности для настройки и сохранить разделение слоев. И очень часто у неопытных разработчиков появляется задача получить данные из модели представления и отдать их на обработку javascript'у. Как итог - выходит, что в представление вклинивается большой блок javascript кода, который и будет "связующим звеном". Выглядит все это так:&lt;/p&gt;
&lt;pre class="language-javascript"&gt;&lt;code&gt;app.controller('ConsultantInformationController', function($scope, $filter, $http) {
    $scope.user = {
        id: 1,
        firstName: @Model.UserBaseViewModel.FirstName,
        lastName: @Model.UserBaseViewModel.LastName
    };

    //other code is here
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Примеры приведены для angular, но они так же будут актуальными для других js framewrk'ов, как к примеру knockout, backbone. Как же решить данные проблемы? А все предельно просто - мы создадим HTML helper, который преобразует наш объект в json и отдаст на обработку скрипту:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public static class JavaScriptHelper
{
	public static IHtmlString Json(this HtmlHelper helper, object obj)
	{
		var settings = new JsonSerializerSettings
		{
			ContractResolver = new CamelCasePropertyNamesContractResolver(),
			Converters = new JsonConverter[]
			{
				new StringEnumConverter(), 
			},
			StringEscapeHandling = StringEscapeHandling.EscapeHtml
		};

		return MvcHtmlString.Create(JsonConvert.SerializeObject(obj, settings));
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;И использовать можно вот так:&lt;/p&gt;
&lt;pre class="language-markup"&gt;&lt;code&gt;&amp;lt;div ng-controller='editIssueController' ng-init='init(@Html.Json(Model))'&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;И если уйти от js framework'ов, то реализация подобного подхода предельно проста - у нас должна быть функция, которая принимает json версию модели и обрабатывает ее далее. Данная практика идет тесно с рекомендацией использовать&amp;nbsp;&lt;a title="bundling и minification" href="https://goo.gl/DQAVK9" target="_blank" rel="noopener noreferrer"&gt;bundling и minification&lt;/a&gt;.&amp;nbsp;Ведь скрипты, которые находятся в представлениях никоим образом не сжимаются и страница становится не оптимальной. Bundling позволяет решить порос минимизации объема кода css и js а так же количества подключений для скачивания файлов.&lt;/p&gt;
&lt;h2&gt;Взаимодействие с пользователем&lt;/h2&gt;
&lt;p&gt;Очень часто стоит задача отображать пользователю результат его действий: успешно добавлена новая сущность, сущность удалена, появление ошибок ввода пользователя, ошибки работы приложения. Для решения такого рода задач есть очень хороший подход.&lt;/p&gt;
&lt;p&gt;&lt;img src="images/ID6nC0F.jpg" alt="Уведомление пользователю" width="870" height="310" /&gt;&lt;/p&gt;
&lt;p&gt;Множество проектов сейчас основаны на bootstrap, поэтому, мы можем реализовать все следующим образом. Сперва добавим новую модель уведомления:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public class Alert
{
	public string AlertClass { get; set; }
	public string Message { get; set; }

	public Alert(string alertClass, string message)
	{
		AlertClass = alertClass;
		Message = message;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Для хранения наших предупреждений мы будем использовать TempData. Для доступа к коллекции предупреждений реализуем следующее расширение:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public static class AlertExtensions
{
	const string Alerts = "_Alerts";

	public static List&amp;lt;Alert&amp;gt; GetAlerts(this TempDataDictionary tempData)
	{
		if (!tempData.ContainsKey(Alerts))
		{
			tempData[Alerts] = new List&amp;lt;Alert&amp;gt;();
		}

		return (List&amp;lt;Alert&amp;gt;) tempData[Alerts];
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Для создания расширения к результату действия мы создадим декоратор для ActionResult, который будет содержать результат действий, css класс для отображения и сообщение для пользователя.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public class AlertDecoratorResult : ActionResult
{
	public ActionResult InnerResult { get; set; }
	public string AlertClass { get; set; }
	public string Message { get; set; }

	public AlertDecoratorResult(ActionResult innerResult, string alertClass, string message)
	{
		InnerResult = innerResult;
		AlertClass = alertClass;
		Message = message;
	}

	public override void ExecuteResult(ControllerContext context)
	{
		var alerts = context.Controller.TempData.GetAlerts();
		alerts.Add(new Alert(AlertClass, Message));
		InnerResult.ExecuteResult(context);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Так же добавим расширяющие методы , которые и будут возвращать сообщения о результате действия. Полный класс будет выглядеть следующим образом:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public static class AlertExtensions
{
	const string Alerts = "_Alerts";

	public static List&amp;lt;Alert&amp;gt; GetAlerts(this TempDataDictionary tempData)
	{
		if (!tempData.ContainsKey(Alerts))
		{
			tempData[Alerts] = new List&amp;lt;Alert&amp;gt;();
		}

		return (List&amp;lt;Alert&amp;gt;) tempData[Alerts];
	}

	public static ActionResult WithSuccess(this ActionResult result, string message)
	{
		return new AlertDecoratorResult(result, "alert-success", message);
	}

	public static ActionResult WithInfo(this ActionResult result, string message)
	{
		return new AlertDecoratorResult(result, "alert-info", message);
	}

	public static ActionResult WithWarning(this ActionResult result, string message)
	{
		return new AlertDecoratorResult(result, "alert-warning", message);
	}

	public static ActionResult WithError(this ActionResult result, string message)
	{
		return new AlertDecoratorResult(result, "alert-danger", message);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Таким образом мы создали методы, которые будут добавлять уведомления пользователю и расширять наш ActionResult. Каждый метод возвращает результат + css класс для уведомления + сам текст уведомления. Для отображения сообщений создадим частичное представление и поместим его в шаблоне страницы.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;return RedirectToAction&amp;lt;HomeController&amp;gt;(c =&amp;gt; c.Index())
				       .WithSuccess("Issue created!");

return RedirectToAction&amp;lt;HomeController&amp;gt;(c =&amp;gt; c.Index())
				       .WithError("Unable to find the issue.  Maybe it was deleted?");

return RedirectToAction&amp;lt;HomeController&amp;gt;(c =&amp;gt; c.Index())
				       .WithInfo("Issue deleted!");&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Результат отображен был на изображении выше.&lt;/p&gt;
&lt;h2&gt;Итог и финальный best practice&lt;/h2&gt;
&lt;p&gt;Приведенные примеры лишь малая часть из общей массы советов и практик решения различных проблем и задач. Главный best practice - это использовать мощь платформы на полную и знать все тонкости ее. Ведь тогда вы будете видеть все, как на ладони и дополнения, расширения каких то моментов вас не затруднит ни коем образом. Используйте голову и не бойтесь экспериментировать и пробовать новое. Если у вас есть предложения по каким то своим практикам или замечания к приведенным мной, с радостью обсужу с вами эти вопросы в комментариях статьи.&lt;/p&gt;</description>
  <author>d2funlife@gmail.com (Danil Pavlov)</author>
  <category>asp.net mvc</category>
  <guid isPermaLink="false">c9d898ec-ebb4-4150-9a78-cf5cade25487</guid>
  <pubDate>Thu, 01 Oct 2015 08:55:55 GMT</pubDate>
</item>
<item>
  <title>Чистый код, или  "Встаньте! Мне нужно убраться".</title>
  <link>https://d2funlife.com/clean-code</link>
  <description>&lt;blockquote&gt;Больница № 389, после длительных и безуспешных попыток найти уборщицу, преобразована в грязелечебницу.&lt;/blockquote&gt;
&lt;p&gt;Каждый день, приходя на работу, мы усаживаемся за компы и начинаем писать что-то эдакое, да по проекту. Но кто-то садиться писать, а кто-то педалить, и я не берусь осуждать и гневно высказываться, просто пару советов, которые я выловил за год работы.&lt;/p&gt;
&lt;p&gt;Для начала хотелось бы ввести два понятия: &amp;laquo;писать&amp;raquo; и &amp;laquo;педалить&amp;raquo;. Писать у нас будет означать так как надо делать, а педалить, в свою очередь, как не надо. И так, включаем пылесос, начнем-с.&amp;nbsp;Начнем от печки, а точнее от дома. Классный дом &amp;mdash; это когда все сделано как надо, а главное фундамент заложен отлично, что гарантирует долговечность. Проведя аналогию с программированием, мы строим каждый день свои дома, и вот как в идеале выглядит чертеж.&lt;/p&gt;
&lt;p&gt;&lt;img src="images/lx7NvU2.jpg" alt="Архитектура приложения" width="818" height="418" /&gt;&lt;/p&gt;
&lt;p&gt;И, как тут показано, основой дома является чистый код. Кто-то возразит, что основой является архитектура, то есть то, что задумал архитектор, но я смотрю со стороны обычно строителя Степана, который все выполняет, что задумал архитектор. Так же помимо чистого кода есть еще ряд немаловажных блоков, многие из них будут описаны в следующих статьях. Как хороший строитель, Степан не должен использовать материалы какие попало, к примеру, вместо цемента при кладке кирпича использовать монтажную пену, похоже, а результат будет вообще не тот. Так и в программировании, не стоить педалить js функции в c# коде.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;string script = @"&amp;lt;script type="text/javascript"&amp;gt;
//
//как правило
//много
//очень много
//строк javascript
//
&amp;lt;/script&amp;gt;";
this.Headers.Controls.Add(new Literalcontrol("\r\n"+script));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Не стоит шуметь в коде, писать нужно только кратко и строго по сути, не распыляясь на прочую ерунду. Мозг человека может удержать в памяти приблизительно 7 элементов сущностей, поэтому не стоит педалить вагон и маленькую тележку переменных, флагов и прочего, если их реально много образовалось, то объедините их в сущности.&lt;/p&gt;
&lt;p&gt;Не повторяйтесь, согласитесь тяжело понять запущенного заику, так и в коде, если есть большой кусок, который можно вынести в отдельный метод, то сделайте это, назвав значимой фразой метод. Потом вместо повторений дирижаблей люди увидят всего одну строку и поймут, что происходит.&lt;/p&gt;
&lt;p&gt;Документирование&amp;hellip; вопрос вообще неоднозначный, все к нему относятся равнодушно, ведь писать комментарии, когда имя метода итак все объясняет, нелепо. Но если в методе реализована сложная логика, которая не является известным стандартным алгоритмом, то стоит описать ее, мало кто будет потом разбираться в вашем коде, помогите ему сразу, а то и вообще время пройдет и сами забудете, что писали, к примеру, заставили править и будет трата времени.&lt;/p&gt;
&lt;p&gt;Нейминг &amp;ndash; не используйте в именах переменных одну букву или сокращения (одну букву &amp;ndash; разве что в итераторе). Нейминг классов &amp;ndash; не педалим имена, которые ничего не отображают, которые имеют окончания на *Processor, *Info, не используйте аббревиатуры. Именем должно быть существительное и со смыслом. Нейминг методов, тут многие уже все итак прекрасно знают, описать имя метода, так чтобы отображалась суть, которую он выполняет.&lt;/p&gt;
&lt;p&gt;Булевые выражения &amp;ndash; все мы что то проверяем сравниваем, но прежде всего, ни вкоем случае не педалим такое:&lt;/p&gt;
&lt;pre class="language-javascript"&gt;&lt;code&gt;if(isLoggedIn==true)
{ 
   //какой-то код 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;loggedIn и так содержит только true или false значения, и сравнивать их никчемую Не стоит также и педалить подобного рода чепуху:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;bool goingToBar;

if(cashInWallet &amp;gt; 10)
{
   goingToBar = true;
}
else{
   goingToBar = false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Не отрицайте отрицание, да почти схоже с тавтологией, но нагляднее :&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;if(!isNotLoggedIn)
{
   //какой-то код
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Цель чистого кода - это сделать как можно компактнее код приложения, и так написать, чтоб человек смог его читать как роман, попивая чек иль чего покрепче (в нерабочее время есессно!) поэтому педалинг такого выражения не будет придавать элегантности моменту чтения или распития и чтения .&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;int pointsCount;
if(isSpeaker)
{
   pointsCount = 0;
}
else
{
   pointsCount = 50;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;А правильный вариант&amp;nbsp;выглядит вот так:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;int pointsCount = isSpeaker ? 0:50;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Не надо сравнивать со string какие-либо значения.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;if(employeeType == &amp;ldquo;manager&amp;rdquo;)
{
   //какой-то код
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;А правильный вариант:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;if(employee.Type == EmployeeType.Manager)
{
   //какой-то код
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Магические числа, вообще прекрасный момент, сравниваем переменную с 25, а что такое 25 ? ответ : 25. И это довольно частая ошибка, ее продолжают и продолжают педалить.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;if(age &amp;gt;= 21)
{
   //какой-то код
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;А правильный вариант:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;const int legalDrinkingAge = 21;
if(age &amp;gt;= legalDrinkingAge)
{
   //какой-то код
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Еще одна очень распространненая ошибка у программистов - это написание очень большого колличества условий в if конструкциях. Для понимания такого выражения, как приведено ниже нужно будет потратить время, возможно большое чем ожидалось :&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;if(employee.Age &amp;gt; 55 &amp;amp;&amp;amp; employee.YearsEmployed &amp;gt; 10 &amp;amp;&amp;amp; employee.IsRetired == true)
{
   //какой-то код
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;А правильный вариант:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;bool eligibleForPension = employee.Age &amp;gt; MinRetirementAge &amp;amp;&amp;amp; employee.YearsEmployed &amp;gt; MinPensionEmploymentYears &amp;amp;&amp;amp; employee.IsRetired;
/*переменная имеет осмысленное название, которое дает понять сразу, 
что проверяется право выхода на пенсию работника*/
if(eligibleForPension)
{
   //какой-то код
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Будьте логичны, изучайте свои инструменты, которыми работаете. Степан не стал бы пахать поле лошадью, если у него есть комбайн, так не стоит и нам. Как пример, использование LINQ для фильтрации данных:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;var matchingUsers = new List&amp;lt;User&amp;gt;();

foreach(var user in users)
{
    if(user.AccountBalance &amp;lt; minimumAccountBalance &amp;amp;&amp;amp;
       user.Status == Status.Active)
       {
           matchingUsers.Add(user);
       }
}

return matchingUsers;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;А лаконичнее будет:&amp;nbsp;&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;return users.Where(u =&amp;gt; u.AccountBalance &amp;lt; minimumAccountBalance)
            .Where(u =&amp;gt; u.Status == Status.Active);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Количество проверок не должно превышать вложенность в три уровня&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;if(some){
    if(anotherSome){
        if(anotherSecondSome){
            //какой то код
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Методы&lt;/p&gt;
&lt;p&gt;Методы имеют свои правила, и правила лучше всего соблюдать, дабы не ходить по полю с граблями, и потом рассказывать, как не справедлив мир. Есть всего три правила основных для методов:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Если код повторяется &amp;ndash; выделяй его в метод&lt;/li&gt;
&lt;li&gt;Если в коде может произойти ошибка, сделай так чтоб она проявлялась максимально быстро, а не в самом конце выполнения метода&lt;/li&gt;
&lt;li&gt;Возвращай значение как можно быстрее. Куй железо пока горячо, тык сказать.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;И если с первым все ясно и не надо обьяснять, то по оставшимся двум приведу пример.Возвращай как можно раньше :&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;private bool ValidateUsername(string username)
{
    var isValid = false;
    const int MinUsernameLength = 6;
    if(username.Length &amp;gt;= MinUsernameLength)
    {
        const int MaxUsernameLength = 25;
        if(username.Length &amp;lt;= MaxUsernameLength)
        {
            var isAlphaNumeric = username.All(Char.IsLetterOrDigit);
            if(isAlphaNumeric)
            {
                if(!ContainsCurseWords(username))
                {
                    isValid = IsUniqueUsername(username);
                }
            }
        }
    }
    return isValid;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;А вот как сделать более лаконично:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;private bool ValidateUsername(string username)
{
    const int MinUsernameLength = 6;
    if (username.Length &amp;lt; MinUsernameLength) 
        return false;
    const int MaxUsernameLength = 25;
    if (username.Length &amp;gt; MaxUsernameLength) 
        return false;
    var isAlphaNumeric = username.All(Char.IsLetterOrDigit);
    if (isAlphaNumeric) 
        return false;
    if (!ContainsCurseWords(username)) 
        return false;
    return IsUniqueUsername(username);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Если может выпасть исколючение или ошибка, то пускай это произойдет как можно раньше:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public void RegisterUser(string username, string password)
{
    if (!string.IsNullOrWhiteSpace(username))
    {
        if (!string.IsNullOrWhiteSpace(password))
        {
            //регистрация пользователя
        }
        else
        {
            throw new ArgumentException("Username is required.");
        }
    }
    else
    {
        throw new ArgumentException("Password is required");
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;И по традиции:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public void RegisterUser(string username, string password)
{
    if (!string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Username is required.");
    if (!string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Password is required");

    //регистрация пользователя
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;И на последок... Зная все и используя все эти знания, можно получить отличный код, но как же быть, если только в начале пути а дать подзатыльник некому? Ответ есть: Visual Studio любезно предоставила такое средство как Code Metrics.&amp;nbsp; Начать его использовать очень просто : всего лишь правой кнопокй мыши по "проекты" и "выбрать" Calculate Code Metrics. Более детально с результатами и значениями всего этого можно ознакомится вот здесь:&amp;nbsp;&lt;a title="Пример использования Code Metrics" href="https://goo.gl/9N6jFc" target="_blank" rel="noopener noreferrer"&gt;Code Metrics&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class="TextRun SCXW17309165" lang="RU-RU" xml:lang="RU-RU"&gt;&lt;span class="NormalTextRun SCXW17309165"&gt;P.S. Пишите отличный код, прочитав&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span class="TextRun Highlight SCXW17309165" lang="RU-RU" xml:lang="RU-RU"&gt;&lt;span class="NormalTextRun SCXW17309165"&gt;статью&lt;/span&gt;&lt;/span&gt;&lt;span class="TextRun Highlight SCXW17309165" lang="RU-RU" xml:lang="RU-RU"&gt;&lt;span class="NormalTextRun SCXW17309165"&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class="TextRun SCXW17309165" lang="RU-RU" xml:lang="RU-RU"&gt;&lt;span class="NormalTextRun SCXW17309165"&gt;&amp;nbsp;дай прочитать ее другу, а тот пусть своему другу&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span class="TextRun Highlight SCXW17309165" lang="RU-RU" xml:lang="RU-RU"&gt;&lt;span class="NormalTextRun SCXW17309165"&gt;передаст&lt;/span&gt;&lt;/span&gt;&lt;span class="TextRun Highlight SCXW17309165" lang="RU-RU" xml:lang="RU-RU"&gt;&lt;span class="NormalTextRun SCXW17309165"&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class="TextRun SCXW17309165" lang="RU-RU" xml:lang="RU-RU"&gt;&lt;span class="NormalTextRun SCXW17309165"&gt;&amp;nbsp;и будет вам счастье, много счастья. И не будет у вас в проекте все так, как было в анекдоте в самом начале&lt;/span&gt;&lt;/span&gt;&lt;span class="TextRun SCXW17309165" lang="RU-RU" xml:lang="RU-RU"&gt;&lt;span class="NormalTextRun SCXW17309165"&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span class="TextRun Highlight SCXW17309165" lang="RU-RU" xml:lang="RU-RU"&gt;&lt;span class="NormalTextRun SCXW17309165"&gt;статьи.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</description>
  <author>d2funlife@gmail.com (Danil Pavlov)</author>
  <category>чистый код</category>
  <guid isPermaLink="false">d03c12bb-06e6-4e92-a5aa-4a24b0e26ad4</guid>
  <pubDate>Sun, 05 Jul 2015 18:50:18 GMT</pubDate>
</item>
<item>
  <title>Настройка TeamCity и получение готовых билдов из Unity проекта.</title>
  <link>https://d2funlife.com/teamcity-unity-get-builds</link>
  <description>&lt;p&gt;Задачка дня : имеем более чем 2-3 разработчика, проект игры на Unity и необходимость получать автоматически "установочные файлы" приложения под разные платформы. "Можно подключить к компьютеру устройство запустить эмулятор и не нужен мне ваш TeamCity" - но так же в команде есть тестировщик, который не должен запариваться со всеми IDE и прочими вещами,а лишь получать и проверять, проверять, проверять, и именно поэтому необходимо настроить процесс получения готовых билдов под разные платформы автоматически. Итоговая формула входных данных : Unity игра + VCS(gitmercurial) + 2 + разработчика + 1 + тестировщик + дополнительные менеджеры заказчик. Решения данной задачи берет на себя такой подход как CI (&lt;em&gt;&lt;span lang="en" xml:lang="en"&gt;Continuous Integration&lt;/span&gt;&lt;/em&gt;&lt;em&gt;&lt;span lang="en" xml:lang="en"&gt; - &lt;/span&gt;&lt;/em&gt;&lt;span lang="en" xml:lang="en"&gt;н&lt;/span&gt;&lt;span lang="en" xml:lang="en"&gt;непрерывная интеграция&lt;/span&gt;&lt;em&gt;&lt;span lang="en" xml:lang="en"&gt;) -&amp;nbsp;&lt;/span&gt;&lt;/em&gt;&lt;span lang="en" xml:lang="en"&gt;подход&lt;/span&gt;&lt;span lang="en" xml:lang="en"&gt; позволяет обеспечить автоматическое выполнение задач проекта(тестирование, получение сборки, отправка уведомлений, отправка готового файла сборки в хранилище и тд.), а инструмент для выполнения - TeamCity (TC далее). Причины выбора и анализ аналогов не привожу, так как это тема отдельной статьи.&lt;/span&gt; На официальном сайте создателей инструмента&amp;nbsp; &lt;a href="https://www.jetbrains.com/teamcity/" target="_blank" rel="noopener noreferrer"&gt;JetBrains&lt;/a&gt; можно увидеть достаточно много обширной информации по TC, а так же получить пробник на 60 дней. Описание установки и настройки TC так же не привожу, потому, как пути решения вопросов и проблем возникающих при установке уже лежат там где и &lt;a href="https://www.google.com/" target="_blank" rel="noopener noreferrer"&gt;должны&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Общая схема иерархии и взаимодействия компонентов в системе такова:&lt;/p&gt;
&lt;p&gt;&lt;img src="images/VsKdBjm.jpg" alt="Схема взаимодействия компонентов системы TeamCity" width="870" height="638" /&gt;&lt;/p&gt;
&lt;p&gt;Project - проект который и является базой. Build configuration - логическая единица настроекшагов, которые обеспечивают группировку по критериям (платформаразные источники исходного кода). Build step - шаг автоматического исполнения, в котором указываются инструкции для выполнения. Build trigger - триггер, который будет срабатывать при заданных условиях.&lt;/p&gt;
&lt;p&gt;Исходя из данной схемы, для начала необходимо создать проект, настроить build configuration,&amp;nbsp; подключить VCS (Version Control System - система управления версиями) и составить&amp;nbsp; build steps. Проект создали, указали все нужные поля, создание конфигурации не содержит никаких сложностей, так как это чисто логическая единица:&lt;/p&gt;
&lt;p&gt;&lt;img src="images/vFqNocz.jpg" alt="Создание новой конфигурации для проекта" width="870" height="248" /&gt;&lt;/p&gt;
&lt;p&gt;К конфигурации необходимо подключить источник VCS, где все исходники проекта содержатся:&lt;/p&gt;
&lt;p&gt;&lt;img src="images/D4JnNSM.jpg" alt="Подключение VCS источника к конфигурации" width="870" height="891" /&gt;&lt;/p&gt;
&lt;p&gt;Type of VCS - в нашем примере Mercurial. VCS root name, VCS root ID - уникальное имя для идентификации VCS источника. Pull changes from - источник исходного кода. Default branch - какую ветку в системе контроля версий использовать. Имя пользователя и пароль - те, что используете в системе VCS. После создания конфигурации и подключения источника VCS необходимо создать build steps, которые будут выполнять всю рутину автоматических задач:&lt;/p&gt;
&lt;p&gt;&lt;img src="images/Rm7Ecym.jpg" alt="Создание нового build step&amp;rsquo;а" width="740" height="413" /&gt;&lt;/p&gt;
&lt;p&gt;Runner type &amp;nbsp;- список плагинов, которые доступны в TC и которые можно использовать. Предоставлено очень много вариаций под самые разные задачи, если ваша задача не решается, то можно написать собственный плагин, используя Java платформу. Таким образом, на высоком уровне проявляется новая схема взаимодействия компонентов в TC - все задачи выполняют плагины(runners), остается только написать скрипт, который и будет выполнятся. Для различных runner'ов разные настройки, к примеру для MSBuild следующие:&lt;/p&gt;
&lt;p&gt;&lt;img src="images/kZlYdwF.jpg" alt="MSBuild runner" width="870" height="559" /&gt;&lt;/p&gt;
&lt;p&gt;Build step'ов мы можем добавлять сколько нам необходимо в рамках нашей задачи, так же можем ограничивать выполнение - если предыдущий step не выполнен, то данный тоже не запускать. Для получения билдов из Unity проекта необходимы следующие setep'ы : построение проекта специфической платформы (для примера Windows Phone 8), получение исполняемого файла, перемещение файла в необходимый каталог сервер. Построение проекта под необходимую платформу из Unity источника:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using System;
using System.IO;

public static class BuildScript
{
	static void BuildWindowsPhone8()
	{
		var folderName = "Build-WindowsPhone8";
		DeleteFolder (folderName);
		CreateFolder (folderName);
		string error = BuildPipeline.BuildPlayer(GetScenes(), folderName, BuildTarget.WP8Player, BuildOptions.None);
		if (error != null &amp;amp;&amp;amp; error.Length &amp;gt; 0)
		{
			throw new Exception("Build failed: " + error);
		}
	}

	static void CreateFolder(string name)
	{
		if (!Directory.Exists(name))
		{
			Direc