<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Emiliano Montesdeoca</title><link>https://emimontesdeoca.github.io/ru/</link><description>Microsoft MVP in Developer Technologies. Cloud Solutions Team Lead. Speaker, blogger, and community advocate.</description><language>ru</language><managingEditor>emimontesdeoca@outlook.es (Emiliano Montesdeoca)</managingEditor><webMaster>emimontesdeoca@outlook.es (Emiliano Montesdeoca)</webMaster><lastBuildDate>Mon, 01 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://emimontesdeoca.github.io/ru/index.xml" rel="self" type="application/rss+xml"/><item><title>Blazor с нуля: Глава 3 — Компоненты, которые масштабируются</title><link>https://emimontesdeoca.github.io/ru/posts/blazor-from-scratch-chapter-3/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/blazor-from-scratch-chapter-3/</guid><description>Глава 3 серии Blazor с нуля. Подробно разбираем компоненты: параметры, композицию, RenderFragment и структуру папок, которая сохраняет UI чистым по мере роста приложения.</description><content:encoded>&lt;p>Добро пожаловать в главу 3 серии &lt;strong>Blazor с нуля&lt;/strong>. Если вы пропустили &lt;a href="../blazor-from-scratch-chapter-2">главу 2&lt;/a>, лучше сначала прочитать её, чтобы базовый проект уже был готов.&lt;/p>
&lt;p>В главе 2 мы запустили приложение. В этой главе сделаем его &lt;strong>поддерживаемым&lt;/strong>.&lt;/p>
&lt;p>Blazor-проекты быстро становятся запутанными, когда каждая страница превращается в огромный &lt;code>.razor&lt;/code>-файл. Компоненты помогают этого избежать: дают единообразие, переиспользование и чёткие границы между частями UI.&lt;/p>
&lt;hr>
&lt;h2 id="что-такое-компонент-blazor-на-практике">Что такое компонент Blazor на практике&lt;/h2>
&lt;p>Компонент — это &lt;code>.razor&lt;/code>-файл, который умеет:&lt;/p>
&lt;ul>
&lt;li>Рендерить разметку&lt;/li>
&lt;li>Хранить локальное состояние&lt;/li>
&lt;li>Принимать входные данные через параметры&lt;/li>
&lt;li>Отправлять события в родительский компонент&lt;/li>
&lt;li>Рендерить дочернее содержимое&lt;/li>
&lt;/ul>
&lt;p>Во время выполнения Blazor рассматривает компонент как маленькую машину состояний. При изменении состояния выполняется повторный рендер и применяется diff DOM.&lt;/p>
&lt;p>Поэтому ваша задача — проектировать ясные входы и предсказуемое поведение.&lt;/p>
&lt;hr>
&lt;h2 id="шаг-1-начните-с-узко-сфокусированного-компонента">Шаг 1: Начните с узко сфокусированного компонента&lt;/h2>
&lt;p>Создайте &lt;code>Components/Common/SectionHeader.razor&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;header class=&amp;#34;section-header&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h2&amp;gt;@Title&amp;lt;/h2&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @if (!string.IsNullOrWhiteSpace(Subtitle))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;@Subtitle&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/header&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string Title { get; set; } = string.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string? Subtitle { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Использование в &lt;code>Components/Pages/Home.razor&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;PageTitle&amp;gt;Blazor с нуля&amp;lt;/PageTitle&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;SectionHeader
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Title=&amp;#34;Blazor с нуля&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Subtitle=&amp;#34;Глава 3 посвящена компонентам.&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Пример маленький, но идея важная: компонент должен быть понятен по своим параметрам.&lt;/p>
&lt;hr>
&lt;h2 id="шаг-2-делайте-поведение-явным-через-параметры">Шаг 2: Делайте поведение явным через параметры&lt;/h2>
&lt;p>Создадим переиспользуемую кнопку.&lt;/p>
&lt;p>&lt;code>Components/Common/AppButton.razor&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;app-button @VariantCssClass&amp;#34;&lt;/span> &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;OnClick&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>Text
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string Text { get; set; } &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Button&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string Variant { get; set; } &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;primary&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public EventCallback OnClick { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private string VariantCssClass &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> Variant&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToLowerInvariant() &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;secondary&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;app-button--secondary&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;danger&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;app-button--danger&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;app-button--primary&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Пример использования:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private int _savedCount;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private void Save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _savedCount++;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;AppButton Text=&amp;#34;Save&amp;#34; Variant=&amp;#34;primary&amp;#34; OnClick=&amp;#34;Save&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;Сохранено @_savedCount раз.&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ключевой момент — дизайн контракта:&lt;/p>
&lt;ul>
&lt;li>Мало, но чётко определённых параметров&lt;/li>
&lt;li>Явные имена вместо &amp;ldquo;магического&amp;rdquo; поведения&lt;/li>
&lt;li>Безопасные значения по умолчанию&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="шаг-3-используйте-renderfragment-для-композиции">Шаг 3: Используйте &lt;code>RenderFragment&lt;/code> для композиции&lt;/h2>
&lt;p>&lt;code>RenderFragment&lt;/code> позволяет родителю передавать блоки UI в дочерний компонент.&lt;/p>
&lt;p>Создайте &lt;code>Components/Common/Card.razor&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;article class=&amp;#34;card&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;header class=&amp;#34;card__header&amp;#34;&amp;gt;@Title&amp;lt;/header&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;section class=&amp;#34;card__body&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @ChildContent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/article&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string Title { get; set; } = string.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public RenderFragment? ChildContent { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Пример:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;Card Title=&amp;#34;Roadmap&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ul&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;li&amp;gt;Компоненты&amp;lt;/li&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;li&amp;gt;Data binding&amp;lt;/li&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;li&amp;gt;Routing&amp;lt;/li&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/ul&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/Card&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Этот подход помогает строить единообразные страницы без копирования обёрток.&lt;/p>
&lt;hr>
&lt;h2 id="шаг-4-композиция-вместо-гигантских-страниц">Шаг 4: Композиция вместо гигантских страниц&lt;/h2>
&lt;p>Если страница разрастается, разделяйте по ответственности:&lt;/p>
&lt;ul>
&lt;li>&lt;code>ProfileSummary&lt;/code> для верхнего блока профиля&lt;/li>
&lt;li>&lt;code>ProfileStats&lt;/code> для метрик&lt;/li>
&lt;li>&lt;code>ProfileActivityList&lt;/code> для последних действий&lt;/li>
&lt;/ul>
&lt;p>Тогда страница становится точкой оркестрации, а не контейнером всех деталей.&lt;/p>
&lt;hr>
&lt;h2 id="шаг-5-баланс-между-разметкой-и-логикой">Шаг 5: Баланс между разметкой и логикой&lt;/h2>
&lt;p>Для простых компонентов inline &lt;code>@code&lt;/code> — нормально.&lt;/p>
&lt;p>Для крупных компонентов лучше вынести логику в code-behind:&lt;/p>
&lt;ul>
&lt;li>&lt;code>UserCard.razor&lt;/code>&lt;/li>
&lt;li>&lt;code>UserCard.razor.cs&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Так и разметка, и C#-логика остаются читаемыми.&lt;/p>
&lt;hr>
&lt;h2 id="шаг-6-практичная-структура-папок">Шаг 6: Практичная структура папок&lt;/h2>
&lt;p>Структура, которая хорошо масштабируется:&lt;/p>
&lt;ul>
&lt;li>&lt;code>Components/Pages/&lt;/code> -&amp;gt; маршрутизируемые страницы&lt;/li>
&lt;li>&lt;code>Components/Layout/&lt;/code> -&amp;gt; shell приложения и навигация&lt;/li>
&lt;li>&lt;code>Components/Common/&lt;/code> -&amp;gt; общие универсальные блоки&lt;/li>
&lt;li>&lt;code>Components/Features/&amp;lt;FeatureName&amp;gt;/&lt;/code> -&amp;gt; компоненты по функциональности&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="частые-ошибки-в-ранних-blazor-проектах">Частые ошибки в ранних Blazor-проектах&lt;/h2>
&lt;ul>
&lt;li>Слишком много параметров вместо отдельной модели&lt;/li>
&lt;li>Бизнес-правила прямо в page-компонентах&lt;/li>
&lt;li>Один &amp;ldquo;god component&amp;rdquo; на сотни строк&lt;/li>
&lt;li>Дублирование разметки вместо выделения переиспользуемых частей&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="что-дальше">Что дальше&lt;/h2>
&lt;p>В главе 4 разберём &lt;strong>data binding и события&lt;/strong>: &lt;code>@bind&lt;/code>, обработку событий, компромиссы two-way binding и паттерны предсказуемого состояния.&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category><category>Series</category></item><item><title>Blazor с нуля: Глава 2 — Ваше первое приложение Blazor</title><link>https://emimontesdeoca.github.io/ru/posts/blazor-from-scratch-chapter-2/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/blazor-from-scratch-chapter-2/</guid><description>Глава 2 серии Blazor с нуля. Создаем первое приложение, запускаем его локально и разбираем ключевые файлы, чтобы структура проекта была понятна с самого начала.</description><content:encoded>&lt;p>Добро пожаловать в главу 2 &lt;strong>Blazor с нуля&lt;/strong>. Если вы пропустили &lt;a href="../blazor-from-scratch-chapter-1">главу 1&lt;/a>, сначала прочитайте ее, чтобы освежить контекст рендеринга.&lt;/p>
&lt;p>В этой главе мы создадим новое приложение Blazor, запустим его локально и поймем, за что отвечают основные файлы.&lt;/p>
&lt;hr>
&lt;h2 id="шаг-1-создайте-проект">Шаг 1: Создайте проект&lt;/h2>
&lt;p>В терминале:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new blazor -o BlazorFromScratch
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd BlazorFromScratch
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Команда создаст Blazor Web App на .NET 9 со стандартной структурой.&lt;/p>
&lt;p>Если инструмент предлагает выбрать модель интерактивности, начните с &lt;strong>Interactive Server&lt;/strong>. Это самый простой режим для старта.&lt;/p>
&lt;hr>
&lt;h2 id="шаг-2-запустите-локально">Шаг 2: Запустите локально&lt;/h2>
&lt;p>Запустите приложение:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet watch
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Откройте URL из терминала (обычно &lt;code>https://localhost:5xxx&lt;/code>). Вы должны увидеть стандартное приложение со страницами Home, Counter и Weather.&lt;/p>
&lt;p>Если все открылось, окружение настроено правильно.&lt;/p>
&lt;hr>
&lt;h2 id="шаг-3-разберитесь-со-структурой-проекта">Шаг 3: Разберитесь со структурой проекта&lt;/h2>
&lt;p>Сначала важно знать следующие файлы и папки:&lt;/p>
&lt;ul>
&lt;li>&lt;code>Program.cs&lt;/code> — регистрация сервисов и настройка HTTP-пайплайна.&lt;/li>
&lt;li>&lt;code>Components/App.razor&lt;/code> — корневой компонент приложения.&lt;/li>
&lt;li>&lt;code>Components/Routes.razor&lt;/code> — маршрутизация страниц.&lt;/li>
&lt;li>&lt;code>Components/Pages/&lt;/code> — страницы с маршрутами (Home, Counter и т.д.).&lt;/li>
&lt;li>&lt;code>Components/Layout/&lt;/code> — общий layout (&lt;code>MainLayout.razor&lt;/code>, меню навигации).&lt;/li>
&lt;li>&lt;code>wwwroot/&lt;/code> — статические ресурсы (CSS, изображения, favicon).&lt;/li>
&lt;li>&lt;code>appsettings.json&lt;/code> — значения конфигурации.&lt;/li>
&lt;/ul>
&lt;p>Пока не нужно понимать все файлы. На этом этапе достаточно знать, где находится UI и где настраивается запуск.&lt;/p>
&lt;hr>
&lt;h2 id="шаг-4-сделайте-первое-изменение">Шаг 4: Сделайте первое изменение&lt;/h2>
&lt;p>Откройте &lt;code>Components/Pages/Home.razor&lt;/code> и замените содержимое на:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;PageTitle&amp;gt;Blazor с нуля&amp;lt;/PageTitle&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;Blazor с нуля&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;Глава 2 запущена локально.&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Сохраните файл и обновите страницу. С &lt;code>dotnet watch&lt;/code> изменения применяются сразу.&lt;/p>
&lt;p>Это подтверждает полный цикл: изменить -&amp;gt; собрать -&amp;gt; отрисовать.&lt;/p>
&lt;hr>
&lt;h2 id="шаг-5-один-раз-прочитайте-programcs">Шаг 5: Один раз прочитайте Program.cs&lt;/h2>
&lt;p>Не нужно заучивать, но важно узнавать ключевые строки:&lt;/p>
&lt;ul>
&lt;li>&lt;code>AddRazorComponents()&lt;/code> включает Razor-компоненты.&lt;/li>
&lt;li>&lt;code>AddInteractiveServerComponents()&lt;/code> включает интерактивные серверные компоненты.&lt;/li>
&lt;li>&lt;code>MapRazorComponents&amp;lt;App&amp;gt;()&lt;/code> сопоставляет корневой компонент с endpoint-ами.&lt;/li>
&lt;/ul>
&lt;p>Эти строки показывают, как приложение запускается и какие возможности рендеринга активны.&lt;/p>
&lt;hr>
&lt;h2 id="частые-проблемы">Частые проблемы&lt;/h2>
&lt;p>Если что-то не работает, обычно причина одна из этих:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Старый SDK&lt;/strong>: выполните &lt;code>dotnet --version&lt;/code> и проверьте .NET 9.&lt;/li>
&lt;li>&lt;strong>Проблема с HTTPS-сертификатом&lt;/strong>: выполните &lt;code>dotnet dev-certs https --trust&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Конфликт портов&lt;/strong>: остановите старые процессы &lt;code>dotnet&lt;/code> и запустите снова.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="что-дальше">Что дальше&lt;/h2>
&lt;p>В главе 3 мы подробно разберем &lt;strong>компоненты&lt;/strong>: параметры, композицию и структуру переиспользуемого UI.&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category><category>Series</category></item><item><title>Blazor с нуля: Глава 1 — Что такое Blazor?</title><link>https://emimontesdeoca.github.io/ru/posts/blazor-from-scratch-chapter-1/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/blazor-from-scratch-chapter-1/</guid><description>Глава 1 серии «Blazor с нуля». Рассматриваем, что такое Blazor на самом деле, откуда он взялся, какие существуют модели рендеринга и как он соотносится с JavaScript-фреймворками.</description><content:encoded>&lt;p>Добро пожаловать в Главу 1 серии &lt;strong>Blazor с нуля&lt;/strong>. Если вы пропустили &lt;a href="../blazor-from-scratch-intro">вводный пост&lt;/a>, в котором я объяснил, о чём эта серия и для кого она предназначена, начните с него — он короткий.&lt;/p>
&lt;p>В этой главе мы ответим на фундаментальный вопрос: &lt;em>что такое Blazor?&lt;/em> Звучит просто, но ответ многослоен — особенно потому, что за эти годы «Blazor» приобрёл несколько разные значения. К концу этого поста вы будете знать, что такое Blazor, как он вписывается в экосистему .NET, какие существуют модели рендеринга и почему вы могли бы выбрать его вместо JavaScript-фреймворка — или нет.&lt;/p>
&lt;hr>
&lt;h2 id="краткая-история">Краткая история&lt;/h2>
&lt;p>Blazor начался как экспериментальный проект Стива Сандерсона в Microsoft примерно в 2017 году. Идея была провокационной: запустить C# в браузере с помощью WebAssembly, полностью устранив необходимость в JavaScript. Это было доказательство концепции, а имя было намеренным слиянием &lt;strong>Bla&lt;/strong>zer и Ra&lt;strong>zor&lt;/strong> — движка шаблонов, на котором Blazor в итоге и был построен.&lt;/p>
&lt;p>Эксперимент вызвал достаточно энтузиазма, чтобы Microsoft отнеслась к нему серьёзно. Blazor вышел в составе ASP.NET Core 3.0 в сентябре 2019 года — сначала как &lt;strong>Blazor Server&lt;/strong>: модель, выполняющая C#-код на сервере и использующая соединение SignalR в реальном времени для отправки обновлений интерфейса в браузер. &lt;strong>Blazor WebAssembly&lt;/strong> последовал в мае 2020 года в рамках ASP.NET Core 3.1, принеся фреймворку настоящее выполнение на стороне клиента.&lt;/p>
&lt;p>.NET 6 и 7 улучшили опыт разработчиков. Затем .NET 8, выпущенный в ноябре 2023 года, коренным образом пересмотрел модель рендеринга с тем, что Microsoft назвала &lt;em>full-stack web UI&lt;/em>. Статический рендеринг на стороне сервера, потоковый рендеринг, интерактивный сервер, интерактивный WebAssembly и новый режим Auto — всё это теперь живёт под одной крышей в одном проекте. .NET 9 развил эту основу и сгладил шероховатости.&lt;/p>
&lt;p>Вот где мы находимся сегодня.&lt;/p>
&lt;hr>
&lt;h2 id="что-такое-blazor-на-самом-деле">Что такое Blazor на самом деле&lt;/h2>
&lt;p>В своей основе Blazor — это &lt;strong>компонентный UI-фреймворк для .NET&lt;/strong>. Вы строите интерфейс из компонентов — самодостаточных единиц C# и HTML-разметки, которые могут хранить состояние, реагировать на события и компоноваться в более крупные структуры. Если вы работали с React или Vue, эта ментальная модель покажется знакомой. Ключевое отличие в том, что вместо JavaScript вы пишете C#.&lt;/p>
&lt;p>Компоненты Blazor пишутся в файлах &lt;code>.razor&lt;/code>. Файл &lt;code>.razor&lt;/code> смешивает HTML-разметку с C#-кодом, используя синтаксис Razor, который вы, возможно, уже знаете по представлениям ASP.NET MVC. Вот как выглядит простой компонент:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/counter&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;Счётчик&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;Текущее значение: @currentCount&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;button @onclick=&amp;#34;IncrementCount&amp;#34;&amp;gt;Нажмите меня&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private int currentCount = 0;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private void IncrementCount()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentCount++;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>JavaScript не виден. Директива &lt;code>@onclick&lt;/code> связывает нажатие кнопки с C#-методом. Выражение &lt;code>@currentCount&lt;/code> отображает текущее значение. При изменении состояния Blazor определяет, что нужно обновить в DOM.&lt;/p>
&lt;p>Вот и весь стержень. Всё остальное — маршрутизация, внедрение зависимостей, формы, HTTP-вызовы — строится поверх этой компонентной модели.&lt;/p>
&lt;hr>
&lt;h2 id="модели-рендеринга">Модели рендеринга&lt;/h2>
&lt;p>Именно здесь кроется много путаницы, поэтому будем точны. Сегодня «Blazor» обозначает семейство режимов рендеринга, а не единую модель развёртывания. Понимать разницу важно, поскольку это влияет на производительность, требования к инфраструктуре и то, что ваши компоненты могут и не могут делать.&lt;/p>
&lt;h3 id="статический-ssr">Статический SSR&lt;/h3>
&lt;p>Простейший режим. Ваши компоненты Razor рендерятся на сервере в HTML, который затем отправляется в браузер. Нет постоянного соединения, нет WebAssembly и по умолчанию нет интерактивности на стороне клиента. По сути это то, что делает Razor Pages, но с использованием компонентной модели.&lt;/p>
&lt;p>Используйте для страниц с большим количеством контента, лендингов, всего, что не требует живой интерактивности.&lt;/p>
&lt;h3 id="интерактивный-сервер">Интерактивный сервер&lt;/h3>
&lt;p>Код компонента выполняется на сервере. Между браузером и сервером устанавливается WebSocket-соединение SignalR. Когда вы нажимаете кнопку или вводите что-то в поле ввода, событие передаётся по WebSocket на сервер, выполняется C#-код, вычисляется разница и обновления DOM отправляются обратно в браузер.&lt;/p>
&lt;p>&lt;strong>Преимущества:&lt;/strong> Полный доступ к ресурсам сервера (базы данных, файловая система, секреты). Быстрая начальная загрузка. Небольшой размер загружаемых данных. Работает в браузерах без поддержки WebAssembly.&lt;/p>
&lt;p>&lt;strong>Недостатки:&lt;/strong> Каждое взаимодействие пользователя требует обращения к серверу, что добавляет задержку. Ресурсы сервера масштабируются по количеству активных соединений. Приложение деградирует при обрыве соединения.&lt;/p>
&lt;p>Используйте для бизнес-приложений, внутренних инструментов, приложений, где серверный доступ к данным важнее офлайн-возможностей.&lt;/p>
&lt;h3 id="интерактивный-webassembly">Интерактивный WebAssembly&lt;/h3>
&lt;p>Среда выполнения .NET загружается в браузер, и приложение работает полностью на стороне клиента. После начальной загрузки приложение полностью работает офлайн, а каждое взаимодействие мгновенное — без обращений к серверу для UI-логики.&lt;/p>
&lt;p>&lt;strong>Преимущества:&lt;/strong> Настоящее выполнение на стороне клиента. Работает офлайн. Снижение нагрузки на сервер после загрузки приложения.&lt;/p>
&lt;p>&lt;strong>Недостатки:&lt;/strong> Больший начальный объём загрузки (среда выполнения .NET + приложение). Более медленная первая загрузка. Для доступа к данным на сервере нужен API.&lt;/p>
&lt;p>Используйте для офлайн-приложений, инструментов, требующих мгновенного отклика, сценариев, где серверная обработка не нужна для UI.&lt;/p>
&lt;h3 id="режим-auto">Режим Auto&lt;/h3>
&lt;p>Введён в .NET 8. Приложение стартует в режиме интерактивного сервера (быстрая начальная загрузка, нет ожидания загрузки WebAssembly). Как только файлы WebAssembly скачаются в фоне, последующие посещения автоматически переключаются в режим WebAssembly.&lt;/p>
&lt;p>Это даёт вам быструю первую загрузку серверного режима и в конечном счёте полное клиентское выполнение WebAssembly. Это наиболее сложная для понимания модель, но практичный выбор по умолчанию для многих приложений.&lt;/p>
&lt;h3 id="смешивание-режимов-в-одном-приложении">Смешивание режимов в одном приложении&lt;/h3>
&lt;p>В .NET 8 и выше вы не привязаны к единственному режиму рендеринга для всего приложения. Маркетинговый лендинг может быть статическим SSR; авторизованная панель управления — интерактивным сервером; виджет визуализации данных — интерактивным WebAssembly. Фреймворк управляет переходами.&lt;/p>
&lt;p>В этой серии мы начнём с интерактивного сервера, поскольку это проще всего запустить и у него наиболее понятная ментальная модель. Другие режимы исследуем по мере продвижения.&lt;/p>
&lt;hr>
&lt;h2 id="как-blazor-соотносится-с-javascript-фреймворками">Как Blazor соотносится с JavaScript-фреймворками&lt;/h2>
&lt;p>Если вы разрабатывали на React, Angular или Vue, вероятно, именно это сравнение вас интересует.&lt;/p>
&lt;p>&lt;strong>Сходства реальны.&lt;/strong> Компонентная модель Blazor намеренно похожа на React. У вас есть props (Parameters в Blazor), локальное состояние (поля в блоке &lt;code>@code&lt;/code>), хуки жизненного цикла и тот же паттерн: данные вниз, события вверх. Если вы знаете React, освоитесь за несколько часов.&lt;/p>
&lt;p>&lt;strong>Ключевое отличие — язык.&lt;/strong> В Blazor вы пишете C#. Бизнес-логика, правила валидации и модели данных могут быть общими для вашего Blazor-фронтенда и ASP.NET Core-бэкенда. Больше не нужно дублировать класс &lt;code>User&lt;/code> в TypeScript, если он уже есть на C#.&lt;/p>
&lt;p>&lt;strong>Разрыв экосистем реален, но сокращается.&lt;/strong> Экосистема npm огромна. Экосистема NuGet для UI-компонентов меньше, хотя существенно выросла. Если нужна специфическая библиотека графиков или виджет drag-and-drop, у JavaScript по-прежнему больше вариантов. Но для большинства бизнес-приложений того, что доступно в мире .NET, более чем достаточно.&lt;/p>
&lt;p>&lt;strong>JavaScript-взаимодействие есть, когда оно нужно.&lt;/strong> Blazor позволяет вызывать JavaScript из C# и C# из JavaScript. Для браузерных API без .NET-обёртки или при необходимости использовать существующую JS-библиотеку interop доступен. Он добавляет слой, но не причиняет боли.&lt;/p>
&lt;p>&lt;strong>Честный ответ:&lt;/strong> если вся ваша команда — JavaScript-разработчики, создающие публичный продукт, где критичны SEO, производительность и экосистема npm, оставайтесь на JavaScript. Если ваша команда — .NET-разработчики, если вы создаёте внутренние или бизнес-приложения, или если для вас важно совместное использование кода между фронтендом и бэкендом, Blazor — убедительный выбор.&lt;/p>
&lt;hr>
&lt;h2 id="что-нужно-для-дальнейшего-изучения">Что нужно для дальнейшего изучения&lt;/h2>
&lt;p>Для остальной части серии вам понадобится:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>.NET 9 SDK&lt;/strong> — загрузить с &lt;a href="https://dotnet.microsoft.com/download">dot.net&lt;/a>&lt;/li>
&lt;li>&lt;strong>IDE&lt;/strong> — Visual Studio 2022 (Community-редакция бесплатна), VS Code с расширением C# Dev Kit или JetBrains Rider&lt;/li>
&lt;li>Базовое знание C# — классы, свойства, интерфейсы, async/await&lt;/li>
&lt;/ul>
&lt;p>Это всё. Никакого Node, npm или webpack.&lt;/p>
&lt;hr>
&lt;h2 id="что-дальше">Что дальше&lt;/h2>
&lt;p>В Главе 2 мы создадим ваше первое приложение Blazor, разберём структуру проекта и убедимся, что вы можете запустить его локально. В итоге у вас будет работающее приложение и чёткое понимание того, что делает каждый файл.&lt;/p>
&lt;p>До встречи там.&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category><category>Series</category></item><item><title>Blazor с нуля: Новая серия</title><link>https://emimontesdeoca.github.io/ru/posts/blazor-from-scratch-intro/</link><pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/blazor-from-scratch-intro/</guid><description>Начало большой серии, в которой мы строим понимание Blazor с самого основания — без срезаний пути, без туманных объяснений, только чёткий разбор и настоящий код.</description><content:encoded>&lt;p>Я уже довольно давно пишу о Blazor — жизненные циклы компонентов, изолированный CSS, модели интерактивности, аутентификация. Эти публикации были полезны сами по себе, но мне всегда казалось, что им не хватает фундамента. Они исходят из того, что вы уже знаете, что такое Blazor, зачем он нужен и как вписывается в экосистему .NET. Знают это далеко не все — и это абсолютно нормально.&lt;/p>
&lt;p>Поэтому я начинаю кое-что новое: &lt;strong>Blazor с нуля&lt;/strong>. Настоящая серия, выстроенная от основ, для разработчиков, которые хотят действительно понять, что они строят, — а не просто копировать и вставлять, пока не заработает.&lt;/p>
&lt;h2 id="для-кого-эта-серия">Для кого эта серия&lt;/h2>
&lt;p>Серия для вас, если:&lt;/p>
&lt;ul>
&lt;li>вы .NET-разработчик, который слышал о Blazor, но никогда не находил времени или подходящей точки входа, чтобы разобраться.&lt;/li>
&lt;li>вы пробовали Blazor, запустили его, но чувствуете, что просто угадываете, &lt;em>почему&lt;/em> что-то работает.&lt;/li>
&lt;li>вы пришли из мира JavaScript/React/Angular и хотите понять, каким Microsoft видит современный фронтенд.&lt;/li>
&lt;li>вам нужен один связный ресурс вместо разрозненной документации и отдельных статей.&lt;/li>
&lt;/ul>
&lt;p>Быть старшим разработчиком не обязательно. Но нужно уверенно знать основы C# — классы, интерфейсы, async/await. Если вы умеете писать простое CRUD API на ASP.NET Core, вы готовы.&lt;/p>
&lt;h2 id="что-мы-рассмотрим">Что мы рассмотрим&lt;/h2>
&lt;p>Вот примерная дорожная карта того, что я планирую. Некоторые темы при необходимости разрастутся в несколько публикаций:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Что такое Blazor?&lt;/strong> — Модели хостинга, история, сравнение с традиционной веб-разработкой&lt;/li>
&lt;li>&lt;strong>Ваше первое приложение на Blazor&lt;/strong> — Scaffolding, структура проекта, запуск локально&lt;/li>
&lt;li>&lt;strong>Компоненты&lt;/strong> — Основной строительный блок любого интерфейса в Blazor&lt;/li>
&lt;li>&lt;strong>Привязка данных и события&lt;/strong> — Как сделать интерфейс реактивным&lt;/li>
&lt;li>&lt;strong>Взаимодействие компонентов&lt;/strong> — Параметры, EventCallbacks, каскадные значения&lt;/li>
&lt;li>&lt;strong>Маршрутизация и навигация&lt;/strong> — Как Blazor работает с URL и переходами между страницами&lt;/li>
&lt;li>&lt;strong>Внедрение зависимостей&lt;/strong> — Сервисы, скоупы и DI-контейнер в Blazor&lt;/li>
&lt;li>&lt;strong>Формы и валидация&lt;/strong> — EditForm, DataAnnotations, пользовательские валидаторы&lt;/li>
&lt;li>&lt;strong>HTTP и внешние данные&lt;/strong> — Вызов API из приложения на Blazor&lt;/li>
&lt;li>&lt;strong>Аутентификация и авторизация&lt;/strong> — Как правильно защитить приложение&lt;/li>
&lt;li>&lt;strong>JavaScript-интероп&lt;/strong> — Когда нужно обратиться напрямую к браузеру&lt;/li>
&lt;li>&lt;strong>Производительность и оптимизация&lt;/strong> — Виртуализация, ленивая загрузка, стратегии рендеринга&lt;/li>
&lt;li>&lt;strong>Тестирование компонентов Blazor&lt;/strong> — bUnit и как выглядит хороший тест&lt;/li>
&lt;li>&lt;strong>Деплой&lt;/strong> — Публикация в Azure, IIS и на статических хостах&lt;/li>
&lt;/ol>
&lt;p>Список будет развиваться. Некоторые темы разделятся на несколько публикаций, другие могут объединиться. Буду обновлять эту запись по мере продвижения серии и добавлять ссылки на каждую часть по мере выхода.&lt;/p>
&lt;h2 id="почему-серия-почему-сейчас">Почему серия, почему сейчас&lt;/h2>
&lt;p>Blazor значительно повзрослел. В .NET 8 и 9 модель рендеринга была кардинально переработана — статический SSR, потоковый рендеринг, интерактивный Server, интерактивный WebAssembly и режим Auto теперь уживаются под одной крышей. Это по-настоящему интересный и мощный фреймворк, но возросшая сложность делает первоначальный опыт дезориентирующим.&lt;/p>
&lt;p>Хочу создать ресурс, который встретит вас там, где вы находитесь, и методично проведёт через всё целиком. Не замена официальной документации — она хороша, и её стоит читать, — а попутчик, объясняющий &lt;em>почему&lt;/em> стоит за &lt;em>что&lt;/em>.&lt;/p>
&lt;h2 id="как-следить-за-серией">Как следить за серией&lt;/h2>
&lt;p>Каждая публикация в серии будет достаточно самостоятельной, чтобы читать её отдельно, но они также будут опираться друг на друга. Если начинаете с нуля — рекомендую идти по порядку. Если подключаетесь, чтобы заполнить конкретный пробел, — это тоже нормально: буду ссылаться на предыдущие материалы там, где это важно.&lt;/p>
&lt;p>Код к каждой публикации будет доступен на GitHub. Ссылки буду делиться по ходу.&lt;/p>
&lt;p>До встречи в следующей части.&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category><category>Series</category></item><item><title>Типы объединений C#: наконец-то появляются размеченные объединения</title><link>https://emimontesdeoca.github.io/ru/posts/csharp-union-types/</link><pubDate>Wed, 01 Apr 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/csharp-union-types/</guid><description>Подробное описание будущих типов дискриминируемых объединений C# — что это такое, как они работают и почему они изменят способ моделирования ваших доменов.</description><content:encoded>&lt;p>Если вы пишете на C# какое-то значительное время, велика вероятность, что вы уперлись в стену, пытаясь смоделировать что-то, что может быть «одной из нескольких вещей». Возможно, вам нужен метод, возвращающий либо успешное значение, либо ошибку. Возможно, вы создавали платежную систему, которая обрабатывает кредитные карты, банковские переводы и цифровые кошельки — каждый с совершенно разными данными. Или, может быть, вы просто посмотрели на F# или Rust и подумали: «Почему я не могу этого сделать на C#?»&lt;/p>
&lt;p>Ожидание почти закончилось. &lt;strong>Дискриминированные объединения приходят в C#.&lt;/strong>&lt;/p>
&lt;p>Это была одна из самых востребованных функций языка на протяжении многих лет, а обсуждения в сообществе начались еще в 2017 году и раньше. Группа разработчиков языка C# работала над предложением, которое вводит ключевое слово &lt;code>union&lt;/code> для определения иерархий закрытого типа с исчерпывающим сопоставлением шаблонов. В этом посте я хочу рассказать вам, что такое дискриминируемые объединения, почему они так важны, как мы до сих пор их подделывали и как на самом деле выглядит предлагаемый синтаксис — с реальными примерами кода для каждого.&lt;/p>
&lt;p>Небольшое примечание, прежде чем мы углубимся: на момент написания этой статьи функция объединения типов все еще находится на стадии предложения и предварительного просмотра. Синтаксис и поведение, которые я описываю здесь, основаны на последних общедоступных документах по проектированию и обсуждениях группы разработчиков языка C#. Все может измениться до финального релиза. Я буду четко объяснять, что подтверждено, а что еще обсуждается.&lt;/p>
&lt;h2 id="что-такое-дискриминируемые-профсоюзы">Что такое дискриминируемые профсоюзы?&lt;/h2>
&lt;p>По своей сути дискриминируемое объединение (иногда называемое «теговым объединением» или «типом суммы») — это тип, который может содержать одно из фиксированного набора возможных значений, причем каждый вариант может нести разные данные. «Дискриминируемая» часть означает, что среда выполнения всегда знает, какой это вариант — существует тег, который его идентифицирует.&lt;/p>
&lt;p>Думайте об этом как о &lt;code>enum&lt;/code>, но каждый участник может нести свою собственную полезную нагрузку данных.&lt;/p>
&lt;p>Если вы использовали другие языки, вы, вероятно, видели эту концепцию раньше:&lt;/p>
&lt;p>В &lt;strong>F#&lt;/strong> профсоюзы дискриминируются с самого первого дня:&lt;/p>
&lt;p>[[[ТОК_2]]]&lt;/p>
&lt;p>&lt;strong>Rust&lt;/strong> использует &lt;code>enum&lt;/code> для той же идеи:&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;p>&lt;strong>TypeScript&lt;/strong> достигает чего-то похожего с теговыми объединениями:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-typescript" data-lang="typescript">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">type&lt;/span> Shape &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> { kind&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;circle&amp;#34;&lt;/span>; radius: &lt;span style="color:#ff7b72">number&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> { kind&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;rectangle&amp;#34;&lt;/span>; width: &lt;span style="color:#ff7b72">number&lt;/span>; height: &lt;span style="color:#ff7b72">number&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> { kind&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;triangle&amp;#34;&lt;/span>; base: &lt;span style="color:#ff7b72">number&lt;/span>; height: &lt;span style="color:#ff7b72">number&lt;/span> };
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Во всех этих случаях компилятор знает все возможные варианты и может заставить вас обработать их все. Это суперсила: &lt;strong>полная проверка&lt;/strong>. Если вы добавите новый вариант, компилятор везде сообщит вам, что вы забыли его обработать.&lt;/p>
&lt;p>В C# никогда не было первоклассного способа выразить это. До настоящего времени.&lt;/p>
&lt;h2 id="как-мы-моделируем-профсоюзы-сегодня">Как мы моделируем профсоюзы сегодня&lt;/h2>
&lt;p>За прошедшие годы сообщество C# придумало несколько обходных путей, каждый из которых имел свои недостатки. Позвольте мне рассказать о наиболее распространенных подходах.&lt;/p>
&lt;h3 id="абстрактные-записи-с-наследованием">Абстрактные записи с наследованием&lt;/h3>
&lt;p>Самый идиоматический обходной путь в современном C# — использование абстрактных записей с запечатанными производными типами:&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;p>Это работает достаточно хорошо. Вы получаете неизменяемость, равенство значений и можете использовать сопоставление с образцом:&lt;/p>
&lt;p>[[[ТОК_7]]]Но есть существенные недостатки. Компилятор не знает, что иерархия закрыта, поэтому вам всегда нужен этот рычаг сброса &lt;code>_&lt;/code>, иначе вы получите предупреждение. Если вы добавите новый вариант, компилятор не сообщит вам обо всех местах, где вы забыли его обработать — сброс молча проглотит его. Это полностью противоречит цели.&lt;/p>
&lt;h3 id="библиотека-oneof">Библиотека OneOf&lt;/h3>
&lt;p>Другой популярный подход — пакет NuGet &lt;a href="https://github.com/mcintyre321/OneOf">OneOf&lt;/a>:&lt;/p>
&lt;p>[[[ТОК_10]]]&lt;/p>
&lt;p>OneOf обеспечивает проверку полноты во время компиляции с помощью параметров универсального типа, и это здорово. Но он полагается на позиционное сопоставление (первого типа, второго типа и т. д.), общие сигнатуры быстро становятся громоздкими и не интегрируются с сопоставлением шаблонов языка. Это умный хак, но это всё равно хак.&lt;/p>
&lt;h3 id="ручное-перечисление--шаблон-данных">Ручное перечисление + шаблон данных&lt;/h3>
&lt;p>Некоторые разработчики идут по классическому пути, используя тег enum и контейнер данных:&lt;/p>
&lt;p>[[[ТОК_11]]]&lt;/p>
&lt;p>Это хрупко. Ничто не мешает вам установить для &lt;code>Type&lt;/code> значение &lt;code>CreditCard&lt;/code>, кроме заполнения свойства &lt;code>BankTransfer&lt;/code>. Компилятор не сможет вам помочь, и вы получите повсюду ошибки времени выполнения и нулевые проверки. Это «строчно типизированный» подход к моделированию типов, и он не масштабируется.&lt;/p>
&lt;p>Все эти подходы имеют общую фундаментальную проблему: &lt;strong>они борются с языком, а не работают с ним.&lt;/strong> Компилятор не может рассуждать о закрытом наборе возможностей, поэтому вы теряете самое ценное свойство дискриминируемых объединений — исчерпывающую проверку.&lt;/p>
&lt;h2 id="предложение-c-ключевое-слово-union">Предложение C#: ключевое слово &lt;code>union&lt;/code>&lt;/h2>
&lt;p>В предложении группы языка C# представлено выделенное ключевое слово &lt;code>union&lt;/code>, которое определяет закрытый набор именованных членов, каждый из которых может необязательно нести данные. Вот основной синтаксис, как он был предложен:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Shape
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Circle(&lt;span style="color:#ff7b72">double&lt;/span> Radius),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Rectangle(&lt;span style="color:#ff7b72">double&lt;/span> Width, &lt;span style="color:#ff7b72">double&lt;/span> Height),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Triangle(&lt;span style="color:#ff7b72">double&lt;/span> Base, &lt;span style="color:#ff7b72">double&lt;/span> Height)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Вот и все. Четко, лаконично и сразу читается. Каждый член внутри союза определяет отдельный вариант со своими собственными данными. Компилятор знает, что &lt;code>Shape&lt;/code> может быть только одним из этих трех объектов.&lt;/p>
&lt;p>Под капотом компилятор генерирует иерархию запечатанных типов — аналогично тому, что вы пишете вручную с абстрактными записями, но с полным пониманием компилятором закрытой природы типа. Это означает, что компилятор может обеспечить полноту сопоставления с образцом, что является ключевым преимуществом.&lt;/p>
&lt;h3 id="члены-имеющие-только-значение">Члены, имеющие только значение&lt;/h3>
&lt;p>Членам профсоюза не нужно переносить данные. Вы можете смешивать элементы, переносящие данные, с простыми элементами-значениями:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Option&amp;lt;T&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Some(T Value),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> None
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это классический тип &lt;code>Option&lt;/code>/&lt;code>Maybe&lt;/code>, который функциональные программисты просили в C# в течение многих лет. &lt;code>None&lt;/code> не содержит данных — это просто тег.&lt;/p>
&lt;h3 id="общие-союзы">Общие союзы&lt;/h3>
&lt;p>Как видно из примера &lt;code>Option&amp;lt;T&amp;gt;&lt;/code> выше, объединения поддерживают дженерики. Вот более сложный пример:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Result&amp;lt;T, E&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Ok(T Value),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(E Err)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это открывает целый стиль обработки ошибок, который не основан на исключениях для ожидаемых случаев сбоя — то, что уже много лет является стандартной практикой в Rust и функциональных языках.&lt;/p>
&lt;h3 id="объединения-с-методами">Объединения с методами&lt;/h3>
&lt;p>Предложение также позволяет объединениям иметь методы, вычисляемые свойства и реализовывать интерфейсы, как и любой другой тип:```csharp
union Shape
{
Circle(double Radius),
Rectangle(double Width, double Height),
Triangle(double Base, double Height);&lt;/p>
&lt;pre>&lt;code>public double Area =&amp;gt; this switch
{
Circle(var r) =&amp;gt; Math.PI * r * r,
Rectangle(var w, var h) =&amp;gt; w * h,
Triangle(var b, var h) =&amp;gt; 0.5 * b * h
};
public double Perimeter =&amp;gt; this switch
{
Circle(var r) =&amp;gt; 2 * Math.PI * r,
Rectangle(var w, var h) =&amp;gt; 2 * (w + h),
Triangle(var b, var h) =&amp;gt; b + h + Math.Sqrt(b * b + h * h)
};
&lt;/code>&lt;/pre>
&lt;p>}&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-mysql" data-lang="mysql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">Обратите&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">внимание&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">что&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">выражение&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>switch&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">внутри&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>Area&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">и&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>Perimeter&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">не&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">требует&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">руки&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">по&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">умолчанию&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">Компилятор&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">знает&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">что&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">объединение&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">является&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">исчерпывающим&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">—&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">существует&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">только&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">три&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">варианта&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">и&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">все&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">три&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">обрабатываются&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">Если&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">позже&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">вы&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">добавите&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">четвертый&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">вариант&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">компилятор&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">пометит&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">каждый&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>switch&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">который&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">его&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">не&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">обрабатывает&lt;/span>.&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">## Интеграция сопоставления с образцом
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">Сопоставление&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">шаблонов&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">развивается&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">в&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>C&lt;span style="color:#8b949e;font-style:italic"># начиная с версии 7.0, и типы объединения созданы для того, чтобы стать первоклассными членами этой системы.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">### Исчерпывающие выражения переключения
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">Самая&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">эффективная&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">функция&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">—&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">исчерпывающая&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">проверка&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">переключателей&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">При&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">использовании&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">объединений&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">компилятор&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#f85149">знает&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">все&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">возможные&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">случаи&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">```&lt;/span>csharp&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>string&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">Describe&lt;/span>(Shape&lt;span style="color:#6e7681"> &lt;/span>shape)&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>shape&lt;span style="color:#6e7681"> &lt;/span>switch&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">{&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#d2a8ff;font-weight:bold">Circle&lt;/span>(var&lt;span style="color:#6e7681"> &lt;/span>r)&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">$&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;A circle with radius {r}&amp;#34;&lt;/span>,&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#d2a8ff;font-weight:bold">Rectangle&lt;/span>(var&lt;span style="color:#6e7681"> &lt;/span>w,&lt;span style="color:#6e7681"> &lt;/span>var&lt;span style="color:#6e7681"> &lt;/span>h)&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">$&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;A {w}x{h} rectangle&amp;#34;&lt;/span>,&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#d2a8ff;font-weight:bold">Triangle&lt;/span>(var&lt;span style="color:#6e7681"> &lt;/span>b,&lt;span style="color:#6e7681"> &lt;/span>var&lt;span style="color:#6e7681"> &lt;/span>h)&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">$&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;A triangle with base {b} and height {h}&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">}&lt;/span>;&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Нет рычага для сброса. Нет &lt;code>_ =&amp;gt; throw new NotImplementedException()&lt;/code>. Если вы забудете регистр, компилятор выдаст ошибку, а не предупреждение. Это фундаментальное улучшение безопасности.&lt;/p>
&lt;h3 id="сопоставление-вложенных-шаблонов">Сопоставление вложенных шаблонов&lt;/h3>
&lt;p>Объединения естественным образом составляются с помощью вложенных шаблонов:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Expr
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Literal(&lt;span style="color:#ff7b72">double&lt;/span> Value),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Add(Expr Left, Expr Right),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Multiply(Expr Left, Expr Right),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Negate(Expr Inner)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">double&lt;/span> Evaluate(Expr expr) =&amp;gt; expr &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Literal(&lt;span style="color:#ff7b72">var&lt;/span> v) =&amp;gt; v,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Add(&lt;span style="color:#ff7b72">var&lt;/span> left, &lt;span style="color:#ff7b72">var&lt;/span> right) =&amp;gt; Evaluate(left) + Evaluate(right),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Multiply(&lt;span style="color:#ff7b72">var&lt;/span> left, &lt;span style="color:#ff7b72">var&lt;/span> right) =&amp;gt; Evaluate(left) * Evaluate(right),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Negate(&lt;span style="color:#ff7b72">var&lt;/span> inner) =&amp;gt; -Evaluate(inner)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Этот вид рекурсивной структуры данных чрезвычайно распространен в компиляторах, интерпретаторах, механизмах правил и математическом моделировании. Сегодня в C# вам понадобится глубокая иерархия классов и шаблон посетителя. С объединениями код значительно упрощается.&lt;/p>
&lt;h3 id="охранные-положения">Охранные положения&lt;/h3>
&lt;p>Сопоставление шаблонов с объединениями поддерживает защиту &lt;code>when&lt;/code> так, как и следовало ожидать:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> Classify(Shape shape) =&amp;gt; shape &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Circle(&lt;span style="color:#ff7b72">var&lt;/span> r) when r &amp;gt; &lt;span style="color:#a5d6ff">100&lt;/span> =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Large circle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Circle(&lt;span style="color:#ff7b72">var&lt;/span> r) when r &amp;gt; &lt;span style="color:#a5d6ff">10&lt;/span> =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Medium circle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Circle(_) =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Small circle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Rectangle(&lt;span style="color:#ff7b72">var&lt;/span> w, &lt;span style="color:#ff7b72">var&lt;/span> h) when w == h =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Square&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Rectangle _ =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Rectangle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Triangle _ =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Triangle&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="практические-примеры">Практические примеры&lt;/h2>
&lt;p>Позвольте мне рассказать о некоторых реальных сценариях, в которых типы объединения значительно улучшают код.&lt;/p>
&lt;h3 id="шаблон-результата-замена-исключений-на-ожидаемые-ошибки">Шаблон результата: замена исключений на ожидаемые ошибки&lt;/h3>
&lt;p>Одним из наиболее распространенных шаблонов в разработке современных приложений является представление операций, которые могут быть успешными или неудачными, без использования исключений для потока управления. Исключения должны быть исключительными — например, сбои сети или нехватка памяти. Ошибка проверки или результат «не найден» — это ожидаемый результат, а не исключение.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Result&amp;lt;T, E&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Ok(T Value),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(E Err)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>union OrderError
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NotFound(Guid OrderId),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> InsufficientStock(&lt;span style="color:#ff7b72">string&lt;/span> ProductId, &lt;span style="color:#ff7b72">int&lt;/span> Requested, &lt;span style="color:#ff7b72">int&lt;/span> Available),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PaymentDeclined(&lt;span style="color:#ff7b72">string&lt;/span> Reason),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ValidationFailed(IReadOnlyList&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; Errors)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Result&amp;lt;Order, OrderError&amp;gt; ProcessOrder(OrderRequest request)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!Validate(request, &lt;span style="color:#ff7b72">out&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> errors))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ValidationFailed(errors);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> product = catalog.Find(request.ProductId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (product &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> NotFound(request.OrderId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (product.Stock &amp;lt; request.Quantity)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> InsufficientStock(request.ProductId, request.Quantity, product.Stock);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> paymentResult = paymentGateway.Charge(request.Payment);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!paymentResult.Success)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> PaymentDeclined(paymentResult.Message);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> order = CreateOrder(request, product);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Ok(order);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Затем вызывающая сторона вынуждена обрабатывать все возможные результаты:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = ProcessOrder(request);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> response = result &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Ok(&lt;span style="color:#ff7b72">var&lt;/span> order) =&amp;gt; Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/orders/{order.Id}&amp;#34;&lt;/span>, order),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(NotFound(&lt;span style="color:#ff7b72">var&lt;/span> id)) =&amp;gt; Results.NotFound(&lt;span style="color:#a5d6ff">$&amp;#34;Order {id} not found&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(InsufficientStock(&lt;span style="color:#ff7b72">var&lt;/span> pid, &lt;span style="color:#ff7b72">var&lt;/span> req, &lt;span style="color:#ff7b72">var&lt;/span> avail)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Results.Conflict(&lt;span style="color:#a5d6ff">$&amp;#34;Product {pid}: requested {req}, only {avail} available&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(PaymentDeclined(&lt;span style="color:#ff7b72">var&lt;/span> reason)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Results.UnprocessableEntity(&lt;span style="color:#a5d6ff">$&amp;#34;Payment declined: {reason}&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(ValidationFailed(&lt;span style="color:#ff7b72">var&lt;/span> errors)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Errors = errors })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Никаких блоков try-catch, забытых типов исключений и сюрпризов во время выполнения. Каждый режим отказа виден в сигнатуре типа и применяется компилятором. Это значительное улучшение надежности API.&lt;/p>
&lt;h3 id="моделирование-домена-типы-платежей">Моделирование домена: типы платежей&lt;/h3>
&lt;p>Вот реальный пример моделирования домена — обработка различных способов оплаты в системе электронной коммерции:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union PaymentMethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CreditCard(&lt;span style="color:#ff7b72">string&lt;/span> CardNumber, &lt;span style="color:#ff7b72">string&lt;/span> Expiry, &lt;span style="color:#ff7b72">string&lt;/span> Cvv),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BankTransfer(&lt;span style="color:#ff7b72">string&lt;/span> Iban, &lt;span style="color:#ff7b72">string&lt;/span> Bic),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DigitalWallet(&lt;span style="color:#ff7b72">string&lt;/span> Provider, &lt;span style="color:#ff7b72">string&lt;/span> Token),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CashOnDelivery
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">decimal&lt;/span> CalculateProcessingFee(PaymentMethod method, &lt;span style="color:#ff7b72">decimal&lt;/span> amount) =&amp;gt; method &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CreditCard _ =&amp;gt; amount * &lt;span style="color:#a5d6ff">0.029&lt;/span>m + &lt;span style="color:#a5d6ff">0.30&lt;/span>m,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BankTransfer _ =&amp;gt; &lt;span style="color:#a5d6ff">1.50&lt;/span>m,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DigitalWallet(provider: &lt;span style="color:#a5d6ff">&amp;#34;PayPal&amp;#34;&lt;/span>, _) =&amp;gt; amount * &lt;span style="color:#a5d6ff">0.034&lt;/span>m + &lt;span style="color:#a5d6ff">0.35&lt;/span>m,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DigitalWallet _ =&amp;gt; amount * &lt;span style="color:#a5d6ff">0.025&lt;/span>m,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CashOnDelivery =&amp;gt; &lt;span style="color:#a5d6ff">4.99&lt;/span>m
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> FormatForReceipt(PaymentMethod method) =&amp;gt; method &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CreditCard(&lt;span style="color:#ff7b72">var&lt;/span> num, _, _) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Credit Card ending in {num[^4..]}&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BankTransfer(&lt;span style="color:#ff7b72">var&lt;/span> iban, _) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Bank Transfer ({iban[..4]}****)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DigitalWallet(&lt;span style="color:#ff7b72">var&lt;/span> provider, _) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Digital Wallet ({provider})&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CashOnDelivery =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Cash on Delivery&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Сравните это с текущим подходом, в котором у вас есть интерфейс или абстрактный класс с пятью различными реализациями, распределенными по пяти файлам, возможно, с шаблоном посетителя сверху. Подход объединения сохраняет определение данных и операции вместе, читабельными и полностью проверяемыми.&lt;/p>
&lt;h3 id="государственные-машины">Государственные машины&lt;/h3>
&lt;p>Конечные автоматы присутствуют повсюду в программном обеспечении — обработка заказов, механизмы рабочих процессов, управление соединениями, состояние пользовательского интерфейса. Объединения делают их явными и безопасными:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union ConnectionState
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Disconnected,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Connecting(&lt;span style="color:#ff7b72">string&lt;/span> Host, &lt;span style="color:#ff7b72">int&lt;/span> Port, DateTime StartedAt),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Connected(&lt;span style="color:#ff7b72">string&lt;/span> Host, &lt;span style="color:#ff7b72">int&lt;/span> Port, TcpClient Client),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Reconnecting(&lt;span style="color:#ff7b72">string&lt;/span> Host, &lt;span style="color:#ff7b72">int&lt;/span> Port, &lt;span style="color:#ff7b72">int&lt;/span> Attempt, TimeSpan Delay),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Failed(&lt;span style="color:#ff7b72">string&lt;/span> Host, Exception Error)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ConnectionState HandleEvent(ConnectionState state, ConnectionEvent evt) =&amp;gt; (state, evt) &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Disconnected, Connect(&lt;span style="color:#ff7b72">var&lt;/span> host, &lt;span style="color:#ff7b72">var&lt;/span> port)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Connecting(host, port, DateTime.UtcNow),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Connecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, _), ConnectionSucceeded(&lt;span style="color:#ff7b72">var&lt;/span> client)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Connected(h, p, client),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Connecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, _), ConnectionFailed(&lt;span style="color:#ff7b72">var&lt;/span> ex)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Reconnecting(h, p, Attempt: &lt;span style="color:#a5d6ff">1&lt;/span>, Delay: TimeSpan.FromSeconds(&lt;span style="color:#a5d6ff">1&lt;/span>)),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Reconnecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, &lt;span style="color:#ff7b72">var&lt;/span> attempt, _), ConnectionSucceeded(&lt;span style="color:#ff7b72">var&lt;/span> client)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Connected(h, p, client),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Reconnecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, &lt;span style="color:#ff7b72">var&lt;/span> attempt, _), ConnectionFailed(&lt;span style="color:#ff7b72">var&lt;/span> ex)) when attempt &amp;gt;= &lt;span style="color:#a5d6ff">5&lt;/span> =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Failed(h, ex),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Reconnecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, &lt;span style="color:#ff7b72">var&lt;/span> attempt, _), ConnectionFailed(_)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Reconnecting(h, p, attempt + &lt;span style="color:#a5d6ff">1&lt;/span>, TimeSpan.FromSeconds(Math.Pow(&lt;span style="color:#a5d6ff">2&lt;/span>, attempt))),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Connected(_, _, &lt;span style="color:#ff7b72">var&lt;/span> client), Disconnect) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Disconnected,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ =&amp;gt; state &lt;span style="color:#8b949e;font-style:italic">// Ignore events that don&amp;#39;t apply to current state&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Каждое состояние несет в себе именно те данные, которые относятся к этому состоянию. Вы не можете случайно получить доступ к &lt;code>TcpClient&lt;/code> в состоянии &lt;code>Connecting&lt;/code>, поскольку он не существует в этом варианте. Система типов обеспечивает соблюдение инвариантов конечного автомата.&lt;/p>
&lt;h2 id="вопросы-сериализации-и-взаимодействияодин-из-практических-вопросов-который-сразу-же-возникает-в-связи-с-типами-объединения-как-их-сериализовать-если-вы-создаете-api-или-храните-данные-для-правильной-работы-вам-необходима-сериализация-json">Вопросы сериализации и взаимодействияОдин из практических вопросов, который сразу же возникает в связи с типами объединения: как их сериализовать? Если вы создаете API или храните данные, для правильной работы вам необходима сериализация JSON.&lt;/h2>
&lt;p>Команда разработчиков обсуждала встроенную поддержку &lt;code>System.Text.Json&lt;/code> для типов объединения. Ожидаемый подход предполагает сериализацию со свойством дискриминатора:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;$type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Circle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;radius&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">5.0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;$type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Rectangle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;width&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">10.0&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;height&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">20.0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это соответствует существующей поддержке полиморфной сериализации, представленной в .NET 7 с атрибутами &lt;code>JsonDerivedType&lt;/code>. Ожидается, что объединения будут работать с &lt;code>System.Text.Json&lt;/code> «из коробки», используя имя варианта в качестве дискриминатора типа по умолчанию.&lt;/p>
&lt;p>Для Entity Framework Core вероятным подходом является сохранение значений объединения с использованием столбца дискриминатора — аналогично тому, как уже работает сопоставление наследования «таблица на иерархию» (TPH). Точная интеграция EF Core все еще разрабатывается, но инфраструктура для обработки иерархий закрытого типа уже существует.&lt;/p>
&lt;p>Стоит отметить, что взаимодействие с другими языками .NET должно быть плавным, поскольку объединения будут компилироваться в стандартные иерархии классов IL. Код F#, использующий объединение C#, будет рассматривать его как стандартную иерархию типов, и наоборот.&lt;/p>
&lt;h2 id="как-попробовать">Как попробовать&lt;/h2>
&lt;p>На момент написания объединенные типы доступны в качестве предварительной функции в последних предварительных версиях .NET SDK. Чтобы поэкспериментировать с предложенным синтаксисом, вам необходимо:&lt;/p>
&lt;ol>
&lt;li>Установите последнюю версию SDK предварительной версии .NET.&lt;/li>
&lt;li>Включите языковую версию предварительного просмотра в файле проекта:&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;Project&lt;/span> Sdk=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.NET.Sdk&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PropertyGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;TargetFramework&amp;gt;&lt;/span>net10.0&lt;span style="color:#7ee787">&amp;lt;/TargetFramework&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;LangVersion&amp;gt;&lt;/span>preview&lt;span style="color:#7ee787">&amp;lt;/LangVersion&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/PropertyGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/Project&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Имейте в виду, что функции предварительной версии могут быть изменены. Синтаксис, поведение и диагностика компилятора могут значительно измениться до окончательного выпуска. Не отправляйте рабочий код, полагаясь на функции языка предварительной версии, но обязательно экспериментируйте с ними и оставляйте отзывы. Команда C# активно следит за обсуждениями репозитория &lt;a href="https://github.com/dotnet/csharplang">csharplan&lt;/a>.&lt;/p>
&lt;p>Если вы хотите следить за ходом рассмотрения предложения, обратите внимание на следующие ключевые места:&lt;/p>
&lt;ul>
&lt;li>Репозиторий &lt;a href="https://github.com/dotnet/csharplang">dotnet/csharplan&lt;/a> для обсуждений языкового дизайна.
— Репозиторий &lt;a href="https://github.com/dotnet/roslyn">dotnet/roslyn&lt;/a> для информации о ходе реализации компилятора.&lt;/li>
&lt;li>Блог .NET для официальных объявлений.&lt;/li>
&lt;/ul>
&lt;h2 id="сравнение-с-существующими-подходами">Сравнение с существующими подходами&lt;/h2>
&lt;p>Позвольте мне провести быстрое сравнение, чтобы вы могли увидеть, как складываются различные подходы:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Особенность&lt;/th>
&lt;th>Абстрактные записи&lt;/th>
&lt;th>OneOf&amp;lt;T1,T2&amp;gt;&lt;/th>
&lt;th>Перечисление + Данные&lt;/th>
&lt;th>Типы союзов&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Проверка полноты&lt;/td>
&lt;td>❌ Нет&lt;/td>
&lt;td>✅ Да&lt;/td>
&lt;td>❌ Нет&lt;/td>
&lt;td>✅ Да&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Сопоставление с образцом&lt;/td>
&lt;td>✅ Да&lt;/td>
&lt;td>❌ Ограниченная&lt;/td>
&lt;td>❌ Руководство&lt;/td>
&lt;td>✅ Родной&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Закрытие, принудительное компилятором&lt;/td>
&lt;td>❌ Нет&lt;/td>
&lt;td>✅ Да&lt;/td>
&lt;td>❌ Нет&lt;/td>
&lt;td>✅ Да&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Данные по варианту&lt;/td>
&lt;td>✅ Да&lt;/td>
&lt;td>✅ Да&lt;/td>
&lt;td>⚠️ Хрупкий&lt;/td>
&lt;td>✅ Да&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Читабельность&lt;/td>
&lt;td>⚠️ Подробный&lt;/td>
&lt;td>⚠️ Позиционный&lt;/td>
&lt;td>❌ Бедный&lt;/td>
&lt;td>✅ Отлично&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Сериализация&lt;/td>
&lt;td>✅ Руководство&lt;/td>
&lt;td>⚠️Комплекс&lt;/td>
&lt;td>✅ Руководство&lt;/td>
&lt;td>✅ Встроенный&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Шаблон&lt;/td>
&lt;td>⚠️ Умеренный&lt;/td>
&lt;td>✅ Низкий&lt;/td>
&lt;td>⚠️ Высокий&lt;/td>
&lt;td>✅ Минимальный&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Нет внешней зависимости&lt;/td>
&lt;td>✅ Да&lt;/td>
&lt;td>❌ NuGet&lt;/td>
&lt;td>✅ Да&lt;/td>
&lt;td>✅ Да&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="что-это-значит-для-экосистемы-net">Что это значит для экосистемы .NET&lt;/h2>
&lt;p>Введение дискриминационных профсоюзов отразится на всей экосистеме .NET. Вот что я ожидаю увидеть:&lt;/p>
&lt;p>&lt;strong>Дизайн библиотеки будет улучшен.&lt;/strong> API, которые в настоящее время возвращают &lt;code>null&lt;/code> для обозначения «не найдено» или выдают исключения в случае сбоев проверки, смогут вместо этого возвращать типы &lt;code>Result&amp;lt;T, E&amp;gt;&lt;/code> . Это делает режимы сбоя явными в сигнатуре типа — вы можете увидеть, что может пойти не так, взглянув на сигнатуру метода, а не читая документацию или исходный код.&lt;/p>
&lt;p>&lt;strong>Моделирование предметной области становится более выразительным.&lt;/strong> Разрыв между предметной областью задачи и представлением кода резко сокращается. Когда ваш эксперт в предметной области говорит: «Платеж может быть кредитной картой, банковским переводом или наложенным платежом», вы можете смоделировать это непосредственно как объединение, а не переводить его в иерархию наследования.&lt;/p>
&lt;p>&lt;strong>Идеи F# становятся доступными разработчикам C#.&lt;/strong> Многие разработчики C# восхищались системой типов F# на расстоянии, но не смогли внедрить F# в своих организациях. Типы объединения привносят в C# одну из самых мощных функций F#, что является победой для всей экосистемы .NET.&lt;/p>
&lt;p>&lt;strong>Меньше ошибок во время выполнения.&lt;/strong> Одна только проверка полноты позволит предотвратить целые категории ошибок. Каждый раз, когда вы добавляете в объединение новый вариант, компилятор покажет вам все места в кодовой базе, требующие обновления. Больше никаких забытых случаев &lt;code>switch&lt;/code> и &lt;code>NotImplementedException&lt;/code> в ветках по умолчанию, которые появляются только в рабочей среде.&lt;/p>
&lt;h2 id="заключение">Заключение&lt;/h2>
&lt;p>Дискриминированные профсоюзы уже почти десять лет находятся на вершине списка пожеланий сообщества C#, и не без причины. Они устраняют фундаментальный пробел в системе типов — возможность моделировать данные, которые могут быть «одной из нескольких вещей» с безопасностью, обеспечиваемой компилятором.&lt;/p>
&lt;p>Предлагаемое ключевое слово &lt;code>union&lt;/code> обеспечивает чистый, лаконичный синтаксис, который глубоко интегрируется с сопоставлением шаблонов C#, работает с универсальными шаблонами, поддерживает методы и интерфейсы и обеспечивает исчерпывающую проверку, которая выявляет ошибки во время компиляции, а не во время выполнения.&lt;/p>
&lt;p>Создаете ли вы модели предметной области, разрабатываете API с явными типами ошибок, реализуете конечные автоматы или просто пытаетесь заменить неудобные иерархии наследования чем-то более естественным — типы объединения изменят то, как вы пишете на C#.&lt;/p>
&lt;p>Мы долго этого ждали. Синтаксис элегантен, интеграция с существующими функциями языка продумана, а практическое влияние будет ощущаться во всей экосистеме .NET.&lt;/p>
&lt;p>Следите за предварительными версиями, экспериментируйте с этой функцией и оставляйте отзывы команде разработчиков языка. Это одна из тех функций, воспользовавшись которой, вы удивитесь, как вы раньше жили без нее.&lt;/p></content:encoded><category>.NET</category><category>C#</category></item><item><title>Создание системы RAG на C# с семантическим ядром</title><link>https://emimontesdeoca.github.io/ru/posts/rag-csharp-semantic-kernel/</link><pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/rag-csharp-semantic-kernel/</guid><description>Реализуйте расширенную генерацию с поиском на C# с использованием семантического ядра, внедрений и векторного поиска.</description><content:encoded>&lt;h2 id="введение">Введение&lt;/h2>
&lt;p>Если вы пытались использовать LLM для ответа на вопросы о ваших собственных данных — документах компании, спецификациях продуктов, внутренних базах знаний — вы, вероятно, заметили, что это либо галлюцинирует, либо просто говорит: «У меня нет информации об этом». Это потому, что модель знает только то, чему она обучалась.&lt;/p>
&lt;p>RAG (Расширенная генерация извлечения) исправляет это. Вместо точной настройки модели на основе ваших данных вы извлекаете соответствующие фрагменты своих документов во время запроса и передаете их в LLM в качестве контекста. Затем модель генерирует ответы, основанные на ваших фактических данных.&lt;/p>
&lt;p>В этом посте я расскажу вам, как создать полный конвейер RAG на C# с использованием семантического ядра.&lt;/p>
&lt;h2 id="как-работает-rag">Как работает RAG&lt;/h2>
&lt;p>Порядок действий прост:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Всасывание&lt;/strong>: разбивайте документы на фрагменты, создавайте вложения для каждого фрагмента и сохраняйте их в векторной базе данных.&lt;/li>
&lt;li>&lt;strong>Запрос&lt;/strong>: когда пользователь задает вопрос, сгенерируйте вложение для запроса, выполните поиск в базе данных векторов похожих фрагментов.&lt;/li>
&lt;li>&lt;strong>Создать&lt;/strong>: передать полученные фрагменты в качестве контекста в LLM вместе с вопросом пользователя.&lt;/li>
&lt;/ol>
&lt;p>Вот и все. Волшебство заключается во встраиваниях — они фиксируют семантическое значение текста в виде векторов, поэтому вы можете найти релевантный контент, даже если точные слова не совпадают.&lt;/p>
&lt;h2 id="предварительные-условия">Предварительные условия&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.AzureOpenAI
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.Extensions.VectorData.Abstractions
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.InMemory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>В рабочей среде вы можете заменить хранилище в памяти на Azure AI Search, Qdrant, Pinecone или любую другую поддерживаемую базу данных векторов. Но in-memory идеально подходит для обучения и прототипирования.&lt;/p>
&lt;h2 id="настройка-ядра">Настройка ядра&lt;/h2>
&lt;p>[[[ТОК_1]]]&lt;/p>
&lt;p>Нам нужны две модели: одна для завершения чата (ответы на вопросы) и одна для генерации вложений (преобразование текста в векторы).&lt;/p>
&lt;h2 id="определение-модели-данных">Определение модели данных&lt;/h2>
&lt;p>Нам нужен класс для представления фрагментов нашего документа в векторном хранилище:&lt;/p>
&lt;p>[[[ТОК_2]]]&lt;/p>
&lt;p>Атрибут &lt;code>VectorStoreRecordVector(1536)&lt;/code> сообщает векторному хранилищу размерность наших вложений. Модель &lt;code>text-embedding-3-small&lt;/code> создает 1536-мерные векторы.&lt;/p>
&lt;h2 id="разбиение-документов-на-части">Разбиение документов на части&lt;/h2>
&lt;p>Прежде чем мы сможем создавать вложения, нам нужно разделить наши документы на управляемые фрагменты. Вот простой разделитель текста:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TextChunker&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; SplitText(&lt;span style="color:#ff7b72">string&lt;/span> text, &lt;span style="color:#ff7b72">int&lt;/span> maxChunkSize = &lt;span style="color:#a5d6ff">500&lt;/span>, &lt;span style="color:#ff7b72">int&lt;/span> overlap = &lt;span style="color:#a5d6ff">50&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> chunks = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> paragraphs = text.Split(&lt;span style="color:#a5d6ff">&amp;#34;\n\n&amp;#34;&lt;/span>, StringSplitOptions.RemoveEmptyEntries);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> currentChunk = &lt;span style="color:#ff7b72">new&lt;/span> System.Text.StringBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> paragraph &lt;span style="color:#ff7b72">in&lt;/span> paragraphs)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (currentChunk.Length + paragraph.Length &amp;gt; maxChunkSize &amp;amp;&amp;amp; currentChunk.Length &amp;gt; &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chunks.Add(currentChunk.ToString().Trim());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Keep overlap from the end of the previous chunk&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> overlapText = currentChunk.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (overlapText.Length &amp;gt; overlap)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Append(overlapText[^overlap..]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Append(&lt;span style="color:#a5d6ff">&amp;#39; &amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Append(paragraph);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Append(&lt;span style="color:#a5d6ff">&amp;#34;\n\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (currentChunk.Length &amp;gt; &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chunks.Add(currentChunk.ToString().Trim());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> chunks;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Перекрытие важно — оно гарантирует, что контекст на границе между фрагментами не будет потерян. Если соответствующее предложение разделено на два фрагмента, перекрытие означает, что оно появится полностью хотя бы в одном из них.&lt;/p>
&lt;h2 id="загрузка-документов">Загрузка документов&lt;/h2>
&lt;p>Теперь давайте соберем все это вместе, чтобы загрузить документы в наше векторное хранилище:&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;h2 id="поиск-соответствующих-фрагментов">Поиск соответствующих фрагментов&lt;/h2>
&lt;p>Когда пользователь задает вопрос, мы генерируем вложение для его запроса и ищем похожие фрагменты:&lt;/p>
&lt;p>[[[ТОК_7]]]&lt;/p>
&lt;h2 id="генерация-ответов-с-учетом-контекста">Генерация ответов с учетом контекста&lt;/h2>
&lt;p>Теперь часть RAG — мы берем полученные фрагменты и включаем их в качестве контекста в нашу подсказку:&lt;/p>
&lt;p>[[[ТОК_8]]]&lt;/p>
&lt;h2 id="использование-этого">Использование этого&lt;/h2>
&lt;p>[[[ТОК_9]]]&lt;/p>
&lt;h2 id="переезд-в-производство">Переезд в производство&lt;/h2>
&lt;p>Хранилище векторов в памяти отлично подходит для прототипирования, но для производства вам понадобится постоянная база данных векторов. Семантическое ядро имеет коннекторы для нескольких опций:&lt;/p>
&lt;p>[[[ТОК_10]]]&lt;/p>
&lt;p>Заменить местами очень просто, поскольку все они реализуют один и тот же интерфейс &lt;code>IVectorStore&lt;/code>:&lt;/p>
&lt;p>[[[ТОК_12]]]Все остальное остается прежним. В этом красота абстракции.&lt;/p>
&lt;h2 id="советы-по-созданию-rag-систем">Советы по созданию RAG-систем&lt;/h2>
&lt;p>Несколько вещей, которые я усвоил на собственном горьком опыте:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Размер блока имеет большое значение.&lt;/strong> Если он слишком мал, вы теряете контекст. Слишком большой размер — и вы потратите токены на нерелевантный контент. Начните с 500-800 токенов и корректируйте их в зависимости от ваших данных.&lt;/li>
&lt;li>&lt;strong>Перекрытие предотвращает проблемы с границами.&lt;/strong> Обычно достаточно перекрытия в 50–100 токенов между фрагментами.&lt;/li>
&lt;li>&lt;strong>Получите больше, чем вы думаете.&lt;/strong> Начните с &lt;code>topK = 5&lt;/code> и уменьшите, если вы слышите слишком много шума. Лучше иметь дополнительный контекст, чем пропустить соответствующий фрагмент.
– &lt;strong>Системные подсказки имеют решающее значение.&lt;/strong> Будьте предельно откровенны, используя только предоставленный контекст. Без этой инструкции модель будет с радостью галлюцинировать «на основе своих обучающих данных».&lt;/li>
&lt;li>&lt;strong>Отслеживайте источники.&lt;/strong> Всегда сохраняйте метаданные вместе с фрагментами, чтобы вы могли указать, откуда пришел ответ. Пользователи больше доверяют ответам, когда могут проверить источник.&lt;/li>
&lt;li>&lt;strong>При необходимости измените ранжирование.&lt;/strong> Сходство векторов не является идеальным. Для критически важных приложений добавьте этап переранжирования с использованием модели перекрестного кодирования для повышения точности.&lt;/li>
&lt;/ul>
&lt;h2 id="заключение">Заключение&lt;/h2>
&lt;p>RAG на данный момент является одним из самых практичных шаблонов в области искусственного интеллекта. Он позволяет создавать системы вопросов и ответов на основе искусственного интеллекта на основе ваших собственных данных без тонкой настройки, а семантическое ядро ​​делает их на C# удивительно чистыми. Начните с хранилища в памяти, правильно разбейте фрагменты и подсказки, а затем замените реальную векторную базу данных, когда будете готовы к работе.&lt;/p>
&lt;p>Приятного кодирования!&lt;/p>
&lt;h2 id="ресурсы">Ресурсы&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/vector-store-connectors/">Документация по хранилищу векторов семантического ядра&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/azure/search/retrieval-augmented-generation-overview">шаблон RAG с поиском Azure AI&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#embeddings">Модели встраивания текста&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>AI</category><category>Semantic Kernel</category><category>C#</category><category>Azure</category></item><item><title>Создание рабочих процессов агента с помощью Microsoft Agent Framework</title><link>https://emimontesdeoca.github.io/ru/posts/agent-framework-workflows/</link><pubDate>Tue, 10 Feb 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/agent-framework-workflows/</guid><description>Проектируйте и организуйте последовательные и параллельные рабочие процессы агентов с помощью Microsoft Agent Framework в .NET.</description><content:encoded>&lt;h2 id="введение">Введение&lt;/h2>
&lt;p>Если вы читали мои предыдущие публикации об Agent Framework от Microsoft, вы знаете, как создавать отдельных агентов и даже групповые чаты с несколькими агентами. Но в реальных сценариях вам часто требуется что-то более структурированное — рабочий процесс, в котором агенты выполняются в определенном порядке, передают результаты друг другу и обрабатывают логику ветвления на основе результатов.&lt;/p>
&lt;p>Именно это дают вам возможности рабочего процесса в Agent Framework. Вместо того, чтобы бросать агентов в групповой чат и надеяться, что они это поймут, вы определяете явные шаги, зависимости и поток данных между агентами.&lt;/p>
&lt;h2 id="что-такое-рабочие-процессы-агента">Что такое рабочие процессы агента?&lt;/h2>
&lt;p>Думайте о рабочих процессах агентов как о конвейере. Каждый шаг обрабатывается специализированным агентом, а результаты одного шага передаются на следующий. Вы можете выполнять шаги последовательно, параллельно или условно в зависимости от результатов.&lt;/p>
&lt;p>Несколько примеров:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Контроллер контента&lt;/strong>: Исследование → Черновик → Обзор → Публикация.&lt;/li>
&lt;li>&lt;strong>Обработка данных&lt;/strong>: Извлечение → Преобразование → Проверка → Загрузка.&lt;/li>
&lt;li>&lt;strong>Поддержка клиентов&lt;/strong>: Классифицировать → Маршрут → Ответ → Последующие действия.&lt;/li>
&lt;/ul>
&lt;h2 id="предварительные-условия">Предварительные условия&lt;/h2>
&lt;p>Убедитесь, что у вас установлены последние версии пакетов Agent Framework:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Agents.Core
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>И настроена конечная точка Azure OpenAI или OpenAI.&lt;/p>
&lt;h2 id="построение-последовательного-рабочего-процесса">Построение последовательного рабочего процесса&lt;/h2>
&lt;p>Давайте построим рабочий процесс создания контента с тремя агентами: исследователем, писателем и редактором.&lt;/p>
&lt;h3 id="определение-агентов">Определение агентов&lt;/h3>
&lt;p>[[[ТОК_1]]]&lt;/p>
&lt;h3 id="выполнение-последовательно">Выполнение последовательно&lt;/h3>
&lt;p>Самый простой рабочий процесс является последовательным — каждый агент обрабатывает выходные данные предыдущего:&lt;/p>
&lt;p>[[[ТОК_2]]]&lt;/p>
&lt;p>Каждый агент получает накопленный контекст, обрабатывает его, и результат переходит на следующий шаг.&lt;/p>
&lt;h2 id="параллельное-выполнение">Параллельное выполнение&lt;/h2>
&lt;p>Иногда агенты могут работать независимо. Например, вы можете захотеть исследовать несколько подтем одновременно:&lt;/p>
&lt;p>[[[ТОК_3]]]&lt;/p>
&lt;p>Затем вы можете передать все исследовательские обзоры одному агенту-писателю, чтобы создать один связный пост.&lt;/p>
&lt;h2 id="условное-ветвление">Условное ветвление&lt;/h2>
&lt;p>Реальные рабочие процессы требуют решений. Возможно, вам нужен этап проверки качества, который направляется обратно автору, если сообщение недостаточно хорошее:&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;p>Этот шаблон очень полезен. Средство проверки качества действует как шлюз, и рабочий процесс повторяется до тех пор, пока результат не станет достаточно хорошим или не достигнет максимального предела версии.&lt;/p>
&lt;h2 id="добавление-плагинов-к-агентам-рабочих-процессов">Добавление плагинов к агентам рабочих процессов&lt;/h2>
&lt;p>Агенты в рабочих процессах могут использовать плагины так же, как и автономные агенты. Здесь все становится действительно мощно — агенты могут вызывать API, запрашивать базы данных или выполнять операции с файлами в рамках своего рабочего процесса:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> researcherWithTools = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Researcher&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;Research the topic using available tools. Summarize findings.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add a web search plugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>kernel.Plugins.AddFromType&amp;lt;WebSearchPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add a database plugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>kernel.Plugins.AddFromObject(&lt;span style="color:#ff7b72">new&lt;/span> DatabasePlugin(connectionString), &lt;span style="color:#a5d6ff">&amp;#34;Database&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;h2 id="обработка-ошибок-в-рабочих-процессах">Обработка ошибок в рабочих процессах&lt;/h2>
&lt;p>Когда вы объединяете несколько агентов, обработка ошибок становится критически важной. Вы не хотите, чтобы один неудачный шаг незаметно привел к сбою всего рабочего процесса:&lt;/p>
&lt;p>[[[ТОК_7]]]&lt;/p>
&lt;h2 id="лучшие-практики">Лучшие практики&lt;/h2>
&lt;p>Построив несколько рабочих процессов агентов, я узнал следующее:- &lt;strong>Соблюдайте четкость инструкций&lt;/strong> — каждый агент должен хорошо выполнять одну задачу. Не пытайтесь сделать агента, использующего швейцарский армейский нож.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Ограничить контекст&lt;/strong> — не передавайте всю историю разговоров каждому агенту. Давайте каждому шагу только то, что ему нужно.&lt;/li>
&lt;li>&lt;strong>Устанавливайте ограничения на версии&lt;/strong> — циклы контроля качества хороши, но могут работать вечно, если вы не будете осторожны. Всегда имейте максимальное количество итераций.&lt;/li>
&lt;li>&lt;strong>Записывайте все&lt;/strong> — результаты работы агента могут быть непредсказуемыми. Регистрируйте ввод и вывод каждого шага для отладки.&lt;/li>
&lt;li>&lt;strong>Используйте параллельное выполнение с умом&lt;/strong> — это ускоряет работу, но следите за ограничениями скорости API и стоимостью токенов.&lt;/li>
&lt;li>&lt;strong>Сначала протестируйте модели меньшего размера&lt;/strong>. Разработайте и протестируйте логику рабочего процесса с помощью GPT-3.5, прежде чем переходить на GPT-4o для производства.&lt;/li>
&lt;/ul>
&lt;h2 id="заключение">Заключение&lt;/h2>
&lt;p>Рабочие процессы агентов позволяют создавать сложные многоэтапные конвейеры искусственного интеллекта, в которых каждый агент является специалистом. Начните с последовательных рабочих процессов, добавьте параллельное выполнение, где шаги независимы, и используйте условное ветвление для контроля качества. Шаблоны можно компоновать — как только вы освоите их, вы сможете создать довольно сложную систему автоматизации.&lt;/p>
&lt;p>Приятного кодирования!&lt;/p>
&lt;h2 id="ресурсы">Ресурсы&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/frameworks/agent">Документация Microsoft Agent Framework&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithAgents">Образцы агентов семантического ядра&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>AI</category><category>Agent Framework</category><category>Semantic Kernel</category></item><item><title>Жизненный цикл компонента Blazor: полное руководство</title><link>https://emimontesdeoca.github.io/ru/posts/blazor-component-lifecycle/</link><pubDate>Thu, 15 Jan 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/blazor-component-lifecycle/</guid><description>Поймите каждый метод жизненного цикла компонента Blazor от инициализации до утилизации, а также когда использовать каждый из них.</description><content:encoded>&lt;p>Я использую Blazor уже некоторое время, и, честно говоря, методы жизненного цикла поначалу меня смутили. &lt;code>OnInitialized&lt;/code> против &lt;code>OnInitializedAsync&lt;/code>? &lt;code>OnParametersSet&lt;/code> против &lt;code>OnAfterRender&lt;/code>? Когда &lt;code>StateHasChanged&lt;/code> фактически запускает повторный рендеринг? После многих проб и ошибок у меня наконец появилась твердая мысленная модель всего этого.&lt;/p>
&lt;h1 id="краткий-обзор-жизненного-цикла">Краткий обзор жизненного цикла&lt;/h1>
&lt;p>При рендеринге компонента Blazor он проходит через следующие методы по порядку:&lt;/p>
&lt;ol>
&lt;li>[[[ТОК_5]]]&lt;/li>
&lt;li>[[[ТОК_6]]] / [[[ТОК_7]]]&lt;/li>
&lt;li>[[[ТОК_8]]] / [[[ТОК_9]]]&lt;/li>
&lt;li>[[[ТОК_10]]] / [[[ТОК_11]]]&lt;/li>
&lt;li>[[[ТОК_12]]] / [[[ТОК_13]]]&lt;/li>
&lt;/ol>
&lt;p>Давайте пройдемся по каждому.&lt;/p>
&lt;h1 id="установитьпараметрыасинхронно">УстановитьПараметрыАсинхронно&lt;/h1>
&lt;p>Это самый первый вызываемый метод. Он получает необработанный &lt;code>ParameterView&lt;/code> от родительского компонента. В большинстве случаев вы не переопределяете это — Blazor автоматически обрабатывает параметры сопоставления. Но если вам нужна обработка или проверка пользовательских параметров перед их назначением:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task SetParametersAsync(ParameterView parameters)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Custom logic before parameters are set&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (parameters.TryGetValue&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">out&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> title))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Title is being set to: {title}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">base&lt;/span>.SetParametersAsync(parameters);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Я использовал это ровно один раз в реальном проекте. Большую часть времени вы пропустите это.&lt;/p>
&lt;h1 id="oninitializedoninitializedasync">OnInitialized/OnInitializedAsync&lt;/h1>
&lt;p>Здесь вы выполняете работу по настройке — загружаете данные, инициализируете службы, устанавливаете значения по умолчанию. Он запускается &lt;strong>один раз&lt;/strong> при первом создании компонента.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;Product&amp;gt; products = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> products = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/products&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Одна вещь, которая меня смутила: на Blazor Server &lt;code>OnInitializedAsync&lt;/code> вызывается &lt;strong>дважды&lt;/strong> во время предварительного рендеринга. Первый раз во время предварительной отрисовки на стороне сервера, а второй раз — при установке соединения SignalR. Если ваш вызов API является дорогостоящим, вы можете решить эту проблему:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> isPrerendering = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> products = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/products&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> isPrerendering = &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Или, что еще лучше, используйте &lt;code>PersistentComponentState&lt;/code>, чтобы полностью избежать двойного вызова.&lt;/p>
&lt;h1 id="onparametersset--onparameterssetasync">OnParametersSet / OnParametersSetAsync&lt;/h1>
&lt;p>Это срабатывает каждый раз, когда родительский компонент выполняет повторную визуализацию и передает новые значения параметров. Он также срабатывает после &lt;code>OnInitialized&lt;/code>. Это подходящее место для реагирования на изменения параметров:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[Parameter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> CategoryId { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> previousCategoryId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;Product&amp;gt; products = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnParametersSetAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (CategoryId != previousCategoryId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> previousCategoryId = CategoryId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> products = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">$&amp;#34;api/products?category={CategoryId}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Проверка на &lt;code>CategoryId != previousCategoryId&lt;/code> важна — без нее вы бы перезагружали данные каждый раз, когда родительский элемент выполняет повторную визуализацию, даже если категория не изменилась.&lt;/p>
&lt;h1 id="onafterrender--onafterrenderasync">OnAfterRender / OnAfterRenderAsync&lt;/h1>
&lt;p>Это срабатывает после того, как компонент отображается в DOM. Параметр &lt;code>firstRender&lt;/code> сообщает вам, является ли это первоначальным рендерингом. Это место для вызовов JS Interop, поскольку на этом этапе существуют элементы DOM:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[Inject]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> IJSRuntime JS { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnAfterRenderAsync(&lt;span style="color:#ff7b72">bool&lt;/span> firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> JS.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;initializeChart&amp;#34;&lt;/span>, chartElement);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Я часто использую &lt;code>firstRender&lt;/code>, чтобы избежать повторной инициализации библиотек JavaScript при каждом повторном рендеринге. Если вы настраиваете прослушиватели событий, библиотеки диаграмм или что-то, что напрямую касается DOM, это то, что вам нужно.&lt;/p>
&lt;h1 id="долженrender">ДолженRender&lt;/h1>
&lt;p>Этот менее известен, но очень полезен. Он контролирует, будет ли компонент выполнять повторную визуализацию при вызове &lt;code>StateHasChanged&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> shouldRender = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> ShouldRender() =&amp;gt; shouldRender;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> HeavyOperation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> shouldRender = &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Do a bunch of state changes without triggering renders&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">for&lt;/span> (&lt;span style="color:#ff7b72">int&lt;/span> i = &lt;span style="color:#a5d6ff">0&lt;/span>; i &amp;lt; &lt;span style="color:#a5d6ff">1000&lt;/span>; i++)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> items[i].Process();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> shouldRender = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged(); &lt;span style="color:#8b949e;font-style:italic">// Now render once with all changes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Я использовал это для оптимизации компонентов, обрабатывающих большие списки. Вместо повторного рендеринга при каждом изменении элемента вы выполняете пакетную обработку обновлений и выполняете рендеринг один раз в конце.&lt;/p>
&lt;h1 id="disposedisposeasync">Dispose/DisposeAsync&lt;/h1>
&lt;p>Когда компонент удаляется из пользовательского интерфейса, вам следует очистить все ресурсы. Реализуйте &lt;code>IDisposable&lt;/code> или &lt;code>IAsyncDisposable&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@implements IAsyncDisposable
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> Timer? timer;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> IJSObjectReference? jsModule;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnInitialized()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> timer = &lt;span style="color:#ff7b72">new&lt;/span> Timer(OnTick, &lt;span style="color:#79c0ff">null&lt;/span>, &lt;span style="color:#a5d6ff">0&lt;/span>, &lt;span style="color:#a5d6ff">1000&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnAfterRenderAsync(&lt;span style="color:#ff7b72">bool&lt;/span> firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> jsModule = &lt;span style="color:#ff7b72">await&lt;/span> JS.InvokeAsync&amp;lt;IJSObjectReference&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;import&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;./timer.js&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> ValueTask DisposeAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> timer?.Dispose();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (jsModule &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> jsModule.DisposeAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Общие вещи, которые нужно удалить: таймеры, подписки на события, ссылки на модули JS, &lt;code>CancellationTokenSource&lt;/code> и любые созданные вами службы &lt;code>IDisposable&lt;/code>.&lt;/p>
&lt;h1 id="состояниеизмененоэто-не-метод-жизненного-цикла-но-он-тесно-связан-он-сообщает-blazor-эй-мое-состояние-изменилось-пожалуйста-перерисуйте-меня-blazor-вызывает-его-автоматически-после-обработчиков-событий-но-иногда-вам-нужно-вызвать-его-вручную--обычно-когда-состояние-изменяется-вне-обычного-потока-событий-blazor">СостояниеИзмененоЭто не метод жизненного цикла, но он тесно связан. Он сообщает Blazor: «Эй, мое состояние изменилось, пожалуйста, перерисуйте меня». Blazor вызывает его автоматически после обработчиков событий, но иногда вам нужно вызвать его вручную — обычно, когда состояние изменяется вне обычного потока событий Blazor:&lt;/h1>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task StartPolling()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">while&lt;/span> (!cts.Token.IsCancellationRequested)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> data = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;Data&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/data&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged(); &lt;span style="color:#8b949e;font-style:italic">// Manual call needed since this isn&amp;#39;t a Blazor event&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.Delay(&lt;span style="color:#a5d6ff">5000&lt;/span>, cts.Token);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Одно важное замечание: если вы обновляетесь из фонового потока на Blazor Server, используйте &lt;code>InvokeAsync&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> InvokeAsync(() =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> data = newData;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="полная-картина">Полная картина&lt;/h1>
&lt;p>Вот порядок, в котором все происходит:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>Component created
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ SetParametersAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnInitialized / OnInitializedAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnParametersSet / OnParametersSetAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ Render
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnAfterRender(firstRender: true)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Parameter change from parent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ SetParametersAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnParametersSet / OnParametersSetAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ ShouldRender?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ Render
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnAfterRender(firstRender: false)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Component removed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ Dispose / DisposeAsync
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Если у вас в голове этот поток, отладка проблем жизненного цикла становится намного проще.&lt;/p>
&lt;p>Надеюсь, вам понравился пост! Не стесняйтесь обращаться ко мне в любой социальной сети по адресу &lt;strong>@emimontesdeoca&lt;/strong>.&lt;/p>
&lt;h1 id="ресурсы">Ресурсы&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle">Жизненный цикл компонента ASP.NET Core Blazor&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle#component-disposal-with-idisposable-and-iasyncdisposable">Удаление компонентов с помощью IDisposable и IAsyncDisposable&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>C#</category></item><item><title>Microsoft Agent Framework спасет Рождество</title><link>https://emimontesdeoca.github.io/ru/posts/agent-framework-christmas-presents/</link><pubDate>Tue, 16 Dec 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/agent-framework-christmas-presents/</guid><description>Создайте мультиагентную систему покупки рождественских подарков, используя Microsoft Agent Framework с .NET.</description><content:encoded>&lt;h2 id="введение">Введение&lt;/h2>
&lt;p>Поиск идеальных рождественских подарков может оказаться стрессом. Между мозговым штурмом идей подарков, сравнением цен в разных магазинах и проверкой того, чтобы все было доставлено вовремя, праздничные покупки быстро становятся непосильными. Что, если бы мы могли делегировать эти задачи специализированным агентам ИИ, которые работали бы вместе? В этом посте мы рассмотрим, как использовать Microsoft Agent Framework для создания многоагентной системы, в которой каждый агент специализируется на определенной задаче, от генерации идей для подарков до сравнения цен, и все это координируется посредством рабочих процессов.&lt;/p>
&lt;h2 id="праздничный-технический-календарь-на-2025-год">Праздничный технический календарь на 2025 год&lt;/h2>
&lt;p выравнивание="центр">
&lt;img src="https://sessionize.com/image/49aa-1140o400o3-sdJUGhdR3FCmm1KuPRM3D3.png"/>
&lt;/p>
&lt;p>Этот проект является частью моего выступления на &lt;strong>Праздничном техническом календаре 2025&lt;/strong>, замечательном общественном мероприятии, посвященном технологиям в праздничный сезон. Дополнительную информацию о мероприятии можно найти на странице &lt;a href="https://sessionize.com/festive-tech-calendar-2025/">Sessionize&lt;/a>.&lt;/p>
&lt;h2 id="что-такое-платформа-агентов-microsoft">Что такое платформа агентов Microsoft?&lt;/h2>
&lt;p>Agent Framework — это решение Microsoft для создания, координации и развертывания агентов искусственного интеллекта и многоагентных систем. Он обеспечивает гибкую основу для создания агентов, которые могут работать последовательно, одновременно или по шаблонам передачи обслуживания. Платформа поддерживает модели OpenAI, Azure OpenAI и Microsoft Foundry, что делает ее невероятно универсальной.&lt;/p>
&lt;p>Ключевые особенности включают в себя:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Многоагентная оркестровка&lt;/strong>: групповой чат, последовательный, одновременный и шаблоны передачи обслуживания.&lt;/li>
&lt;li>&lt;strong>Экосистема плагинов&lt;/strong>: расширение за счет встроенных функций, OpenAPI и протокола контекста модели (MCP).&lt;/li>
&lt;li>&lt;strong>Поддержка рабочих процессов&lt;/strong>: создавайте сложные конвейеры агентов с исполнителями и границами.&lt;/li>
&lt;/ul>
&lt;h2 id="предварительные-условия">Предварительные условия&lt;/h2>
&lt;p>Прежде чем углубляться в код, убедитесь, что у вас есть:&lt;/p>
&lt;ul>
&lt;li>.NET 9&lt;/li>
&lt;li>Доступ к Azure OpenAI (или ключ API OpenAI)&lt;/li>
&lt;li>Visual Studio или код Visual Studio&lt;/li>
&lt;/ul>
&lt;p>Установите необходимые пакеты (обратите внимание, что в предварительной версии требуется флаг &lt;code>--prerelease&lt;/code>:&lt;/p>
&lt;p>[[[ТОК_3]]]&lt;/p>
&lt;h2 id="агенты-по-покупке-подарков">Агенты по покупке подарков&lt;/h2>
&lt;p>Наш поисковик рождественских подарков будет состоять из трех специализированных агентов, работающих вместе:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Агент по идеям подарков&lt;/strong> — генерирует креативные предложения подарков на основе профиля получателя.&lt;/li>
&lt;li>&lt;strong>Агент сравнения цен&lt;/strong> — находит лучшие цены в разных магазинах.&lt;/li>
&lt;li>&lt;strong>Агент сводки&lt;/strong> – собирает окончательные рекомендации.&lt;/li>
&lt;/ol>
&lt;h2 id="модели">Модели&lt;/h2>
&lt;p>Давайте начнем с определения наших моделей данных, которые будут проходить через конвейер агента:&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;h2 id="создание-агентов">Создание агентов&lt;/h2>
&lt;p>Прелесть Agent Framework в том, насколько легко создавать специализированные агенты. Каждый агент представляет собой просто &lt;code>ChatClientAgent&lt;/code> со специальным системным приглашением, определяющим его опыт.&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;h2 id="создание-рабочего-процесса">Создание рабочего процесса&lt;/h2>
&lt;p>Теперь самое интересное — подключение наших агентов к последовательному рабочему процессу. Платформа агентов предоставляет &lt;code>WorkflowBuilder&lt;/code> и &lt;code>AgentWorkflowBuilder&lt;/code> для объединения агентов в различные шаблоны.&lt;/p>
&lt;p>[[[ТОК_9]]]&lt;/p>
&lt;h2 id="одновременный-запуск-агентов">Одновременный запуск агентов&lt;/h2>
&lt;p>Что делать, если мы хотим искать подарки для нескольких человек одновременно? Agent Framework поддерживает одновременное выполнение, что идеально подходит для этого сценария:&lt;/p>
&lt;p>[[[ТОК_10]]]&lt;/p>
&lt;h2 id="пользовательские-исполнители-для-большего-контролядля-более-сложных-сценариев-вы-можете-создавать-собственные-исполнители-которые-дают-вам-детальный-контроль-над-рабочим-процессом">Пользовательские исполнители для большего контроляДля более сложных сценариев вы можете создавать собственные исполнители, которые дают вам детальный контроль над рабочим процессом:&lt;/h2>
&lt;p>[[[ТОК_11]]]&lt;/p>
&lt;p>Затем вы можете вставить этот исполнитель в свой рабочий процесс:&lt;/p>
&lt;p>[[[ТОК_12]]]&lt;/p>
&lt;h2 id="добавление-реального-веб-поиска-с-заземлением-bing">Добавление реального веб-поиска с заземлением Bing&lt;/h2>
&lt;p>На данный момент наши агенты генерируют ответы на основе знаний модели ИИ. Но что, если мы хотим найти в Интернете актуальные цены и наличие товаров? Именно здесь на помощь приходит &lt;strong>Обоснование с помощью Bing Search&lt;/strong>. Это инструмент, доступный в Microsoft Foundry (ранее Azure AI Foundry), который позволяет вашим агентам включать общедоступные веб-данные в реальном времени при формировании ответов.&lt;/p>
&lt;p>Сначала вам необходимо создать ресурс &lt;strong>Заземление с помощью поиска Bing&lt;/strong> на &lt;a href="https://portal.azure.com/#create/Microsoft.BingGroundingSearch">Портале Azure&lt;/a>. Обязательно создайте его в той же группе ресурсов, что и ваш проект ИИ.&lt;/p>
&lt;h3 id="настройка-заземления-с-помощью-поиска-bing">Настройка заземления с помощью поиска Bing&lt;/h3>
&lt;p>Прелесть заземления с помощью Bing Search заключается в том, что оно напрямую интегрируется с агентами Azure AI. Агент решает, когда использовать инструмент поиска, на основе запроса пользователя, выполняет поиск в Интернете и использует результаты для генерации обоснованного ответа.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.AI.Agents.Persistent&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">BingGroundingSetup&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;PersistentAgent&amp;gt; CreateAgentWithBingGroundingAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> projectEndpoint,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> modelDeploymentName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> bingConnectionId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create the Persistent Agents client&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> agentClient = &lt;span style="color:#ff7b72">new&lt;/span> PersistentAgentsClient(projectEndpoint, &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Configure the Bing Grounding tool&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bingGroundingTool = &lt;span style="color:#ff7b72">new&lt;/span> BingGroundingToolDefinition(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> BingGroundingSearchToolParameters(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [new BingGroundingSearchConfiguration(bingConnectionId)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create the agent with Bing Grounding enabled&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> agent = &lt;span style="color:#ff7b72">await&lt;/span> agentClient.Administration.CreateAgentAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model: modelDeploymentName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name: &lt;span style="color:#a5d6ff">&amp;#34;ChristmasPriceHunter&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> instructions: &lt;span style="color:#a5d6ff">@&amp;#34;You are a Christmas gift price comparison specialist.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> When given gift ideas, use Bing to search for:
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Current prices at major retailers (Amazon, Best Buy, Target, Walmart)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Available discounts and holiday deals
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Shipping times to ensure delivery before Christmas
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Always provide URLs to the products you find.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Highlight the best deals and recommend where to buy.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tools: [bingGroundingTool]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> agent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="создание-агента-сравнения-цен-с-помощью-реального-поиска">Создание агента сравнения цен с помощью реального поиска&lt;/h3>
&lt;p>Теперь давайте создадим полноценный агент сравнения цен, который ищет в Интернете реальную информацию о продуктах:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.AI.Agents.Persistent&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ChristmasPriceAgent&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> PersistentAgentsClient _client;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _modelDeployment;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _bingConnectionId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ChristmasPriceAgent(&lt;span style="color:#ff7b72">string&lt;/span> projectEndpoint, &lt;span style="color:#ff7b72">string&lt;/span> modelDeployment, &lt;span style="color:#ff7b72">string&lt;/span> bingConnectionId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _client = &lt;span style="color:#ff7b72">new&lt;/span> PersistentAgentsClient(projectEndpoint, &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _modelDeployment = modelDeployment;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _bingConnectionId = bingConnectionId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; FindGiftPricesAsync(&lt;span style="color:#ff7b72">string&lt;/span> giftIdeas)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create agent with Bing Grounding&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bingTool = &lt;span style="color:#ff7b72">new&lt;/span> BingGroundingToolDefinition(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> BingGroundingSearchToolParameters(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [new BingGroundingSearchConfiguration(_bingConnectionId)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> agent = &lt;span style="color:#ff7b72">await&lt;/span> _client.Administration.CreateAgentAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model: _modelDeployment,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name: &lt;span style="color:#a5d6ff">&amp;#34;PriceHunter&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> instructions: &lt;span style="color:#a5d6ff">@&amp;#34;Search the web for current prices on the given gift ideas.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> For each gift, find prices from at least 2-3 different stores.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Include direct links to the products.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Note any Christmas sales or discounts available.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tools: [bingTool]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create a thread for the conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> thread = &lt;span style="color:#ff7b72">await&lt;/span> _client.Threads.CreateThreadAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Add the gift ideas as a message&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _client.Messages.CreateMessageAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> thread.Id,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MessageRole.User,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">$&amp;#34;Find current prices for these gift ideas: {giftIdeas}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Run the agent&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> run = &lt;span style="color:#ff7b72">await&lt;/span> _client.Runs.CreateRunAsync(thread.Id, agent.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Wait for completion&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.Delay(&lt;span style="color:#a5d6ff">500&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> run = &lt;span style="color:#ff7b72">await&lt;/span> _client.Runs.GetRunAsync(thread.Id, run.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">while&lt;/span> (run.Status == RunStatus.Queued || run.Status == RunStatus.InProgress);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Get the response&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> messages = _client.Messages.GetMessages(thread.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = messages
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(m =&amp;gt; m.Role == MessageRole.Agent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SelectMany(m =&amp;gt; m.ContentItems)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OfType&amp;lt;MessageTextContent&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .FirstOrDefault();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Clean up&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _client.Threads.DeleteThreadAsync(thread.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response?.Text ?? &lt;span style="color:#a5d6ff">&amp;#34;No results found.&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">finally&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Clean up the agent&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _client.Administration.DeleteAgentAsync(agent.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="интеграция-заземления-bing-в-рабочий-процесс">Интеграция заземления Bing в рабочий процесс&lt;/h3>
&lt;p>Вот как можно использовать ценового агента на базе Bing в полном рабочем процессе поиска подарков:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task RunWithBingGroundingAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> projectEndpoint = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;PROJECT_ENDPOINT&amp;#34;&lt;/span>)!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> modelDeployment = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;MODEL_DEPLOYMENT_NAME&amp;#34;&lt;/span>) ?? &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bingConnectionId = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;BING_CONNECTION_ID&amp;#34;&lt;/span>)!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Connection ID format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{account}/projects/{project}/connections/{connection}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;🔍 Searching the web for real prices with Bing Grounding...\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> priceAgent = &lt;span style="color:#ff7b72">new&lt;/span> ChristmasPriceAgent(projectEndpoint, modelDeployment, bingConnectionId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// First, generate gift ideas (could come from another agent)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> giftIdeas = &lt;span style="color:#a5d6ff">&amp;#34;1. Milwaukee cordless drill set, 2. Weber portable grill, 3. Carhartt beanie&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Search for real prices&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> priceResults = &lt;span style="color:#ff7b72">await&lt;/span> priceAgent.FindGiftPricesAsync(giftIdeas);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;📊 Price Comparison Results:&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(priceResults);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;\n🎁 Happy Shopping! 🎁&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="обработка-цитат-из-результатов-bing">Обработка цитат из результатов Bing&lt;/h3>
&lt;p>Одним из важных аспектов заземления с помощью поиска Bing является то, что ответы включают цитаты со ссылками на исходные веб-сайты. Вот как их извлечь и отобразить:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ProcessBingGroundingResponse(IEnumerable&amp;lt;PersistentThreadMessage&amp;gt; messages)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> messages.Where(m =&amp;gt; m.Role == MessageRole.Agent))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> content &lt;span style="color:#ff7b72">in&lt;/span> message.ContentItems)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (content &lt;span style="color:#ff7b72">is&lt;/span> MessageTextContent textContent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = textContent.Text;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Process URL citations&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (textContent.Annotations != &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> annotation &lt;span style="color:#ff7b72">in&lt;/span> textContent.Annotations)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (annotation &lt;span style="color:#ff7b72">is&lt;/span> MessageTextUriCitationAnnotation uriAnnotation)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Replace citation placeholder with markdown link&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response = response.Replace(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> uriAnnotation.Text,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">$&amp;#34; [{uriAnnotation.UriCitation.Title}]({uriAnnotation.UriCitation.Uri})&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(response);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Теперь, когда агент сравнения цен запускается, он использует &lt;strong>Обоснование с помощью поиска Bing&lt;/strong> для поиска реальных списков продуктов, текущих цен и доступных предложений в Интернете. Агент автоматически решает, когда выполнять поиск, на основе запроса и возвращает обоснованные ответы с соответствующими цитатами.&lt;/p>
&lt;h2 id="полная-программа">Полная программа&lt;/h2>
&lt;p>Вот как все устроено в &lt;code>Program.cs&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.AI.OpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Agents.AI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Agents.AI.Workflows&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Extensions.AI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;🎄 Christmas Gift Finder - Powered by AI Agents 🎄\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> endpoint = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_ENDPOINT&amp;#34;&lt;/span>)!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> deployment = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_DEPLOYMENT_NAME&amp;#34;&lt;/span>) ?? &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o-mini&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> chatClient = &lt;span style="color:#ff7b72">new&lt;/span> AzureOpenAIClient(&lt;span style="color:#ff7b72">new&lt;/span> Uri(endpoint), &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GetChatClient(deployment)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AsIChatClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Create the agent team&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> ideaAgent = ChristmasAgentFactory.CreateGiftIdeaAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> priceAgent = ChristmasAgentFactory.CreatePriceComparisonAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> summaryAgent = ChristmasAgentFactory.CreateSummaryAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Build the workflow&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> workflow = AgentWorkflowBuilder.BuildSequential(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;ChristmasGiftWorkflow&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ideaAgent, priceAgent, summaryAgent]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;Who are you shopping for? &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> recipientName = Console.ReadLine() ?? &lt;span style="color:#a5d6ff">&amp;#34;Someone special&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;What are their interests? &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> interests = Console.ReadLine() ?? &lt;span style="color:#a5d6ff">&amp;#34;general&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;What&amp;#39;s your budget? $&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> budget = Console.ReadLine() ?? &lt;span style="color:#a5d6ff">&amp;#34;50&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> prompt = &lt;span style="color:#a5d6ff">$&amp;#34;Find Christmas gifts for {recipientName} who enjoys {interests}. Budget: ${budget}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;\n🔍 Searching for the perfect gifts...\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> run = &lt;span style="color:#ff7b72">await&lt;/span> InProcessExecution.StreamAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> workflow,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ChatMessage(ChatRole.User, prompt));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> run.TrySendMessageAsync(&lt;span style="color:#ff7b72">new&lt;/span> TurnToken(emitEvents: &lt;span style="color:#79c0ff">true&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> evt &lt;span style="color:#ff7b72">in&lt;/span> run.WatchStreamAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (evt)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> AgentRunUpdateEvent update:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Write(update.Update.Text);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> WorkflowOutputEvent output:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;\n\n🎁 Happy Shopping! 🎁&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="заключение">Заключение&lt;/h2>
&lt;p>Microsoft Agent Framework позволяет на удивление легко создавать многоагентные системы, в которых каждый агент специализируется на определенной задаче. Координируя рабочие процессы этих агентов, мы можем структурированным и эффективным образом решать сложные проблемы, такие как рождественские покупки.&lt;/p>
&lt;p>Что делает это еще более мощным, так это возможность добавлять реальные возможности с помощью инструментов. Благодаря интеграции &lt;strong>Grounding с Bing Search&lt;/strong> от Microsoft Foundry наш агент по сравнению цен может фактически искать в Интернете текущие цены, предложения и наличие товаров, превращая простого чат-бота с искусственным интеллектом в действительно полезного помощника по покупкам с соответствующими цитатами.&lt;/p>
&lt;p>Поддержка структурой последовательных, параллельных шаблонов и шаблонов передачи обслуживания означает, что вы можете разрабатывать системы агентов, которые точно соответствуют вашим потребностям. Будь то поиск подарков, планирование поездок или любая другая многоэтапная задача, Agent Framework предоставляет все необходимые элементы для ее реализации.В этот праздничный сезон позвольте ИИ-агентам заняться исследованиями, а вы сосредоточитесь на упаковке подарков и приятном времяпрепровождении с семьей!&lt;/p>
&lt;h2 id="исходный-код">Исходный код&lt;/h2>
&lt;p>Концепции, показанные в этом посте, основаны на официальных образцах Agent Framework. Дополнительные примеры можно найти в &lt;a href="https://github.com/microsoft/agent-framework">репозитории Microsoft Agent Framework GitHub&lt;/a>.&lt;/p>
&lt;p>Счастливых праздников и удачного кодирования! 🎄&lt;/p></content:encoded><category>.NET</category><category>Azure</category><category>AI</category><category>Agent Framework</category></item><item><title>Создание агрегатора RSS-каналов на базе искусственного интеллекта</title><link>https://emimontesdeoca.github.io/ru/posts/ai-agent-socials/</link><pubDate>Fri, 12 Dec 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/ai-agent-socials/</guid><description>Автоматизируйте мониторинг RSS-каналов и создание публикаций в социальных сетях с помощью семантического ядра и Azure OpenAI.</description><content:encoded>&lt;p>Будучи MVP Microsoft и энтузиастом технологий, я постоянно тону в океане потрясающего контента, публикуемого в блогах разработчиков Microsoft. От анонсов .NET до обновлений Visual Studio, от инноваций Azure до глубокого изучения семантического ядра — в экосистеме Microsoft всегда происходит что-то новое и интересное.&lt;/p>
&lt;p>Проблема? &lt;strong>Успеть за всем этим практически невозможно.&lt;/strong>&lt;/p>
&lt;p>Я хотел быть в курсе последних объявлений и делиться ими со своей сетью, но проверка вручную семи различных RSS-каналов, чтение статей, создание интересных постов в социальных сетях и отслеживание того, чем я уже поделился, само по себе становилось работой на полный рабочий день. Каждое утро я открывал несколько вкладок браузера, просматривал десятки статей, пытался вспомнить, какими из них я уже поделился, а затем тратил драгоценное время на написание сообщений о тех, которые привлекли мое внимание.&lt;/p>
&lt;p>Поэтому я сделал то, что сделал бы любой разработчик: &lt;strong>я автоматизировал это.&lt;/strong>&lt;/p>
&lt;p>В этом подробном руководстве я расскажу вам, как я создал агрегатор RSS-каналов на базе искусственного интеллекта, который отслеживает несколько RSS-каналов Microsoft DevBlogs на наличие нового контента, использует Azure OpenAI и семантическое ядро для анализа статей и создания интересных публикаций, создает подробную документацию по уценке для каждой проанализированной статьи, отправляет уведомления через Telegram, чтобы я мог просматривать и делиться контентом, отслеживает все, чтобы избежать дублирования сообщений, и автоматически запускается через GitHub Actions.&lt;/p>
&lt;p>Давайте углубимся в каждый аспект этого решения.&lt;/p>
&lt;h2 id="история-этого-проекта">История этого проекта&lt;/h2>
&lt;h3 id="жизнь-с-информационной-перегрузкой">Жизнь с информационной перегрузкой&lt;/h3>
&lt;p>Позвольте мне нарисовать вам картину моего типичного утра до того, как я создал этот инструмент. Я просыпался, брал кофе и открывал ноутбук, чтобы проверить, что нового в экосистеме разработчиков Microsoft. Сначала я зашел на главный сайт DevBlogs, чтобы посмотреть, есть ли какие-нибудь важные объявления. Затем я специально проверял блог .NET, потому что это мой основной технологический стек. После этого я бы перешел к блогу Semantic Kernel, поскольку ИИ становится все более важным. Блог Visual Studio был следующим в списке, поскольку обновления IDE могут существенно повлиять на мой ежедневный рабочий процесс. Затем последовал блог DevOps с новостями, связанными с CI/CD и GitHub, за ним последовал блог All Things Azure, посвященный обновлениям облачной инфраструктуры, и, наконец, блог Azure SQL, посвященный инновациям в области баз данных.&lt;/p>
&lt;p>Это семь разных каналов, которые нужно проверить. Каждый из этих блогов публикует несколько статей в неделю, иногда несколько в день в периоды важных объявлений, таких как .NET Conf или Build. Это потенциально десятки статей, которые можно отслеживать, читать и делиться. И вот в чем дело: как человек, который ценит обмен знаниями с сообществом, я не хотел просто читать эти статьи. Я хотел поделиться наиболее ценными из них со своей сетью LinkedIn, помогая другим разработчикам тоже оставаться в курсе.Но создание хорошего поста в LinkedIn требует времени. Вам нужно внимательно прочитать статью, понять ключевые моменты, подумать, почему она важна для вашей аудитории, написать увлекательную зацепку и красиво все отформатировать. Умножьте это на несколько статей в неделю, и вы получите количество часов работы.&lt;/p>
&lt;h3 id="чего-я-действительно-хотел">Чего я действительно хотел&lt;/h3>
&lt;p>Проработав с этим несколько месяцев, я сел и задумался о том, как должно выглядеть идеальное решение. Прежде всего, я больше никогда не хотел пропускать важные объявления. Система должна автоматически обнаруживать новые статьи сразу после их публикации. Я также хотел сэкономить время на создании контента, позволив ИИ помогать создавать интересные посты — не для того, чтобы полностью заменить мой голос, а для того, чтобы дать мне надежную отправную точку, которую я мог бы настроить.&lt;/p>
&lt;p>Последовательность была еще одним важным фактором. Я хотел регулярно делиться контентом, не забывая делать это вручную каждый день. Аспект отслеживания также имел решающее значение: мне нужен был способ узнать, чем я уже поделился, чтобы не публиковать дубликаты и не раздражать своих подписчиков. Наконец, я хотел оставаться организованным и вести постоянную запись всего, что я обработал, чтобы иметь возможность оглянуться назад и посмотреть, какие темы я затронул.&lt;/p>
&lt;h3 id="решение-обретает-форму">Решение обретает форму&lt;/h3>
&lt;p>Решение, которое я предполагал, будет запускаться по расписанию с использованием GitHub Actions, полностью без помощи рук. Он будет автоматически получать все семь каналов, и мне не придется открывать ни одну вкладку браузера. Компонент ИИ фактически прочитает и поймет контент, а затем обобщит его так, чтобы это было полезно для моей аудитории. Вместо того, чтобы писать сообщения с нуля, он создавал готовый контент для социальных сетей, который я мог бы настроить при необходимости. Все будет отправлено в мой Telegram для проверки, чтобы я мог быстро взглянуть на свой телефон и решить, чем поделиться. И, конечно же, он будет постоянно записывать все для дальнейшего использования.&lt;/p>
&lt;h2 id="прежде-чем-мы-начнем-строить">Прежде чем мы начнем строить&lt;/h2>
&lt;h3 id="что-вам-понадобится-на-вашей-машине">Что вам понадобится на вашей машине&lt;/h3>
&lt;p>Чтобы следовать этому руководству, вам понадобится несколько вещей, установленных на вашей машине разработки. Наиболее важным из них является .NET SDK версии 9.0 или более поздней. Это наша среда выполнения, которая предоставляет все необходимые нам инструменты сборки. Если он у вас не установлен, зайдите на dot.net и загрузите последнюю версию. Установка проста в Windows, macOS или Linux.&lt;/p>
&lt;p>Вам также понадобится установить Git для контроля версий. Мы будем размещать наш код на GitHub и использовать GitHub Actions для автоматизации, поэтому крайне важно настроить Git локально. Любая последняя версия будет работать нормально.&lt;/p>
&lt;p>Для вашей среды разработки я рекомендую Visual Studio или VS Code. Лично я сейчас использую VS Code для большей части своей работы, потому что он легкий и имеет отличную поддержку C# через расширение C# Dev Kit. Но если вам удобнее работать с полной версией Visual Studio, она тоже отлично работает.&lt;/p>
&lt;h3 id="сервисы-и-учетные-записи-которые-вам-понадобятсяпомимо-локальных-инструментов-вам-понадобятся-учетные-записи-с-несколькими-сервисами-самым-важным-из-них-является-azure-openai-на-котором-основан-наш-анализ-ии-это-услуга-с-оплатой-по-мере-использования-но-затраты-для-этого-варианта-использования-минимальны--мы-говорим-о-центах-за-проанализированную-статью-если-у-вас-нет-учетной-записи-azure-вы-можете-подписаться-на-бесплатную-пробную-версию-которая-включает-в-себя-некоторые-кредиты-для-начала-работы">Сервисы и учетные записи, которые вам понадобятсяПомимо локальных инструментов, вам понадобятся учетные записи с несколькими сервисами. Самым важным из них является Azure OpenAI, на котором основан наш анализ ИИ. Это услуга с оплатой по мере использования, но затраты для этого варианта использования минимальны — мы говорим о центах за проанализированную статью. Если у вас нет учетной записи Azure, вы можете подписаться на бесплатную пробную версию, которая включает в себя некоторые кредиты для начала работы.&lt;/h3>
&lt;p>Для уведомлений мы будем использовать Telegram-бот. Самое замечательное в Telegram то, что API их ботов можно использовать совершенно бесплатно. Вы можете создать столько ботов, сколько захотите, и отправлять неограниченное количество сообщений. Я расскажу вам о процессе установки позже в этом руководстве.&lt;/p>
&lt;p>Наконец, вам понадобится учетная запись GitHub для размещения вашего кода и запуска действий GitHub. Уровень бесплатного пользования более чем достаточен для этого проекта. GitHub предоставляет вам 2000 минут выполнения Actions в месяц в частных репозиториях и неограниченное количество минут в публичных репозиториях.&lt;/p>
&lt;h3 id="библиотеки-которые-делают-это-возможным">Библиотеки, которые делают это возможным&lt;/h3>
&lt;p>Наш проект опирается на три основных пакета NuGet, каждый из которых служит определенной цели.&lt;/p>
&lt;p>Первый — HtmlAgilityPack, который является золотым стандартом анализа HTML в .NET. Когда мы извлекаем статью из блога, мы получаем полный HTML-код страницы, включая навигационные меню, нижние колонтитулы, рекламные объявления и всевозможные элементы, которые нас не интересуют. HtmlAgilityPack позволяет нам анализировать этот HTML и извлекать только необходимое нам содержимое статьи.&lt;/p>
&lt;p>Второй пакет — Microsoft.SemanticKernel, который представляет собой SDK Microsoft для интеграции моделей искусственного интеллекта в приложения. Думайте об этом как о мосте между вашим кодом .NET и большими языковыми моделями, такими как GPT-4. Он берет на себя всю сложность вызовов API, управления токенами и анализа ответов, позволяя вам сосредоточиться на том, что на самом деле вы хотите от ИИ.&lt;/p>
&lt;p>Третий пакет — System.ServiceModel.Syndicate, который обеспечивает встроенную поддержку анализа каналов RSS и Atom. RSS может показаться старой технологией, но это по-прежнему лучший способ получать структурированные обновления из блогов и новостных сайтов. Этот пакет превращает необработанные XML-каналы в строго типизированные объекты C#, с которыми легко работать.&lt;/p>
&lt;h2 id="понимание-архитектуры">Понимание архитектуры&lt;/h2>
&lt;h3 id="как-части-соединяются-друг-с-другом">Как части соединяются друг с другом&lt;/h3>
&lt;p>Прежде чем мы углубимся в код, позвольте мне объяснить, как все компоненты работают вместе. Понимание общей картины сделает детали реализации намного яснее.&lt;/p>
&lt;p>На самом высоком уровне у нас есть основной файл Program.cs, который действует как оркестратор. Это точка входа нашего приложения, и она координирует все остальные компоненты. Когда приложение запускается, оно сначала загружает конфигурацию из переменных среды — таких как ключи API и учетные данные Telegram. Затем он выходит и получает RSS-каналы из всех семи источников Microsoft DevBlogs. Обрабатывая эти каналы, он дедуплицирует статьи, чтобы обрабатывать случаи, когда одна и та же статья появляется в нескольких каналах. Он сверяет каждую статью с нашим файлом отслеживания, чтобы узнать, обрабатывали ли мы ее уже. Новые статьи передаются на обработку ИИ-анализатору.Класс ArticleAnalyzer — это место, где творится магия искусственного интеллекта. Этот компонент получает статью и выполняет с ней несколько действий. Сначала он извлекает полное HTML-содержимое из URL-адреса статьи. Затем он извлекает из этого HTML-кода чистый текст, удаляя все ненужные элементы навигации, скрипты и стили. Получив чистый текст, он отправляет его в Azure OpenAI через семантическое ядро ​​с тщательно составленным приглашением. ИИ анализирует статью и возвращает структурированный ответ, который включает в себя краткое изложение, ключевые темы, релевантное объяснение и, самое главное, готовую к использованию публикацию в LinkedIn. Анализатор анализирует этот ответ и возвращает объект ArticleAnaлиз, содержащий всю эту информацию.&lt;/p>
&lt;p>Класс MarkdownGenerator берет этот объект ArticleAnaлиз и создает его постоянную запись. Он генерирует красиво отформатированный файл уценки, который включает в себя все метаданные статьи, анализ ИИ и сгенерированный пост. Эти файлы хранятся в каталоге сгенерированных сообщений, что дает вам доступный для поиска архив всего, что вы обработали.&lt;/p>
&lt;p>Наконец, интеграция с Telegram отправляет сгенерированный контент сообщения на ваш телефон. Это момент, когда вы, как человек, можете просмотреть работу ИИ и решить, стоит ли ею делиться. Бот отправляет вам сообщение с содержимым публикации, и вы можете либо скопировать его непосредственно в LinkedIn, либо сначала изменить.&lt;/p>
&lt;h3 id="поток-данных">Поток данных&lt;/h3>
&lt;p>Позвольте мне рассказать вам, что происходит, когда в блоге .NET публикуется новая статья. Рабочий процесс начинается, когда GitHub Actions запускает наше приложение по расписанию — скажем, каждые шесть часов. Приложение просыпается и начинает получать все семь RSS-каналов. Каждый канал возвращает XML-документ, содержащий самые последние статьи из этого блога.&lt;/p>
&lt;p>Анализируя каждый канал, мы извлекаем отдельные статьи и сохраняем их в списке. Но есть одна сложность: основная лента DevBlogs часто включает в себя статьи, которые также появляются в лентах отдельных категорий. Таким образом, статья о «.NET 10» может появиться как в основной ленте, так и в ленте, посвященной .NET. Мы справляемся с этим, отслеживая URL-адреса в HashSet, который автоматически предотвращает дублирование.&lt;/p>
&lt;p>Получив дедуплицированный список статей, мы отфильтровываем его только до последних — обычно статей, опубликованных в последний день или около того. Мы не хотим обрабатывать старые статьи, которые уже обрабатывались в предыдущих запусках. Затем мы сверяем каждую недавнюю статью с нашим файлом отслеживания. Если мы уже обработали и опубликовали статью, мы ее пропускаем.&lt;/p>
&lt;p>Для каждой новой статьи мы запускаем конвейер анализа ИИ. Анализатор извлекает полный HTML-код статьи, очищает его и отправляет в GPT-4 с нашей подсказкой. ИИ читает статью и генерирует всесторонний анализ вместе с публикацией в LinkedIn. Мы сохраняем этот анализ в файл уценки для наших записей.После завершения анализа форматируем сообщение и отправляем его в Telegram. Сообщение включает в себя сгенерированный контент публикации с добавленными URL-адресом и хэштегами. На свой телефон я получаю уведомление, просматриваю публикацию и, если она мне нравится, могу скопировать ее и поделиться в LinkedIn всего несколькими нажатиями.&lt;/p>
&lt;p>Наконец, мы обновляем наш файл отслеживания, чтобы пометить эту статью как обработанную, чтобы больше не обрабатывать ее при будущих запусках. Если какие-либо файлы были созданы или изменены, GitHub Actions фиксирует эти изменения обратно в репозиторий, сохраняя все в синхронизации.&lt;/p>
&lt;h2 id="настройка-проекта-с-нуля">Настройка проекта с нуля&lt;/h2>
&lt;h3 id="создание-структуры-решения">Создание структуры решения&lt;/h3>
&lt;p>Начнем строить. Откройте терминал и перейдите туда, где вы хотите создать проект. Мне нравится хранить свои проекты в папке «Разработка», но вы можете разместить их там, где вам удобно.&lt;/p>
&lt;p>Сначала мы создадим новый файл решения. В .NET решение — это контейнер, в котором может храниться несколько проектов. Несмотря на то, что на данный момент у нас есть только один проект, начав с решения, при необходимости будет проще добавить больше проектов позже. Запустите команду &lt;code>dotnet new sln -n vs-feed-linkedin&lt;/code> для создания решения с именем vs-feed-linkedin.&lt;/p>
&lt;p>Далее нам нужно создать проект консольного приложения. Мы поместим это в подкаталог src, чтобы все было организовано. Запустите &lt;code>dotnet new console -n VsFeedLinkedin -o src&lt;/code> для создания консольного проекта с именем VsFeedLinkedin в папке src. Затем добавьте этот проект в наше решение с помощью &lt;code>dotnet sln add src/VsFeedLinkedin.csproj&lt;/code>.&lt;/p>
&lt;p>Теперь перейдите в каталог src с помощью &lt;code>cd src&lt;/code>. Здесь мы добавим пакеты NuGet и выполним большую часть разработки.&lt;/p>
&lt;h3 id="добавление-необходимых-пакетов">Добавление необходимых пакетов&lt;/h3>
&lt;p>После создания нашего проекта нам нужно добавить три пакета NuGet, о которых я упоминал ранее. Запустите каждую из этих команд последовательно:&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;p>После выполнения этих команд файл вашего проекта должен выглядеть примерно так:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;Project&lt;/span> Sdk=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.NET.Sdk&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PropertyGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;OutputType&amp;gt;&lt;/span>Exe&lt;span style="color:#7ee787">&amp;lt;/OutputType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;TargetFramework&amp;gt;&lt;/span>net9.0&lt;span style="color:#7ee787">&amp;lt;/TargetFramework&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;ImplicitUsings&amp;gt;&lt;/span>enable&lt;span style="color:#7ee787">&amp;lt;/ImplicitUsings&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;Nullable&amp;gt;&lt;/span>enable&lt;span style="color:#7ee787">&amp;lt;/Nullable&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/PropertyGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;ItemGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;HtmlAgilityPack&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;1.11.72&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.SemanticKernel&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;1.30.0&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;System.ServiceModel.Syndication&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;9.0.9&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/ItemGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/Project&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Файл проекта сообщает .NET, что мы создаем исполняемый файл (OutputType — Exe), ориентированный на .NET 9.0 и использующий современные функции C#, такие как неявное использование и ссылочные типы, допускающие значение NULL. В разделе ItemGroup перечислены три зависимости наших пакетов с указанием их точных версий.&lt;/p>
&lt;h2 id="глубокое-погружение-в-rss-каналы">Глубокое погружение в RSS-каналы&lt;/h2>
&lt;h3 id="что-такое-rss">Что такое RSS?&lt;/h3>
&lt;p>Прежде чем мы начнем писать код для получения каналов, давайте удостоверимся, что мы понимаем, с чем работаем. RSS означает Really Simple Syndicate и представляет собой стандартизированный формат XML для распространения обновлений контента. Идея проста: вместо того, чтобы требовать от пользователей посещения вашего веб-сайта для проверки наличия нового контента, вы публикуете машиночитаемый файл, в котором перечислены ваши последние материалы. Приложения могут затем периодически опрашивать этот файл для обнаружения новых статей.&lt;/p>
&lt;p>RSS существует с конца 1990-х — начала 2000-х годов. Вы можете подумать, что это устаревшая технология, но на самом деле она все еще широко используется – особенно в блогах, новостных сайтах и ​​подкастах. Прелесть RSS в его простоте. Это просто XML с определенной структурой, и любое приложение может его анализировать.&lt;/p>
&lt;h3 id="структура-ленты-devblogsкогда-вы-получаете-rss-канал-из-microsoft-devblogs-вы-получаете-обратно-xml-документ-имеющий-определенную-структуру-на-верхнем-уровне-есть-элемент-rss-содержащий-один-элемент-канала-канал-представляет-сам-блог-и-включает-метаданные-такие-как-заголовок-url-адрес-и-описание-блога">Структура ленты DevBlogsКогда вы получаете RSS-канал из Microsoft DevBlogs, вы получаете обратно XML-документ, имеющий определенную структуру. На верхнем уровне есть элемент rss, содержащий один элемент канала. Канал представляет сам блог и включает метаданные, такие как заголовок, URL-адрес и описание блога.&lt;/h3>
&lt;p>Внутри канала вы найдете несколько элементов item, каждый из которых представляет отдельную публикацию в блоге. Каждый элемент включает заголовок (заголовок статьи), ссылку (URL-адрес, по которому можно прочитать полную статью), дату публикации (когда статья была опубликована), элемент dc:creator (имя автора), один или несколько элементов категории (теги для статьи) и описание (обычно краткое изложение или отрывок статьи).&lt;/p>
&lt;p>Вот упрощенный пример того, как это выглядит:&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;p>Самое замечательное в пакете System.ServiceModel.Syndicate .NET заключается в том, что он анализирует все это за нас. Нам не нужно вручную перемещаться по узлам XML или беспокоиться о разных версиях RSS. Мы просто загружаем фид и получаем обратно строго типизированные объекты.&lt;/p>
&lt;h3 id="семь-каналов-которые-мы-отслеживаем">Семь каналов, которые мы отслеживаем&lt;/h3>
&lt;p>В своей реализации я отслеживаю семь различных каналов Microsoft DevBlogs. Основной канал DevBlogs по адресу devblogs.microsoft.com/feed дает нам общее представление обо всем, что Microsoft публикует во всех своих блогах разработчиков. Лента, посвященная .NET, по адресу devblogs.microsoft.com/dotnet/feed посвящена выпускам, функциям и передовым практикам .NET. Лента Semantic Kernel на сайте devblogs.microsoft.com/semantic-kernel/feed посвящена оркестровке и интеграции ИИ, что становится все более важным, поскольку ИИ становится центральным элементом современного развития.&lt;/p>
&lt;p>Лента Visual Studio по адресу devblogs.microsoft.com/visualstudio/feed позволяет мне быть в курсе улучшений IDE и функций повышения производительности. Канал DevOps по адресу devblogs.microsoft.com/devops/feed охватывает темы Azure DevOps, GitHub и CI/CD. Канал All Things Azure по адресу devblogs.microsoft.com/all-things-azure/feed посвящен облачным сервисам и шаблонам архитектуры. Наконец, канал Azure SQL по адресу devblogs.microsoft.com/azure-sql/feed посвящен инновациям и функциям баз данных.&lt;/p>
&lt;p>Вы можете задаться вопросом, почему я проверяю как основной канал, так и каналы отдельных категорий. Основной канал дает мне простор: я вижу статьи из любого блога разработчиков Microsoft, включая те, о которых я, возможно, не знаю. Ленты категорий дают мне глубину — они гарантируют, что я не пропущу ничего важного в моих основных областях интересов, даже если эти статьи будут вытеснены из основного канала новым контентом.&lt;/p>
&lt;h2 id="построение-логики-получения-rss">Построение логики получения RSS&lt;/h2>
&lt;h3 id="основная-функция-выборки">Основная функция выборки&lt;/h3>
&lt;p>Теперь давайте напишем немного кода. Основой нашего приложения является возможность получения и анализа RSS-каналов. Вот функция, которая это обрабатывает:&lt;/p>
&lt;p>[[[ТОК_7]]]&lt;/p>
&lt;p>Позвольте мне рассказать, что делает этот код. Начнем с создания HttpClient — встроенного класса .NET для выполнения HTTP-запросов. Мы устанавливаем заголовок User-Agent, потому что некоторые серверы блокируют запросы, которые не идентифицируют себя. Рекомендуется устанавливать это значение, даже если серверы этого не требуют.Затем мы делаем запрос GET к URL-адресу канала и получаем ответ в виде строки. Эта строка содержит необработанный XML-файл RSS-канала.&lt;/p>
&lt;p>Чтобы проанализировать этот XML, мы создаем StringReader для переноса нашей строки ответа, а затем настраиваем некоторые XmlReaderSettings. Параметр DtdProcessing важен: RSS-каналы иногда включают объявления DTD (определение типа документа), которые необходимо обработать. Параметр MaxCharactersFromEntities — это мера безопасности, которая предотвращает атаки XML-бомбы, ограничивая степень расширения сущности.&lt;/p>
&lt;p>Наконец, мы создаем XmlReader с этими настройками и используем SyndictionFeed.Load для анализа XML в строго типизированный объект SyndictionFeed. Это дает нам доступ к метаданным канала и всем его элементам через удобные свойства C# вместо простой навигации по XML.&lt;/p>
&lt;h3 id="получение-нескольких-каналов-с-обработкой-ошибок">Получение нескольких каналов с обработкой ошибок&lt;/h3>
&lt;p>В реальном мире сетевые запросы не выполняются. Серверы выходят из строя, время соединения истекает, а XML может быть искажен. Нам нужно обращаться с такими случаями изящно. Вот как мы получаем все наши каналы, сохраняя при этом устойчивость к сбоям:&lt;/p>
&lt;p>[[[ТОК_8]]]&lt;/p>
&lt;p>Мы храним здесь две коллекции. Список allArticles будет содержать все найденные нами статьи, а также из какого канала они взяты. Набор хэшей «seenUrls» отслеживает URL-адреса статей, которые мы уже видели, что помогает нам избежать дублирования.&lt;/p>
&lt;p>Мы перебираем каждый URL-адрес канала и заключаем операцию выборки в блок try-catch. Если получение определенного канала не удается (возможно, сервер временно не работает), мы регистрируем предупреждение и продолжаем работу со следующим каналом. Таким образом, проблема с одним фидом не мешает нам обрабатывать остальные.&lt;/p>
&lt;p>Для каждого успешно полученного канала мы перебираем его элементы. Мы извлекаем URL-адрес статьи из коллекции ссылок элемента. Метод HashSet.Add возвращает false, если URL-адрес уже находится в наборе, что идеально подходит для нашей логики дедупликации. Мы добавляем статью в наш список только в том случае, если она новая.&lt;/p>
&lt;p>Мы сохраняем URL-адрес канала рядом с каждой статьей, поскольку эта информация может пригодиться позже — например, нам может потребоваться узнать, из какого конкретного канала пришла статья, в целях отладки или регистрации.&lt;/p>
&lt;h2 id="обработка-дубликатов-и-отслеживание-состояния">Обработка дубликатов и отслеживание состояния&lt;/h2>
&lt;h3 id="проблема-дедупликации">Проблема дедупликации&lt;/h3>
&lt;p>Как я упоминал ранее, Microsoft DevBlogs имеет иерархическую структуру каналов, что создает интересную проблему. Когда член команды .NET публикует статью, скажем, об улучшении производительности в .NET 10, эта статья, скорее всего, появится как в основной ленте DevBlogs, так и в ленте, посвященной .NET. Иногда оно может даже появиться в ленте Visual Studio, если оно относится к функциям IDE.&lt;/p>
&lt;p>Если бы мы наивно обрабатывали каждую статью из каждого канала, нам пришлось бы анализировать и публиковать одну и ту же статью несколько раз. Это приведет к пустой трате вызовов API к Azure OpenAI, спаму в Telegram дубликатами уведомлений и потенциально раздражению наших подписчиков, если мы опубликуем дубликаты.Решением является дедупликация на основе URL-адресов. Каждая статья имеет уникальный URL-адрес, поэтому мы можем использовать его в качестве идентификатора. Структура данных HashSet идеально подходит для этого, поскольку обеспечивает время поиска O(1) и автоматически предотвращает дублирование. Когда мы пытаемся добавить URL-адрес, который уже есть в наборе, метод Add просто возвращает false, давая нам понять, что эту статью следует пропустить.&lt;/p>
&lt;h3 id="постоянное-состояние-с-markdown">Постоянное состояние с Markdown&lt;/h3>
&lt;p>Дедупликация обрабатывает дубликаты за один прогон, а как насчет нескольких прогонов? Когда наше приложение запускается каждые шесть часов, нам необходимо запомнить, какие статьи мы уже обработали, чтобы больше не обрабатывать их.&lt;/p>
&lt;p>Я решил сохранить это состояние в файле уценки под названием Posted-articles.md. Почему уценка? Несколько причин. Во-первых, он удобен для чтения человеком. Я могу открыть файл и сразу увидеть, какими статьями я поделился. Во-вторых, он контролируется версиями. Поскольку этот файл находится в нашем репозитории Git, у меня есть полная история обработки статей. В-третьих, он служит документацией. Любой, кто заглянет в репозиторий, сможет увидеть, что сделало приложение.&lt;/p>
&lt;p>Формат этого файла простой. Он имеет заголовок, временную метку, показывающую дату последнего запуска приложения, а затем список статей в формате ссылки уценки:&lt;/p>
&lt;p>[[[ТОК_9]]]&lt;/p>
&lt;h3 id="загрузка-и-анализ-файла-отслеживания">Загрузка и анализ файла отслеживания&lt;/h3>
&lt;p>Чтобы проверить, обрабатывали ли мы уже статью, нам нужно загрузить этот файл и извлечь URL-адреса. Вот функция, которая это делает:&lt;/p>
&lt;p>[[[ТОК_10]]]&lt;/p>
&lt;p>Эта функция возвращает HashSet, содержащий все URL-адреса, которые мы уже обработали. Начнем с проверки существования файла — при первом запуске его не будет, поэтому мы возвращаем пустой набор.&lt;/p>
&lt;p>Для каждой строки файла мы используем регулярное выражение для извлечения URL-адреса из формата ссылки уценки. Регулярное выражение &lt;code>\(([^)]+)\)&lt;/code> соответствует всему, что находится внутри круглых скобок, где ссылки уценки хранят свои URL-адреса.&lt;/p>
&lt;p>Затем следует важный шаг: нормализация URL-адресов. URL-адреса одной и той же статьи могут различаться по формату. RSS-канал может дать нам &lt;code>https://devblogs.microsoft.com/dotnet/article&lt;/code>, но к нашей сохраненной версии добавлен параметр отслеживания: &lt;code>https://devblogs.microsoft.com/dotnet/article?wt.mc_id=DT-MVP-5004972&lt;/code>. Некоторые URL-адреса имеют косую черту в конце, другие — нет.&lt;/p>
&lt;p>Чтобы справиться с этим, мы удаляем все параметры запроса (все после &lt;code>?&lt;/code>) и удаляем конечные косые черты. Эта нормализация гарантирует, что мы распознаем статьи как дубликаты, даже если их URL-адреса различаются такими поверхностными способами.&lt;/p>
&lt;h3 id="сохранение-новых-статей">Сохранение новых статей&lt;/h3>
&lt;p>Когда мы успешно обработаем статью, нам нужно добавить ее в наш файл отслеживания:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> SavePostedArticle(&lt;span style="color:#ff7b72">string&lt;/span> filePath, &lt;span style="color:#ff7b72">string&lt;/span> url, &lt;span style="color:#ff7b72">string&lt;/span> title, DateTimeOffset publishDate)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> markdownEntry = &lt;span style="color:#a5d6ff">$&amp;#34;- [{title}]({url}) - Posted on {DateTime.Now:yyyy-MM-dd HH:mm:ss} (Published: {publishDate:yyyy-MM-dd})\n&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!File.Exists(filePath))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> File.WriteAllText(filePath, &lt;span style="color:#a5d6ff">&amp;#34;# Posted Articles\n\n*Last run: {DateTime.Now:yyyy-MM-dd HH:mm:ss}*\n\nList of articles posted:\n\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> File.AppendAllText(filePath, markdownEntry);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Эта функция создает запись в формате уценки с заголовком статьи в виде ссылки, за которой следуют временные метки, показывающие, когда мы ее опубликовали и когда она была первоначально опубликована. Если файл еще не существует, мы сначала создаем его с заголовком.&lt;/p>
&lt;h2 id="механизм-анализа-ии">Механизм анализа ИИ&lt;/h2>
&lt;h3 id="понимание-семантического-ядратеперь-мы-переходим-к-самой-интересной-части-нашего-приложения--анализу-ии-семантическое-ядро--это-пакет-sdk-microsoft-с-открытым-исходным-кодом-для-интеграции-больших-языковых-моделей-в-приложения-это-больше-чем-просто-оболочка-вызовов-api-он-обеспечивает-основу-для-создания-сложных-приложений-искусственного-интеллекта-с-такими-функциями-как-плагины-планировщики-и-память">Понимание семантического ядраТеперь мы переходим к самой интересной части нашего приложения — анализу ИИ. Семантическое ядро ​​— это пакет SDK Microsoft с открытым исходным кодом для интеграции больших языковых моделей в приложения. Это больше, чем просто оболочка вызовов API. Он обеспечивает основу для создания сложных приложений искусственного интеллекта с такими функциями, как плагины, планировщики и память.&lt;/h3>
&lt;p>В нашем случае мы используем возможности завершения чата семантического ядра. Мы отправим запрос в Azure OpenAI, и модель проанализирует нашу статью и сгенерирует ответ. Семантическое ядро ​​берет на себя всю сложность аутентификации API, форматирования запросов и анализа ответов.&lt;/p>
&lt;h3 id="настройка-анализатора-статей">Настройка анализатора статей&lt;/h3>
&lt;p>Давайте посмотрим, как мы настроили наш класс анализатора:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">HtmlAgilityPack&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">VsFeedLinkedin.Services&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ArticleAnalyzer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> Kernel _kernel;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IChatCompletionService _chatService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ArticleAnalyzer(&lt;span style="color:#ff7b72">string&lt;/span> endpoint, &lt;span style="color:#ff7b72">string&lt;/span> apiKey, &lt;span style="color:#ff7b72">string&lt;/span> deploymentName)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: deploymentName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: endpoint,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: apiKey
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _chatService = _kernel.GetRequiredService&amp;lt;IChatCompletionService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Семантическое ядро использует шаблон компоновщика для настройки. Мы создаем KernelBuilder, добавляем нашу службу завершения чата Azure OpenAI с необходимыми учетными данными, а затем собираем ядро. Из построенного ядра мы получаем интерфейс IChatCompletionService, который будем использовать для отправки подсказок и получения ответов.&lt;/p>
&lt;p>Конструктор принимает три параметра: конечную точку Azure OpenAI (что-то вроде &lt;code>https://your-resource.openai.azure.com/&lt;/code>), ваш ключ API и имя развертывания (например, &lt;code>gpt-4o&lt;/code>). Они передаются из переменных среды, обеспечивая безопасность наших учетных данных.&lt;/p>
&lt;h3 id="создание-идеальной-подсказки">Создание идеальной подсказки&lt;/h3>
&lt;p>Подсказка, которую мы отправляем ИИ, имеет решающее значение. Хорошо продуманная подсказка обеспечивает стабильные и высококачественные результаты. Расплывчатая или плохо структурированная подсказка приводит к непоследовательным и посредственным результатам. Я потратил много времени на повторение этого запроса, чтобы получить результаты, которые меня устраивают:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> prompt = &lt;span style="color:#a5d6ff">$&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> You are a professional tech content analyst and LinkedIn content creator.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Analyze the following Microsoft DevBlogs article and create an engaging LinkedIn post.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Article Title: {title}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Author: {author}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> URL: {url}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Tags: {string.Join(&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;, tags)}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Article Content:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {cleanContent}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Please provide:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">1.&lt;/span> A brief summary (&lt;span style="color:#a5d6ff">2&lt;/span>-&lt;span style="color:#a5d6ff">3&lt;/span> sentences) of the key points
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">2.&lt;/span> The main technologies or topics covered
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">3.&lt;/span> Why &lt;span style="color:#ff7b72">this&lt;/span> &lt;span style="color:#ff7b72">is&lt;/span> relevant &lt;span style="color:#ff7b72">for&lt;/span> developers/tech professionals
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">4.&lt;/span> An engaging LinkedIn post (max &lt;span style="color:#a5d6ff">1300&lt;/span> characters) that:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Starts with a hook or attention-grabbing statement
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Highlights the key &lt;span style="color:#ff7b72">value&lt;/span> &lt;span style="color:#ff7b72">for&lt;/span> readers
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Includes a call to action
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Uses appropriate emojis (but not too many)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Maintains a professional yet approachable tone
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - DO NOT include hashtags &lt;span style="color:#ff7b72">in&lt;/span> the post (they will be added separately)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - DO NOT include the URL &lt;span style="color:#ff7b72">in&lt;/span> the post (it will be added separately)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Format your response &lt;span style="color:#ff7b72">as&lt;/span> follows:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">##&lt;/span> Summary
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Your summary here]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">##&lt;/span> Key Topics
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [List of main topics/technologies]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">##&lt;/span> Relevance
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Why this matters]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">##&lt;/span> LinkedIn Post
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Your engaging LinkedIn post here]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;;
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Позвольте мне объяснить здесь дизайнерские решения. Начнем с того, что дадим ИИ четкую роль: «Вы — профессиональный аналитик технического контента и создатель контента LinkedIn». Это побуждает модель реагировать соответствующим стилем и голосом.&lt;/p>
&lt;p>Мы предоставляем весь контекст, необходимый ИИ: название статьи, автора, URL-адрес, теги из RSS-канала и полное содержание статьи. Чем больше контекста мы дадим, тем лучше будет анализ.&lt;/p>
&lt;p>Затем мы точно указываем, что мы хотим получить обратно. Я прошу четыре вещи: краткое изложение, ключевые темы, релевантное объяснение и публикацию в LinkedIn. В частности, для публикации в LinkedIn я даю подробные инструкции о том, что делает публикацию хорошей: она должна иметь зацепку, подчеркивать ценность, включать призыв к действию, правильно использовать смайлы и поддерживать профессиональный тон.&lt;/p>
&lt;p>Негативные инструкции не менее важны. Я прямо говорю ИИ НЕ включать хэштеги или URL-адрес в сообщение. Почему? Потому что я добавляю их отдельно, и если бы ИИ включил их, у меня были бы дубликаты. Подобные явные инструкции предотвращают распространенные ошибки.&lt;/p>
&lt;p>Наконец, я указываю точный формат вывода. Запрашивая разделы, отмеченные заголовками ##, я упрощаю программный анализ ответа. ИИ очень хорошо выполняет инструкции по форматированию, и такая согласованность делает наш код синтаксического анализа более простым и надежным.&lt;/p>
&lt;h3 id="выполнение-анализа">Выполнение анализа&lt;/h3>
&lt;p>Вот как мы все это собрали:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ArticleAnalysis&amp;gt; AnalyzeArticleAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> title,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> url,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> htmlContent,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> author,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; tags)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> cleanContent = ExtractTextFromHtml(htmlContent);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (cleanContent.Length &amp;gt; &lt;span style="color:#a5d6ff">8000&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cleanContent = cleanContent.Substring(&lt;span style="color:#a5d6ff">0&lt;/span>, &lt;span style="color:#a5d6ff">8000&lt;/span>) + &lt;span style="color:#a5d6ff">&amp;#34;...&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> chatHistory = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chatHistory.AddUserMessage(prompt);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _chatService.GetChatMessageContentAsync(chatHistory);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> responseText = response.Content ?? &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> ParseAnalysisResponse(responseText, title, url, author, tags);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```Сначала&lt;/span> &lt;span style="color:#f85149">мы&lt;/span> &lt;span style="color:#f85149">извлекаем&lt;/span> &lt;span style="color:#f85149">чистый&lt;/span> &lt;span style="color:#f85149">текст&lt;/span> &lt;span style="color:#f85149">из&lt;/span> &lt;span style="color:#f85149">содержимого&lt;/span> HTML (&lt;span style="color:#f85149">я&lt;/span> &lt;span style="color:#f85149">объясню&lt;/span> &lt;span style="color:#f85149">это&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">следующем&lt;/span> &lt;span style="color:#f85149">разделе&lt;/span>). &lt;span style="color:#f85149">Затем&lt;/span> &lt;span style="color:#f85149">мы&lt;/span> &lt;span style="color:#f85149">обрезаем&lt;/span> &lt;span style="color:#f85149">контент&lt;/span>, &lt;span style="color:#f85149">если&lt;/span> &lt;span style="color:#f85149">он&lt;/span> &lt;span style="color:#f85149">слишком&lt;/span> &lt;span style="color:#f85149">длинный&lt;/span>. &lt;span style="color:#f85149">Большие&lt;/span> &lt;span style="color:#f85149">языковые&lt;/span> &lt;span style="color:#f85149">модели&lt;/span> &lt;span style="color:#f85149">имеют&lt;/span> &lt;span style="color:#f85149">ограничения&lt;/span> &lt;span style="color:#f85149">по&lt;/span> &lt;span style="color:#f85149">токенам&lt;/span>, &lt;span style="color:#f85149">а&lt;/span> &lt;span style="color:#f85149">очень&lt;/span> &lt;span style="color:#f85149">длинные&lt;/span> &lt;span style="color:#f85149">статьи&lt;/span> &lt;span style="color:#f85149">могут&lt;/span> &lt;span style="color:#f85149">их&lt;/span> &lt;span style="color:#f85149">превышать&lt;/span>. &lt;span style="color:#f85149">Ограничивая&lt;/span> &lt;span style="color:#f85149">текст&lt;/span> &lt;span style="color:#f85149">до&lt;/span> &lt;span style="color:#a5d6ff">8000&lt;/span> &lt;span style="color:#f85149">символов&lt;/span>, &lt;span style="color:#f85149">мы&lt;/span> &lt;span style="color:#f85149">гарантируем&lt;/span>, &lt;span style="color:#f85149">что&lt;/span> &lt;span style="color:#f85149">не&lt;/span> &lt;span style="color:#f85149">выйдем&lt;/span> &lt;span style="color:#f85149">за&lt;/span> &lt;span style="color:#f85149">рамки&lt;/span> &lt;span style="color:#f85149">ограничений&lt;/span>, &lt;span style="color:#f85149">сохраняя&lt;/span> &lt;span style="color:#f85149">при&lt;/span> &lt;span style="color:#f85149">этом&lt;/span> &lt;span style="color:#f85149">существенный&lt;/span> &lt;span style="color:#f85149">контекст&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Мы&lt;/span> &lt;span style="color:#f85149">создаем&lt;/span> &lt;span style="color:#f85149">объект&lt;/span> ChatHistory &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">добавляем&lt;/span> &lt;span style="color:#f85149">наше&lt;/span> &lt;span style="color:#f85149">приглашение&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">качестве&lt;/span> &lt;span style="color:#f85149">сообщения&lt;/span> &lt;span style="color:#f85149">пользователя&lt;/span>. &lt;span style="color:#f85149">Это&lt;/span> &lt;span style="color:#f85149">абстракция&lt;/span> &lt;span style="color:#f85149">семантического&lt;/span> &lt;span style="color:#f85149">ядра&lt;/span> &lt;span style="color:#f85149">для&lt;/span> &lt;span style="color:#f85149">взаимодействия&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">чате&lt;/span>. &lt;span style="color:#f85149">Мы&lt;/span> &lt;span style="color:#f85149">отправляем&lt;/span> &lt;span style="color:#f85149">это&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">службу&lt;/span> &lt;span style="color:#f85149">завершения&lt;/span> &lt;span style="color:#f85149">чата&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">получаем&lt;/span> &lt;span style="color:#f85149">ответ&lt;/span>. &lt;span style="color:#f85149">Наконец&lt;/span>, &lt;span style="color:#f85149">мы&lt;/span> &lt;span style="color:#f85149">анализируем&lt;/span> &lt;span style="color:#f85149">ответ&lt;/span>, &lt;span style="color:#f85149">чтобы&lt;/span> &lt;span style="color:#f85149">извлечь&lt;/span> &lt;span style="color:#f85149">отдельные&lt;/span> &lt;span style="color:#f85149">разделы&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">###&lt;/span> &lt;span style="color:#f85149">Анализ&lt;/span> &lt;span style="color:#f85149">ответа&lt;/span> &lt;span style="color:#f85149">ИИ&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">ИИ&lt;/span> &lt;span style="color:#f85149">возвращает&lt;/span> &lt;span style="color:#f85149">свой&lt;/span> &lt;span style="color:#f85149">ответ&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">виде&lt;/span> &lt;span style="color:#f85149">текста&lt;/span>, &lt;span style="color:#f85149">отформатированного&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">соответствии&lt;/span> &lt;span style="color:#f85149">с&lt;/span> &lt;span style="color:#f85149">запрошенной&lt;/span> &lt;span style="color:#f85149">нами&lt;/span> &lt;span style="color:#f85149">структурой&lt;/span>. &lt;span style="color:#f85149">Нам&lt;/span> &lt;span style="color:#f85149">нужно&lt;/span> &lt;span style="color:#f85149">разобрать&lt;/span> &lt;span style="color:#f85149">это&lt;/span> &lt;span style="color:#f85149">на&lt;/span> &lt;span style="color:#f85149">отдельные&lt;/span> &lt;span style="color:#f85149">поля&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> ArticleAnalysis ParseAnalysisResponse(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> response,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> title,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> url,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> author,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; tags)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> analysis = &lt;span style="color:#ff7b72">new&lt;/span> ArticleAnalysis
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Title = title,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Url = url,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Author = author,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Tags = tags,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RawAnalysis = response
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sections = response.Split(&lt;span style="color:#a5d6ff">&amp;#34;##&amp;#34;&lt;/span>, StringSplitOptions.RemoveEmptyEntries);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> section &lt;span style="color:#ff7b72">in&lt;/span> sections)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> lines = section.Trim().Split(&lt;span style="color:#a5d6ff">&amp;#39;\n&amp;#39;&lt;/span>, &lt;span style="color:#a5d6ff">2&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (lines.Length &amp;lt; &lt;span style="color:#a5d6ff">2&lt;/span>) &lt;span style="color:#ff7b72">continue&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sectionTitle = lines[&lt;span style="color:#a5d6ff">0&lt;/span>].Trim().ToLower();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sectionContent = lines[&lt;span style="color:#a5d6ff">1&lt;/span>].Trim();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (sectionTitle)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;summary&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analysis.Summary = sectionContent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;key topics&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analysis.KeyTopics = sectionContent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;relevance&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analysis.Relevance = sectionContent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;linkedin post&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analysis.LinkedInPost = sectionContent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> analysis;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Мы разделяем ответ по маркерам &lt;code>##&lt;/code>, что дает нам каждый раздел. Для каждого раздела мы разделяем новую строку, чтобы отделить заголовок от содержимого. Затем мы используем оператор переключения, чтобы назначить содержимое каждого раздела соответствующему свойству.&lt;/p>
&lt;p>Мы также сохраняем необработанный, неанализированный ответ. Это полезно для отладки — если при парсинге что-то пойдет не так, мы сможем посмотреть, что на самом деле вернул ИИ.&lt;/p>
&lt;h2 id="извлечение-контента-из-html">Извлечение контента из HTML&lt;/h2>
&lt;h3 id="почему-нам-нужно-чистить-html">Почему нам нужно чистить HTML&lt;/h3>
&lt;p>Когда мы получаем статью из блога, мы получаем полный HTML-код страницы. Это включает в себя гораздо больше, чем просто содержимое статьи: здесь есть навигационные меню, верхние и нижние колонтитулы, боковые панели, виджеты связанных статей, разделы комментариев, сценарии для аналитики и отслеживания, таблицы стилей и всевозможные другие элементы.&lt;/p>
&lt;p>Если бы мы отправили все это нашему ИИ, произошло бы несколько плохих вещей. ИИ пришлось бы обрабатывать много ненужного текста, тратя токены и потенциально запутывая анализ. Текст навигации и нижнего колонтитула может быть включен в сводку. Скрипты и CSS будут рассматриваться как контент, что еще больше затруднит анализ.&lt;/p>
&lt;p>Нам нужно извлечь только содержание статьи — ту часть, которую на самом деле прочитает читатель.&lt;/p>
&lt;h3 id="использование-htmlagilitypack">Использование HtmlAgilityPack&lt;/h3>
&lt;p>HtmlAgilityPack — это надежная библиотека анализа HTML для .NET. В отличие от XML, HTML часто имеет неправильный формат: теги могут быть неправильно закрыты, атрибуты могут быть заключены в кавычки неправильно. HtmlAgilityPack прекрасно справляется со всем этим, предоставляя нам структуру, подобную DOM, которую мы можем запрашивать и манипулировать ею.&lt;/p>
&lt;p>Вот наша функция извлечения:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> ExtractTextFromHtml(&lt;span style="color:#ff7b72">string&lt;/span> html)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(html))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> doc = &lt;span style="color:#ff7b72">new&lt;/span> HtmlDocument();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> doc.LoadHtml(html);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> nodesToRemove = doc.DocumentNode.SelectNodes(&lt;span style="color:#a5d6ff">&amp;#34;//script|//style|//nav|//footer|//header&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (nodesToRemove != &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> node &lt;span style="color:#ff7b72">in&lt;/span> nodesToRemove)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> node.Remove();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> text = doc.DocumentNode.InnerText;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> text = System.Text.RegularExpressions.Regex.Replace(text, &lt;span style="color:#a5d6ff">@&amp;#34;\s+&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34; &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> text.Trim();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Мы загружаем HTML в HtmlDocument, который анализирует его в древовидную структуру. Затем мы используем XPath, чтобы выбрать все узлы, которые хотим удалить. Выражение XPath &lt;code>//script|//style|//nav|//footer|//header&lt;/code> выбирает все элементы скрипта (код JavaScript нам не нужен), элементы стиля (CSS нам не нужен), элементы навигации (меню навигации), элементы нижнего колонтитула и элементы заголовка.&lt;/p>
&lt;p>После удаления этих узлов мы получаем свойство InnerText, которое извлекает весь текстовый контент, удаляя при этом HTML-теги. Это дает нам простой текст статьи.Наконец, мы очищаем пробелы. HTML часто имеет много дополнительных пробелов для форматирования — несколько пробелов, табуляции, новой строки. Мы используем регулярное выражение для замены любой последовательности пробельных символов одним пробелом, а затем обрезаем результат.&lt;/p>
&lt;h3 id="получение-полной-статьи">Получение полной статьи&lt;/h3>
&lt;p>RSS-канал предоставляет нам только краткое изложение, а не полное содержание статьи. Чтобы получить полный текст, нам нужно получить веб-страницу статьи:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; FetchArticleContentAsync(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> httpClient.DefaultRequestHeaders.Add(&lt;span style="color:#a5d6ff">&amp;#34;User-Agent&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;VsFeedLinkedin/1.0&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">await&lt;/span> httpClient.GetStringAsync(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;⚠️ Failed to fetch article content: {ex.Message}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это просто: мы делаем HTTP-запрос GET к URL-адресу статьи и возвращаем ответ в формате HTML. Мы обертываем его в try-catch, потому что сетевые запросы могут завершиться неудачно, и мы предпочитаем вернуть пустую строку, чем привести к сбою всего приложения.&lt;/p>
&lt;h2 id="создание-постоянной-документации">Создание постоянной документации&lt;/h2>
&lt;h3 id="зачем-создавать-файлы-markdown">Зачем создавать файлы Markdown&lt;/h3>
&lt;p>Каждый раз, когда мы анализируем статью, мы создаем подробный файл уценки, документирующий этот анализ. Это служит нескольким целям.&lt;/p>
&lt;p>Во-первых, он создает архив с возможностью поиска. Со временем вы создадите коллекцию проанализированных статей. Вы можете выполнить поиск по этим файлам, чтобы найти прошлый контент по определенным темам.&lt;/p>
&lt;p>Во-вторых, это обеспечивает прозрачность. Вы можете увидеть, что именно сгенерировал ИИ для каждой статьи, включая полный анализ и публикацию в LinkedIn.&lt;/p>
&lt;p>В-третьих, это полезно для отладки. Если с публикацией что-то пойдет не так, вы можете посмотреть файл уценки, чтобы понять, что произошло.&lt;/p>
&lt;h3 id="класс-генератора-markdown">Класс генератора Markdown&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">MarkdownGenerator&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _outputDirectory;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> MarkdownGenerator(&lt;span style="color:#ff7b72">string&lt;/span> outputDirectory)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _outputDirectory = outputDirectory;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!Directory.Exists(_outputDirectory))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Directory.CreateDirectory(_outputDirectory);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GenerateMarkdownFile(ArticleAnalysis analysis)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sb = &lt;span style="color:#ff7b72">new&lt;/span> StringBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> safeTitle = GenerateSafeFileName(analysis.Title);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> fileName = &lt;span style="color:#a5d6ff">$&amp;#34;{analysis.AnalyzedAt:yyyy-MM-dd}_{safeTitle}.md&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> filePath = Path.Combine(_outputDirectory, fileName);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;# {analysis.Title}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;## Article Information&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **Author:** {analysis.Author}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **URL:** [{analysis.Url}]({analysis.Url})&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **Published:** {analysis.PublishDate:yyyy-MM-dd}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **Analyzed:** {analysis.AnalyzedAt:yyyy-MM-dd HH:mm:ss}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **Tags:** {string.Join(&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;, analysis.Tags)}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;## AI Analysis&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;### Summary&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(analysis.Summary);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;### Key Topics&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(analysis.KeyTopics);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;### Relevance for Developers&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(analysis.Relevance);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;## Generated LinkedIn Post&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;```&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(analysis.LinkedInPost);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;```&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;*This analysis was generated using AI (Semantic Kernel with Azure OpenAI)*&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> File.WriteAllText(filePath, sb.ToString());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> filePath;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Конструктор принимает путь к выходному каталогу и создает его, если он не существует. Метод GenerateMarkdownFile принимает объект ArticleAnaлиз и создает красиво отформатированный документ с уценкой.&lt;/p>
&lt;p>Имя файла включает дату и исправленную версию названия. Это позволяет легко сортировать файлы в хронологическом порядке и сразу идентифицировать их.&lt;/p>
&lt;h3 id="обработка-небезопасных-имен-файлов">Обработка небезопасных имен файлов&lt;/h3>
&lt;p>Заголовки статей могут содержать символы, которые не разрешены в именах файлов, например двоеточия, косую черту, вопросительные знаки и кавычки. Нам нужно продезинфицировать следующее:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GenerateSafeFileName(&lt;span style="color:#ff7b72">string&lt;/span> title)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> invalidChars = Path.GetInvalidFileNameChars();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> safeTitle = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>(title
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(c =&amp;gt; !invalidChars.Contains(c))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToArray());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> safeTitle = safeTitle.Replace(&lt;span style="color:#a5d6ff">&amp;#34; &amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>).Replace(&lt;span style="color:#a5d6ff">&amp;#34;--&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (safeTitle.Length &amp;gt; &lt;span style="color:#a5d6ff">50&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> safeTitle = safeTitle.Substring(&lt;span style="color:#a5d6ff">0&lt;/span>, &lt;span style="color:#a5d6ff">50&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> safeTitle.TrimEnd(&lt;span style="color:#a5d6ff">&amp;#39;-&amp;#39;&lt;/span>).ToLowerInvariant();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Мы используем Path.GetInvalidFileNameChars(), чтобы получить список символов, которые не могут появляться в именах файлов в текущей операционной системе. Мы отфильтровываем их, заменяем пробелы дефисами для удобства чтения, ограничиваем длину 50 символами и преобразуем в нижний регистр для обеспечения единообразия.&lt;/p>
&lt;h2 id="настройка-уведомлений-telegram">Настройка уведомлений Telegram&lt;/h2>
&lt;h3 id="почему-я-выбрал-telegram">Почему я выбрал Telegram&lt;/h3>
&lt;p>Для компонента уведомлений я рассматривал несколько вариантов — электронная почта, SMS, Slack, Discord и Telegram. В конечном итоге я выбрал Telegram по нескольким причинам.&lt;/p>
&lt;p>API полностью бесплатен и не имеет ограничений по скорости для разумного использования. Многие службы уведомлений имеют ограничения на количество сообщений, которые вы можете отправить бесплатно, но Telegram не ограничивает сообщения ботов отдельными пользователями.&lt;/p>
&lt;p>API бота невероятно прост. Это просто HTTP-запросы с полезной нагрузкой JSON. Никаких сложных потоков аутентификации и веб-перехватчиков, необходимых для базовой функциональности.Telegram работает везде – на моем телефоне, на моем рабочем столе, в моем веб-браузере. Я могу получать уведомления, где бы я ни находился, и немедленно отвечать на них.&lt;/p>
&lt;p>Сообщения поддерживают расширенное форматирование. Я могу использовать жирный текст, курсив и даже блоки кода, чтобы сделать мои уведомления более читабельными.&lt;/p>
&lt;h3 id="создание-бота-в-telegram">Создание бота в Telegram&lt;/h3>
&lt;p>Настроить бота Telegram на удивление просто. Откройте Telegram и найдите @BotFather — это официальный бот Telegram для создания ботов и управления ими. Начните разговор с BotFather и отправьте команду /newbot. BotFather запросит у вас имя для вашего бота (это отображаемое имя) и имя пользователя (оно должно быть уникальным и заканчиваться на «bot»). Как только вы их предоставите, BotFather создаст вашего бота и предоставит вам токен API. Этот токен подобен паролю — храните его в секрете и не передавайте в публичные репозитории.&lt;/p>
&lt;p>Чтобы найти свой идентификатор чата, чтобы бот знал, куда отправлять сообщения, начните разговор с новым ботом, выполнив поиск и нажав «Старт». Затем откройте URL-адрес &lt;code>https://api.telegram.org/bot&amp;lt;YOUR_TOKEN&amp;gt;/getUpdates&lt;/code> в браузере или с помощью Curl. Найдите в ответе объект &lt;code>chat&lt;/code> — поле &lt;code>id&lt;/code> — это ваш идентификатор чата.&lt;/p>
&lt;h3 id="отправка-сообщений-через-api">Отправка сообщений через API&lt;/h3>
&lt;p>Вот наша функция для отправки сообщений Telegram:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task SendToTelegramAsync(&lt;span style="color:#ff7b72">string&lt;/span> botToken, &lt;span style="color:#ff7b72">string&lt;/span> chatId, &lt;span style="color:#ff7b72">string&lt;/span> message)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> telegramApiUrl = &lt;span style="color:#a5d6ff">$&amp;#34;https://api.telegram.org/bot{botToken}/sendMessage&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> payload = &lt;span style="color:#ff7b72">new&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chat_id = chatId,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> text = message,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parse_mode = &lt;span style="color:#a5d6ff">&amp;#34;HTML&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> jsonContent = JsonSerializer.Serialize(payload);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> content = &lt;span style="color:#ff7b72">new&lt;/span> StringContent(jsonContent, Encoding.UTF8, &lt;span style="color:#a5d6ff">&amp;#34;application/json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> httpClient.PostAsync(telegramApiUrl, content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!response.IsSuccessStatusCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> errorContent = &lt;span style="color:#ff7b72">await&lt;/span> response.Content.ReadAsStringAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">$&amp;#34;Telegram API error: {response.StatusCode} - {errorContent}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>API Telegram Bot основан на REST. Мы делаем POST-запрос к конечной точке sendMessage с телом JSON, содержащим идентификатор чата (куда отправлять), текст сообщения (что отправлять) и, при необходимости, режим анализа (для форматирования).&lt;/p>
&lt;p>Установка parse_mode на «HTML» позволяет нам использовать в наших сообщениях базовые HTML-теги – такие, как &lt;code>&amp;lt;b&amp;gt;bold&amp;lt;/b&amp;gt;&lt;/code> и &lt;code>&amp;lt;i&amp;gt;italic&amp;lt;/i&amp;gt;&lt;/code>. Это может сделать уведомления более читабельными, хотя в нашем текущем случае мы отправляем простой текст.&lt;/p>
&lt;p>Если запрос завершается неудачей, мы выдаем исключение с подробной информацией о том, что пошло не так. Это помогает при отладке, если что-то не работает.&lt;/p>
&lt;h2 id="настройка-приложения">Настройка приложения&lt;/h2>
&lt;h3 id="переменные-среды">Переменные среды&lt;/h3>
&lt;p>Нашему приложению требуется несколько частей конфиденциальной информации — ключи API, токены ботов и URL-адреса конечных точек. Мы никогда не должны жестко запрограммировать их или передать их под контроль версий. Вместо этого мы используем переменные среды, которые можно безопасно установить в каждой среде, в которой работает приложение.&lt;/p>
&lt;p>Для Telegram нам нужен TELEGRAM_BOT_TOKEN (токен, который вам дал BotFather) и TELEGRAM_CHAT_ID (ваш идентификатор чата, куда следует отправлять сообщения).&lt;/p>
&lt;p>Для Azure OpenAI нам нужны AZURE_OPENAI_ENDPOINT (URL-адрес вашего ресурса), AZURE_OPENAI_API_KEY (ваш ключ API) и AZURE_OPENAI_DEPLOYMENT (имя вашей развернутой модели, например «gpt-4o»).&lt;/p>
&lt;h3 id="загрузка-конфигурации-в-код">Загрузка конфигурации в код&lt;/h3>
&lt;p>Вот как мы загружаем эти значения при запуске приложения:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> telegramBotToken = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;TELEGRAM_BOT_TOKEN&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> telegramChatId = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;TELEGRAM_CHAT_ID&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> azureOpenAiEndpoint = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_ENDPOINT&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> azureOpenAiKey = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_API_KEY&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> azureOpenAiDeployment = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_DEPLOYMENT&amp;#34;&lt;/span>) ?? &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> aiAnalysisEnabled = !&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(azureOpenAiEndpoint) &amp;amp;&amp;amp;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> !&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(azureOpenAiKey);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Мы используем Environment.GetEnvironmentVariable для чтения каждого значения. В качестве имени развертывания мы предоставляем значение по умолчанию «gpt-4o», если значение не задано.&lt;/p>
&lt;p>Затем мы проверяем, следует ли включать анализ ИИ, проверяя, что у нас есть как конечная точка, так и ключ API. Это позволяет приложению работать в ухудшенном режиме, если Azure OpenAI не настроен — оно по-прежнему будет получать каналы и отслеживать статьи, только без анализа ИИ.### Грациозная деградация&lt;/p>
&lt;p>Эта концепция изящной деградации важна. Мы не хотим, чтобы приложение аварийно завершало работу только потому, что не настроена одна дополнительная функция:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>ArticleAnalyzer? articleAnalyzer = &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>MarkdownGenerator? markdownGenerator = &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">if&lt;/span> (aiAnalysisEnabled)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;🤖 AI Analysis enabled - Using Azure OpenAI with Semantic Kernel&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> articleAnalyzer = &lt;span style="color:#ff7b72">new&lt;/span> ArticleAnalyzer(azureOpenAiEndpoint!, azureOpenAiKey!, azureOpenAiDeployment);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> markdownGenerator = &lt;span style="color:#ff7b72">new&lt;/span> MarkdownGenerator(articlesOutputDir);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;ℹ️ AI Analysis disabled - Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY to enable&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Если AI включен, создаем анализатор и генератор уценки. Если нет, мы оставляем их нулевыми и пропускаем шаги, связанные с ИИ, во время обработки. Приложение по-прежнему приносит пользу, получая каналы и отправляя базовые уведомления, даже без усовершенствования искусственного интеллекта.&lt;/p>
&lt;h2 id="автоматизация-с-помощью-действий-github">Автоматизация с помощью действий GitHub&lt;/h2>
&lt;h3 id="почему-действия-github">Почему действия GitHub&lt;/h3>
&lt;p>Настоящая сила этого решения заключается в автоматизации. Мы не хотим вручную запускать приложение каждые несколько часов — мы хотим, чтобы оно запускалось автоматически в фоновом режиме.&lt;/p>
&lt;p>GitHub Actions идеально подходит для этого. Он встроен в GitHub, поэтому не требуется настраивать дополнительный сервис. Он бесплатен для общедоступных репозиториев и включает щедрые бесплатные минуты для частных репозиториев. Он может работать по расписанию, запуская наше приложение через определенные промежутки времени. Он имеет встроенное управление секретами для безопасного хранения наших ключей API. И он может фиксировать изменения обратно в репозиторий, поддерживая актуальность нашего файла отслеживания.&lt;/p>
&lt;h3 id="файл-рабочего-процесса">Файл рабочего процесса&lt;/h3>
&lt;p>Создайте файл .github/workflows/fetch-and-notify.yml со следующим содержимым:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Fetch DevBlogs and Notify&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">on&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">schedule&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">cron&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#39;0 */6 * * *&amp;#39;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">workflow_dispatch&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">jobs&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">fetch-and-notify&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">runs-on&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">ubuntu-latest&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">steps&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Checkout repository&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">uses&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">actions/checkout@v4&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Setup .NET&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">uses&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">actions/setup-dotnet@v4&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">with&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">dotnet-version&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#39;9.0.x&amp;#39;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Restore dependencies&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">dotnet restore src/VsFeedLinkedin.csproj&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Build&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">dotnet build src/VsFeedLinkedin.csproj --no-restore&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Run application&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">env&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">TELEGRAM_BOT_TOKEN&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.TELEGRAM_BOT_TOKEN }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">TELEGRAM_CHAT_ID&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.TELEGRAM_CHAT_ID }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">AZURE_OPENAI_ENDPOINT&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.AZURE_OPENAI_ENDPOINT }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">AZURE_OPENAI_API_KEY&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.AZURE_OPENAI_API_KEY }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">AZURE_OPENAI_DEPLOYMENT&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.AZURE_OPENAI_DEPLOYMENT }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">dotnet run --project src/VsFeedLinkedin.csproj&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Commit and push changes&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>|&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git config user.name &amp;#34;GitHub Actions Bot&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git config user.email &amp;#34;actions@github.com&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> if [[ -n $(git status --porcelain posted-articles.md generated-posts/) ]]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> TIMESTAMP=$(date +%Y%m%d_%H%M%S)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git add posted-articles.md generated-posts/
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git commit -m &amp;#34;chore($TIMESTAMP): processed new articles&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git push
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> else
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> echo &amp;#34;No changes to commit&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> fi&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Позвольте мне объяснить каждую часть. Раздел on определяет, когда запускается рабочий процесс. Триггер расписания использует синтаксис cron — &lt;code>0 */6 * * *&lt;/code> означает «в 0-ю минуту каждого 6-го часа». Таким образом, рабочий процесс выполняется в полночь, 6 утра, полдень и 18:00 по всемирному координированному времени. Триггер workflow_dispatch позволяет запускать вручную из пользовательского интерфейса GitHub, что полезно для тестирования.&lt;/p>
&lt;p>Задание выполняется на Ubuntu-latest, виртуальной машине Linux. Мы проверяем наш репозиторий, настраиваем .NET 9, восстанавливаем пакеты NuGet и собираем проект.&lt;/p>
&lt;p>На этапе «Запуск приложения» происходит волшебство. Мы передаем наши секреты как переменные среды, используя синтаксис ${{ secrets.SECRET_NAME }}. Эти секреты надежно хранятся в GitHub и никогда не раскрываются в журналах.&lt;/p>
&lt;p>Наконец, мы фиксируем все изменения обратно в репозиторий. Мы настраиваем Git с идентификатором бота, проверяем, есть ли какие-либо изменения в нашем файле отслеживания или каталоге сгенерированных сообщений, и если да, создаем коммит и отправляем его.&lt;/p>
&lt;h3 id="настройка-секретов">Настройка секретов&lt;/h3>
&lt;p>Чтобы добавить секреты в свой репозиторий GitHub, перейдите в «Настройки» вашего репозитория, затем «Секреты и переменные», затем «Действия». Нажмите «Новый секрет репозитория» и добавьте каждую переменную среды. Имена должны точно соответствовать тем, на которые мы ссылаемся в файле рабочего процесса.&lt;/p>
&lt;h2 id="подведение-итогов">Подведение итогов&lt;/h2>
&lt;h3 id="что-мы-создали">Что мы создали&lt;/h3>
&lt;p>Оглядываясь назад на все, что мы рассмотрели, мы создали комплексный агрегатор RSS-каналов на базе искусственного интеллекта, который автоматизирует то, что раньше было утомительным ручным процессом. Приложение автоматически отслеживает семь каналов Microsoft DevBlogs, отслеживая каждую новую статью сразу после ее публикации. Он справляется со сложностями дедупликации, распознавая, когда одна и та же статья появляется в нескольких каналах.Анализ ИИ на основе семантического ядра и Azure OpenAI считывает и понимает содержание статей, создает резюме, определяет ключевые темы и объясняет актуальность — и все это автоматически. Самое главное, он создает интересные публикации в LinkedIn, которыми я могу поделиться с минимальным редактированием.&lt;/p>
&lt;p>Интеграция Telegram означает, что я получаю уведомления на свой телефон, когда появляется новый контент для просмотра. Я могу взглянуть на сообщение, решить, хочу ли я им поделиться, и действовать немедленно.&lt;/p>
&lt;p>А поскольку он запускается в GitHub Actions по расписанию, мне не нужно ничего не забывать делать. Система работает в фоновом режиме, и я подключаюсь только тогда, когда есть что-то, чем стоит поделиться.&lt;/p>
&lt;h3 id="технологии-которые-сделали-это-возможным">Технологии, которые сделали это возможным&lt;/h3>
&lt;p>Этот проект объединил несколько технологий, каждая из которых сыграла решающую роль. .NET 9 обеспечил прочную основу благодаря своим современным языковым функциям и превосходной производительности. Семантическое ядро ​​упростило интеграцию ИИ, справившись со всей сложностью вызовов API и управлением ответами. Azure OpenAI предоставил интеллект — возможность действительно понимать и анализировать технический контент. HtmlAgilityPack решил сложную проблему извлечения чистого текста с веб-страниц. System.ServiceModel.Syndicate упростил анализ RSS. API Telegram Bot предоставил нам бесплатные и надежные уведомления. А GitHub Actions связал все это вместе с автоматическим запланированным выполнением.&lt;/p>
&lt;h3 id="думая-о-затратах">Думая о затратах&lt;/h3>
&lt;p>У вас может возникнуть один вопрос: сколько стоит его эксплуатация? Ответ: совсем немного.&lt;/p>
&lt;p>Telegram полностью бесплатен — за отправку сообщений через вашего бота плата не взимается.&lt;/p>
&lt;p>GitHub Actions бесплатен для общедоступных репозиториев. Для частных репозиториев вы получаете 2000 минут в месяц на бесплатном уровне, чего более чем достаточно для нашего варианта использования.&lt;/p>
&lt;p>Azure OpenAI — единственный платный компонент, а затраты минимальны. Используя GPT-4o, анализ типичной статьи в блоге стоит от одного до трех центов. Даже если вы обрабатываете десятки статей в месяц, затраты на ИИ составляют менее доллара.&lt;/p>
&lt;h3 id="где-вы-могли-бы-взять-это-дальше">Где вы могли бы взять это дальше&lt;/h3>
&lt;p>Хотя это решение отлично подходит для моих нужд, существует множество способов его расширения. Вы можете добавить поддержку нескольких социальных платформ — например, публиковать сообщения в Twitter/X, Mastodon или Bluesky в дополнение к LinkedIn. Вы можете реализовать анализ настроений, чтобы отслеживать тон статей с течением времени и выявлять тенденции. Вы можете разрешить разные шаблоны подсказок для разных каналов, создавая разные стили сообщений для разных тем. Вы можете создать веб-панель для просмотра и управления публикациями вместо использования Telegram. Вы можете отслеживать показатели вовлеченности публикуемого контента, чтобы увидеть, какие темы больше всего резонируют с вашей аудиторией.&lt;/p>
&lt;h3 id="заключительные-мысличто-мне-больше-всего-нравится-в-этом-проекте-так-это-то-что-он-воплощает-философию-в-которую-я-твердо-верю-автоматизация-должна-выполнять-утомительные-части-оставляя-творческую-часть-и-принятие-решений-людям-система-выполняет-всю-тяжелую-работу--выборку-анализ-анализ-генерацию--но-я-все-равно-проверяю-все-прежде-чем-поделиться-публикации-созданные-с-помощью-ии--это-отправная-точка-которую-я-могу-настроить-и-персонализировать">Заключительные мыслиЧто мне больше всего нравится в этом проекте, так это то, что он воплощает философию, в которую я твердо верю: автоматизация должна выполнять утомительные части, оставляя творческую часть и принятие решений людям. Система выполняет всю тяжелую работу — выборку, анализ, анализ, генерацию — но я все равно проверяю все, прежде чем поделиться. Публикации, созданные с помощью ИИ, — это отправная точка, которую я могу настроить и персонализировать.&lt;/h3>
&lt;p>Объединив возможности .NET, семантического ядра и Azure OpenAI, мы создали инструмент, который каждую неделю экономит часы ручной работы, сохраняя при этом качество и согласованность. Это своего рода практическая автоматизация, которая действительно меняет повседневную жизнь.&lt;/p>
&lt;p>Если вы создадите что-то подобное или у вас есть идеи по улучшению, я буду рад об этом услышать. Не стесняйтесь обращаться к LinkedIn!&lt;/p>
&lt;p>Приятного кодирования и счастливого Рождества! 🎄&lt;/p></content:encoded><category>.NET</category><category>Azure</category><category>NuGet</category><category>CI/CD</category><category>Docker</category><category>AI</category></item><item><title>Создание многоагентных систем искусственного интеллекта с помощью Microsoft Agent Framework</title><link>https://emimontesdeoca.github.io/ru/posts/microsoft-agent-framework-multi-agent/</link><pubDate>Mon, 01 Dec 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/microsoft-agent-framework-multi-agent/</guid><description>Практическое руководство по созданию, организации и развертыванию многоагентных систем искусственного интеллекта с использованием Microsoft Agent Framework в .NET.</description><content:encoded>&lt;h2 id="введение">Введение&lt;/h2>
&lt;p>Мы вступили в эпоху мультиагентных систем искусственного интеллекта. Вместо единого монолитного искусственного интеллекта, управляющего всем, отрасль движется к специализированным агентам, которые сотрудничают для решения сложных проблем — во многом подобно хорошо организованной команде экспертов. Один агент исследует, другой анализирует, третий пишет, а координатор держит всех в курсе.&lt;/p>
&lt;p>Если вы работали с большими языковыми моделями, вы, вероятно, достигли потолка того, что может сделать одно приглашение. Контекстные окна заполняются, инструкции путаются, качество ухудшается. Мультиагентные архитектуры решают эту проблему путем разложения сложных задач на конкретные обязанности, где каждый агент является экспертом в одном деле.&lt;/p>
&lt;p>Microsoft Agent Framework, являющаяся частью более широкой экосистемы семантического ядра, предоставляет разработчикам .NET первоклассный набор инструментов для создания именно таких систем. В этом посте мы пройдем путь от нуля до полностью работающего многоагентного конвейера, охватывая основные концепции, шаблоны оркестрации и практический код, необходимый для начала работы.&lt;/p>
&lt;h2 id="что-такое-платформа-агентов-microsoft">Что такое платформа агентов Microsoft?&lt;/h2>
&lt;p>Agent Framework — это ответ Microsoft на создание, оркестрацию и развертывание агентов искусственного интеллекта и многоагентных систем в .NET. Он находится рядом и глубоко интегрируется с семантическим ядром, которое с 2023 года является SDK Microsoft с открытым исходным кодом для оркестрации ИИ.&lt;/p>
&lt;p>Подумайте об этом так: &lt;strong>Семантическое ядро&lt;/strong> предоставляет вам примитивы (ядра, плагины, память, планировщики), а &lt;strong>Agent Framework&lt;/strong> предоставляет вам абстракции более высокого уровня, специально предназначенные для взаимодействия и координации между агентами.&lt;/p>
&lt;p>Платформа поддерживает несколько поставщиков моделей, включая Azure OpenAI, OpenAI и модели, размещенные в Azure AI Foundry. Его дизайн не зависит от модели, но он глубоко интегрирован с экосистемой Azure, что делает его особенно привлекательным для корпоративных сценариев.&lt;/p>
&lt;p>Ключевые возможности включают в себя:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Несколько типов агентов&lt;/strong>: &lt;code>ChatCompletionAgent&lt;/code>, &lt;code>OpenAIAssistantAgent&lt;/code> и &lt;code>AzureAIAgent&lt;/code> для разных серверов.&lt;/li>
&lt;li>&lt;strong>Шаблоны оркестровки&lt;/strong>: последовательные, одновременные, перераспределяющие и групповые рабочие процессы чата.&lt;/li>
&lt;li>&lt;strong>Экосистема плагинов&lt;/strong>: расширяйте возможности агентов с помощью встроенных функций C#, спецификаций OpenAPI и инструментов протокола контекста модели (MCP).&lt;/li>
&lt;li>&lt;strong>Управление беседами&lt;/strong>: встроенные функции управления потоками, историей и стратегиями завершения.&lt;/li>
&lt;li>&lt;strong>Наблюдаемость&lt;/strong>: интеграция с OpenTelemetry для отслеживания взаимодействия агентов.&lt;/li>
&lt;/ul>
&lt;h2 id="ключевые-понятия">Ключевые понятия&lt;/h2>
&lt;p>Прежде чем писать какой-либо код, давайте определимся со словарем. Agent Framework вращается вокруг нескольких основных абстракций.&lt;/p>
&lt;h3 id="агенты">Агенты&lt;/h3>
&lt;p>Агент — это объект, поддерживаемый моделью искусственного интеллекта, настроенный с использованием определенных инструкций (системного приглашения), имени и, при необходимости, набора плагинов или инструментов. Каждый агент — специалист — вы определяете, что он знает, что он может делать и как ему следует себя вести.&lt;/p>
&lt;h3 id="chatcompletionagentсамый-простой-тип-агента-он-оборачивает-конечную-точку-завершения-чата-azure-openai-openai-и-т-д-и-поддерживает-разговор-между-вызовами-он-не-имеет-состояния--вы-предоставляете-историю-и-он-отвечает-это-делает-его-легким-и-понятным">ChatCompletionAgentСамый простой тип агента. Он оборачивает конечную точку завершения чата (Azure OpenAI, OpenAI и т. д.) и поддерживает разговор. Между вызовами он не имеет состояния — вы предоставляете историю, и он отвечает. Это делает его легким и понятным.&lt;/h3>
&lt;p>[[[ТОК_3]]]&lt;/p>
&lt;h3 id="openaiassistantagent">OpenAIAssistantAgent&lt;/h3>
&lt;p>Этот тип агента использует API-интерфейс OpenAI Assistants, который обеспечивает состояние диалога на стороне сервера, обработку файлов и интерпретацию кода. Он более тяжелый, но дает вам постоянные потоки и встроенные инструменты, такие как интерпретатор кода и поиск файлов.&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;p>###ГрупповойЧат Агента&lt;/p>
&lt;p>Это оркестратор. &lt;code>AgentGroupChat&lt;/code> управляет многоэтапными разговорами между несколькими агентами, контролируя, кто говорит следующим, когда разговор заканчивается и как передается история. Именно здесь происходит волшебство многоагентного сотрудничества.&lt;/p>
&lt;h2 id="шаблоны-оркестровки">Шаблоны оркестровки&lt;/h2>
&lt;p>Платформа поддерживает четыре основных шаблона оркестровки, каждый из которых подходит для решения различных задач.&lt;/p>
&lt;h3 id="последовательный">Последовательный&lt;/h3>
&lt;p>Агенты выполняются один за другим в определенном порядке. Выходные данные агента A передаются агенту B, чьи выходные данные передаются агенту C. Это идеально подходит для конвейеров: черновик → просмотр → редактирование → публикация.&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;h3 id="параллельно">Параллельно&lt;/h3>
&lt;p>Несколько агентов одновременно работают с одним и тем же входом. Вы распределяете работу, а затем суммируете результаты. Отлично подходит для получения разных точек зрения — например, когда три рецензента рассматривают один и тот же запрос на включение.&lt;/p>
&lt;h3 id="передача">Передача&lt;/h3>
&lt;p>Агент решает передать управление другому агенту в зависимости от контекста разговора. Это имитирует работу команды обслуживания клиентов: агент первой линии обрабатывает базовые запросы и при необходимости передает их специалистам.&lt;/p>
&lt;h3 id="групповой-чат">Групповой чат&lt;/h3>
&lt;p>В открытом разговоре участвуют несколько агентов, по очереди, в зависимости от стратегии выбора. Класс &lt;code>AgentGroupChat&lt;/code> реализует этот шаблон с настраиваемой логикой очередности и завершения.&lt;/p>
&lt;h2 id="создайте-своего-первого-агента">Создайте своего первого агента&lt;/h2>
&lt;p>Давайте перейдем к практике. Вот как шаг за шагом создать своего первого агента.&lt;/p>
&lt;h3 id="предварительные-условия">Предварительные условия&lt;/h3>
&lt;p>Вам понадобится:&lt;/p>
&lt;ul>
&lt;li>.NET 9 SDK
— Ресурс Azure OpenAI с развернутой моделью (например, &lt;code>gpt-4o&lt;/code>).&lt;/li>
&lt;li>Visual Studio или VS Code&lt;/li>
&lt;/ul>
&lt;h3 id="настройка-проекта">Настройка проекта&lt;/h3>
&lt;p>Создайте новое консольное приложение и установите необходимые пакеты:&lt;/p>
&lt;p>[[[ТОК_9]]]&lt;/p>
&lt;h3 id="создание-простого-агента">Создание простого агента&lt;/h3>
&lt;p>Сначала настройте ядро с вашей конфигурацией Azure OpenAI:&lt;/p>
&lt;p>[[[ТОК_10]]]&lt;/p>
&lt;p>Теперь создайте агента и вызовите его:&lt;/p>
&lt;p>[[[ТОК_11]]]&lt;/p>
&lt;p>Вот и все. У вас есть работающий агент. Но настоящая сила приходит, когда агенты работают вместе.&lt;/p>
&lt;h2 id="многоагентная-оркестровка-с-помощью-agentgroupchat">Многоагентная оркестровка с помощью AgentGroupChat&lt;/h2>
&lt;p>Давайте создадим что-то более интересное — групповой чат, в котором сотрудничают несколько агентов. Класс &lt;code>AgentGroupChat&lt;/code> управляет ходом разговора, включая то, кто говорит следующим и когда остановиться.&lt;/p>
&lt;h3 id="определение-агентов">Определение агентов&lt;/h3>
&lt;p>Мы создадим трех агентов: писателя, рецензента и редактора.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>ChatCompletionAgent writer = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Writer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a content writer. When given a topic, produce a well-structured
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> draft. Focus &lt;span style="color:#ff7b72">on&lt;/span> clarity and technical accuracy.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> When you receive feedback &lt;span style="color:#ff7b72">from&lt;/span> the Reviewer, incorporate it &lt;span style="color:#ff7b72">into&lt;/span> a
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> revised draft.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent reviewer = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Reviewer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a content reviewer. Analyze drafts &lt;span style="color:#ff7b72">for&lt;/span> technical accuracy,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> clarity, and completeness. Provide specific, actionable feedback.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Do NOT rewrite the content &lt;span style="color:#f85149">—&lt;/span> only provide feedback.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> When the content meets your standards, respond with: APPROVED
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent editor = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Editor&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are an editor. Once content &lt;span style="color:#ff7b72">is&lt;/span> approved &lt;span style="color:#ff7b72">by&lt;/span> the Reviewer,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> polish it &lt;span style="color:#ff7b72">for&lt;/span> grammar, tone, and formatting.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Output only the final polished version.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> When you have produced the final version, respond with: COMPLETE
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="настройка-группового-чата">Настройка группового чата&lt;/h3>
&lt;p>Для &lt;code>AgentGroupChat&lt;/code> необходимы две ключевые конфигурации: &lt;strong>стратегия выбора&lt;/strong> (кто говорит следующим) и &lt;strong>стратегия завершения&lt;/strong> (когда остановиться).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>AgentGroupChat chat = &lt;span style="color:#ff7b72">new&lt;/span>(writer, reviewer, editor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ExecutionSettings = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> SelectionStrategy = &lt;span style="color:#ff7b72">new&lt;/span> SequentialSelectionStrategy(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TerminationStrategy = &lt;span style="color:#ff7b72">new&lt;/span> ApprovalTerminationStrategy()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Agents = [editor],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MaximumIterations = &lt;span style="color:#a5d6ff">12&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="пользовательская-стратегия-завершениястратегия-завершения-определяет-когда-разговор-заканчивается-вот-специальный-вариант-который-ищет-ключевое-слово-complete">Пользовательская стратегия завершенияСтратегия завершения определяет, когда разговор заканчивается. Вот специальный вариант, который ищет ключевое слово «COMPLETE»:&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ApprovalTerminationStrategy&lt;/span> : TerminationStrategy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt; ShouldAgentTerminateAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Agent agent,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IReadOnlyList&amp;lt;ChatMessageContent&amp;gt; history,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CancellationToken cancellationToken = &lt;span style="color:#ff7b72">default&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">bool&lt;/span> isComplete = history
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Last()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Content?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Contains(&lt;span style="color:#a5d6ff">&amp;#34;COMPLETE&amp;#34;&lt;/span>, StringComparison.OrdinalIgnoreCase) ?? &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Task.FromResult(isComplete);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="ведение-разговора">Ведение разговора&lt;/h3>
&lt;p>Начните разговор с пользовательского сообщения и позвольте агентам сотрудничать:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>chat.AddChatMessage(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ChatMessageContent(AuthorRole.User,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Write a 300-word technical overview of gRPC vs REST for microservices.&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (ChatMessageContent message &lt;span style="color:#ff7b72">in&lt;/span> chat.InvokeAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;[{message.AuthorName}]: {message.Content}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;---&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Поток будет выглядеть примерно так:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Автор&lt;/strong> создает черновик&lt;/li>
&lt;li>&lt;strong>Рецензент&lt;/strong> оставляет отзыв.&lt;/li>
&lt;li>&lt;strong>Автор&lt;/strong> вносит правки на основе отзывов.&lt;/li>
&lt;li>&lt;strong>Рецензент&lt;/strong> говорит: «УТВЕРЖДЕНО».&lt;/li>
&lt;li>&lt;strong>Редактор&lt;/strong> доводит до ума и говорит: «ЗАВЕРШЕНО».&lt;/li>
&lt;li>Разговор завершается.&lt;/li>
&lt;/ol>
&lt;p>Это движение вперед и назад продолжается автоматически до тех пор, пока не будет выполнено условие завершения или не будет достигнуто &lt;code>MaximumIterations&lt;/code>.&lt;/p>
&lt;h2 id="плагины-и-инструменты">Плагины и инструменты&lt;/h2>
&lt;p>Агенты становятся по-настоящему мощными, когда они могут взаимодействовать с внешними системами. Agent Framework поддерживает три основных механизма расширения.&lt;/p>
&lt;h3 id="собственные-функции-плагины-ядра">Собственные функции (плагины ядра)&lt;/h3>
&lt;p>Вы можете предоставить агентам доступ к методам C# в качестве инструментов. Агент вызовет эти функции, когда определит, что они необходимы:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ContentToolsPlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;word_count&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Counts the number of words in the provided text.&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> WordCount([Description(&lt;span style="color:#a5d6ff">&amp;#34;The text to count words in&amp;#34;&lt;/span>)] &lt;span style="color:#ff7b72">string&lt;/span> text)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> text.Split(&lt;span style="color:#a5d6ff">&amp;#39; &amp;#39;&lt;/span>, StringSplitOptions.RemoveEmptyEntries).Length;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;check_readability&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Calculates a readability score for the given text.&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> CheckReadability([Description(&lt;span style="color:#a5d6ff">&amp;#34;The text to analyze&amp;#34;&lt;/span>)] &lt;span style="color:#ff7b72">string&lt;/span> text)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> words = text.Split(&lt;span style="color:#a5d6ff">&amp;#39; &amp;#39;&lt;/span>, StringSplitOptions.RemoveEmptyEntries).Length;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sentences = text.Split([&lt;span style="color:#a5d6ff">&amp;#39;.&amp;#39;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#39;!&amp;#39;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#39;?&amp;#39;&lt;/span>], StringSplitOptions.RemoveEmptyEntries).Length;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (sentences == &lt;span style="color:#a5d6ff">0&lt;/span>) &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Unable to calculate — no sentences found.&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">double&lt;/span> avgWordsPerSentence = (&lt;span style="color:#ff7b72">double&lt;/span>)words / sentences;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> avgWordsPerSentence &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt; &lt;span style="color:#a5d6ff">15&lt;/span> =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Easy to read (avg {avgWordsPerSentence:F1} words/sentence)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt; &lt;span style="color:#a5d6ff">25&lt;/span> =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Moderate difficulty (avg {avgWordsPerSentence:F1} words/sentence)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Difficult to read (avg {avgWordsPerSentence:F1} words/sentence). Consider shorter sentences.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Зарегистрируйте плагин в ядре перед созданием агента:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>kernel.Plugins.AddFromType&amp;lt;ContentToolsPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent analyst = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;ContentAnalyst&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;Analyze content using available tools. Report word count and readability.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Kernel = kernel,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Arguments = &lt;span style="color:#ff7b72">new&lt;/span> KernelArguments(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> PromptExecutionSettings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="протокол-контекста-модели-mcp">Протокол контекста модели (MCP)&lt;/h3>
&lt;p>MCP — это открытый стандарт для подключения моделей ИИ к внешним инструментам и источникам данных. Платформа агентов поддерживает MCP, то есть ваши агенты могут использовать инструменты, предоставляемые любым MCP-совместимым сервером. Это открывает двери для файловых систем, баз данных, API и многого другого — и все это через стандартизированный интерфейс.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Example: Adding an MCP server for file operations&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>kernel.Plugins.AddFromMcpServer(&lt;span style="color:#a5d6ff">&amp;#34;filesystem&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Uri(&lt;span style="color:#a5d6ff">&amp;#34;http://localhost:3000/mcp&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это особенно интересно, поскольку означает, что ваши агенты не ограничиваются тем, что вы создаете — они могут подключиться к экосистеме инструментов MCP, которые другие разрабатывают и которыми делятся.&lt;/p>
&lt;h2 id="пример-из-реальной-жизни-конвейер-проверки-контента">Пример из реальной жизни: конвейер проверки контента&lt;/h2>
&lt;p>Давайте объединим все это с практическим сценарием. Представьте, что вы создаете внутренний инструмент, который автоматизирует проверку контента для группы документации. Трубопровод состоит из четырех этапов:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Исследователь&lt;/strong> — собирает соответствующую техническую информацию.&lt;/li>
&lt;li>&lt;strong>Писатель&lt;/strong> — создает черновик на основе исследований.&lt;/li>
&lt;li>&lt;strong>Рецензент&lt;/strong> — проверяет точность и полноту.&lt;/li>
&lt;li>&lt;strong>Издатель&lt;/strong> — форматирует и подготавливает окончательный результат.&lt;/li>
&lt;/ol>
&lt;p>Вот сокращенная реализация:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Agents&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Build the kernel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> credentials: &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Kernel kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Define specialized agents&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent researcher = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Researcher&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a technical researcher. Given a topic, identify the key
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> concepts, recent developments, and important details that should
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> be covered. Output a structured research brief.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent writer = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Writer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a technical writer. Using the research brief provided,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> write a comprehensive, well-structured article. Include code
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> examples &lt;span style="color:#ff7b72">where&lt;/span> relevant. Target audience: experienced developers.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent reviewer = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Reviewer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a senior technical reviewer. Check the article &lt;span style="color:#ff7b72">for&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Technical accuracy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Completeness relative to the research brief
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Code correctness
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Clarity and structure
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> If everything looks good, respond with APPROVED.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Otherwise, provide specific feedback &lt;span style="color:#ff7b72">for&lt;/span> revision.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent publisher = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Publisher&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a content publisher. Once the article &lt;span style="color:#ff7b72">is&lt;/span> approved,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> format it with proper markdown, &lt;span style="color:#ff7b72">add&lt;/span> a summary at the top,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> and ensure consistent formatting throughout.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> End your response with COMPLETE.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Configure the group chat&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>AgentGroupChat chat = &lt;span style="color:#ff7b72">new&lt;/span>(researcher, writer, reviewer, publisher)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ExecutionSettings = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> SelectionStrategy = &lt;span style="color:#ff7b72">new&lt;/span> SequentialSelectionStrategy(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TerminationStrategy = &lt;span style="color:#ff7b72">new&lt;/span> ApprovalTerminationStrategy()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Agents = [publisher],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MaximumIterations = &lt;span style="color:#a5d6ff">15&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Start the pipeline&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>chat.AddChatMessage(&lt;span style="color:#ff7b72">new&lt;/span> ChatMessageContent(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AuthorRole.User,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Create a technical article about implementing health checks in ASP.NET Core microservices.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (ChatMessageContent message &lt;span style="color:#ff7b72">in&lt;/span> chat.InvokeAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.ForegroundColor = message.AuthorName &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Researcher&amp;#34;&lt;/span> =&amp;gt; ConsoleColor.Cyan,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Writer&amp;#34;&lt;/span> =&amp;gt; ConsoleColor.Green,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Reviewer&amp;#34;&lt;/span> =&amp;gt; ConsoleColor.Yellow,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Publisher&amp;#34;&lt;/span> =&amp;gt; ConsoleColor.Magenta,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ =&amp;gt; ConsoleColor.White
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;\n{&amp;#39;=&amp;#39;new string(&amp;#39;=&amp;#39;, 60)}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;[{message.AuthorName}]&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>(&lt;span style="color:#a5d6ff">&amp;#39;=&amp;#39;&lt;/span>, &lt;span style="color:#a5d6ff">60&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(message.Content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.ResetColor();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Этот конвейер создает полностью исследованную, написанную, проверенную и отформатированную статью — и все это благодаря сотрудничеству агентов. Каждый агент фокусируется на том, что он делает лучше всего, а &lt;code>AgentGroupChat&lt;/code> координирует рабочий процесс.&lt;/p>
&lt;h2 id="лучшие-практики">Лучшие практики&lt;/h2>
&lt;p>После создания нескольких мультиагентных систем ниже приведены шаблоны и методы, которые я нашел наиболее ценными.&lt;/p>
&lt;h3 id="обработка-ошибок">Обработка ошибок&lt;/h3>
&lt;p>Всегда устанавливайте &lt;code>MaximumIterations&lt;/code> в своей стратегии завершения. Без этого агенты могут входить в бесконечные циклы, особенно когда рецензент продолжает находить проблемы, а автор продолжает вносить изменения без улучшений.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>TerminationStrategy = &lt;span style="color:#ff7b72">new&lt;/span> ApprovalTerminationStrategy()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MaximumIterations = &lt;span style="color:#a5d6ff">12&lt;/span> &lt;span style="color:#8b949e;font-style:italic">// Safety net&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Оберните вызовы агента в блоки try-catch. Ограничения скорости API, проблемы с сетью и ошибки моделей — все это реалии производственных систем:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> chat.InvokeAsync(cancellationToken))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Process messages&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">catch&lt;/span> (HttpOperationException ex) when (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Implement retry with exponential backoff&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.Delay(TimeSpan.FromSeconds(&lt;span style="color:#a5d6ff">30&lt;/span>), cancellationToken);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="наблюдаемостьплатформа-агента-интегрируется-с-opentelemetry-что-означает-что-вы-можете-отслеживать-каждое-взаимодействие-агента-вызов-инструмента-и-использование-токена-это-важно-для-отладки-многоагентных-рабочих-процессов-где-не-всегда-очевидно-какой-агент-вызвал-проблему">НаблюдаемостьПлатформа агента интегрируется с OpenTelemetry, что означает, что вы можете отслеживать каждое взаимодействие агента, вызов инструмента и использование токена. Это важно для отладки многоагентных рабочих процессов, где не всегда очевидно, какой агент вызвал проблему.&lt;/h3>
&lt;p>Настройте базовую телеметрию, добавив пакеты телеметрии семантического ядра и настроив предпочитаемый экспортер (Application Insights, Jaeger и т. д.):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddOpenTelemetry()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithTracing(tracing =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tracing.AddSource(&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.SemanticKernel*&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tracing.AddOtlpExporter();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="управление-затратами">Управление затратами&lt;/h3>
&lt;p>Мультиагентные системы увеличивают ваши затраты на API: каждый поворот агента — это вызов API, а групповые чаты могут генерировать множество поворотов. Некоторые стратегии, позволяющие держать расходы под контролем:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Используйте более дешевые модели для более простых агентов&lt;/strong>: не каждому агенту требуется GPT-4o. Рецензент может прекрасно работать с моделью меньшего размера, в то время как мощный нападающий нужен только автору.&lt;/li>
&lt;li>&lt;strong>Ограничить историю&lt;/strong>: используйте &lt;code>ReducedHistoryCount&lt;/code> в настройках выполнения, чтобы ограничить объем контекста разговора, который получает каждый агент.&lt;/li>
&lt;li>&lt;strong>Установите строгие ограничения на итерацию&lt;/strong>: предотвращайте неконтролируемые разговоры с разумными значениями &lt;code>MaximumIterations&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Кэшировать, когда это возможно&lt;/strong>: если агент неоднократно выполняет один и тот же поиск, кэшируйте результаты в плагине.&lt;/li>
&lt;/ul>
&lt;h3 id="дизайн-агента">Дизайн агента&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Соблюдайте четкость инструкций&lt;/strong>: у каждого агента должна быть единая и четкая ответственность. Широкие инструкции приводят к посредственной производительности во всех задачах.
– &lt;strong>Четко указывайте формат вывода&lt;/strong>. Сообщите агентам, как именно структурировать свои ответы. Это делает последующий синтаксический анализ надежным.
– &lt;strong>Используйте ключевые слова завершения&lt;/strong>. Определите четкие сигналы (например, «УТВЕРЖДЕНО» или «ЗАВЕРШЕНО»), которые агенты будут использовать, чтобы указать, что они закончили. Это делает стратегии завершения простыми и предсказуемыми.&lt;/li>
&lt;/ul>
&lt;h2 id="заключение">Заключение&lt;/h2>
&lt;p>Многоагентные системы искусственного интеллекта представляют собой фундаментальный сдвиг в том, как мы создаем интеллектуальные приложения. Вместо того, чтобы бороться с единым запросом, чтобы справиться со всем, мы можем разложить проблемы на специализированные роли и позволить агентам сотрудничать.&lt;/p>
&lt;p>Microsoft Agent Framework делает это практичным для разработчиков .NET. Абстракции чистые — агенты, групповые чаты, стратегии выбора и прекращения — и они складываются естественным образом. В сочетании с экосистемой подключаемых модулей Semantic Kernel и хостингом моделей Azure у вас есть полный стек для создания многоагентных систем производственного уровня.&lt;/p>
&lt;p>Платформа все еще развивается (многие пакеты находятся в предварительной версии), но основные шаблоны надежны и направление ясно. Если вы создаете приложения на основе искусственного интеллекта в .NET, сейчас самое время начать экспериментировать с многоагентными архитектурами.&lt;/p>
&lt;p>Начните с малого — создайте двух агентов, которые совместно выполняют одну простую задачу. Как только вы увидите улучшение качества по сравнению с подходами с одним агентом, вам захочется разложить все на команды агентов.&lt;/p>
&lt;p>Приятного кодирования!&lt;/p></content:encoded><category>AI</category><category>.NET</category><category>Agent Framework</category><category>Semantic Kernel</category></item><item><title>Рендеринг необработанного HTML в Blazor с помощью MarkupString</title><link>https://emimontesdeoca.github.io/ru/posts/blazor-markup-string-raw-html/</link><pubDate>Sat, 22 Nov 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/blazor-markup-string-raw-html/</guid><description>Безопасно визуализируйте необработанный HTML-контент в компонентах Blazor, используя MarkupString вместо экранированного текста.</description><content:encoded>&lt;p>На днях я создавал компонент, который должен был отображать HTML-код, полученный из CMS. У меня была строка HTML в переменной, и я просто поместил ее в шаблон, например &lt;code>@myHtml&lt;/code>. И, конечно же, Blazor избегал всего и отображал настоящие теги в виде текста на странице. Не то, что я хотел.&lt;/p>
&lt;h1 id="проблема">Проблема&lt;/h1>
&lt;p>По умолчанию Blazor кодирует любую строку, отображаемую в шаблоне. Итак, если у вас есть:&lt;/p>
&lt;p>[[[ТОК_1]]]&lt;/p>
&lt;p>И вы делаете это:&lt;/p>
&lt;p>[[[ТОК_2]]]&lt;/p>
&lt;p>На странице вы увидите буквальный текст &lt;code>&amp;lt;strong&amp;gt;Hello&amp;lt;/strong&amp;gt; &amp;lt;em&amp;gt;world&amp;lt;/em&amp;gt;&lt;/code> вместо &lt;strong>Hello&lt;/strong> &lt;em>world&lt;/em>. Blazor делает это специально, чтобы предотвратить XSS-атаки, что является правильным поведением по умолчанию.&lt;/p>
&lt;h1 id="решение-markupstring">Решение: MarkupString&lt;/h1>
&lt;p>Если вам действительно нужно отобразить необработанный HTML, вы заключаете строку в &lt;code>MarkupString&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;@((MarkupString)content)&amp;lt;/&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>И все! Теперь Blazor будет отображать HTML как фактическую разметку. Вы также можете присвоить его переменной:&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;p>[[[ТОК_7]]]&lt;/p>
&lt;h1 id="реальный-пример">Реальный пример&lt;/h1>
&lt;p>Я извлекал контент блога из API, и мне нужно было отобразить его в компоненте предварительного просмотра. В контенте были все виды HTML — заголовки, блоки кода, ссылки, изображения. Вот примерно как это выглядело:&lt;/p>
&lt;p>[[[ТОК_8]]]&lt;/p>
&lt;p>Работает отлично. HTML из API отображается как фактическая разметка.&lt;/p>
&lt;h1 id="будьте-осторожны-с-ненадежным-контентом">Будьте осторожны с ненадежным контентом&lt;/h1>
&lt;p>Это важно: &lt;code>MarkupString&lt;/code> &lt;strong>не&lt;/strong> очищает HTML. Он отображает все, что вы ему дадите, включая теги &lt;code>&amp;lt;script&amp;gt;&lt;/code>. Поэтому, если контент получен пользователем или из ненадежного источника, вам необходимо сначала его очистить.&lt;/p>
&lt;p>В Blazor нет встроенного средства очистки HTML, но вы можете использовать такую библиотеку, как &lt;a href="https://github.com/mganss/HtmlSanitizer">HtmlSanitizer&lt;/a>:&lt;/p>
&lt;p>[[[ТОК_12]]]&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;@SafeHtml(untrustedContent)&amp;lt;/&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это удаляет опасные элементы, такие как обработчики &lt;code>&amp;lt;script&amp;gt;&lt;/code>, &lt;code>onclick&lt;/code> и другие элементы, которые вы не хотите отображать из пользовательского контента.&lt;/p>
&lt;h1 id="когда-его-использовать">Когда его использовать&lt;/h1>
&lt;p>Я использую &lt;code>MarkupString&lt;/code> для:&lt;/p>
&lt;ul>
&lt;li>Содержимое CMS или уценка, преобразованные в HTML на стороне сервера.&lt;/li>
&lt;li>Вывод расширенного текстового редактора&lt;/li>
&lt;li>Предварительный просмотр шаблонов электронной почты&lt;/li>
&lt;li>Любой готовый HTML из надежных источников.&lt;/li>
&lt;/ul>
&lt;p>Все, что исходит от пользователя, всегда сначала очищайте. Лучше перестраховаться, чем потом сожалеть.&lt;/p>
&lt;p>Надеюсь, вам понравился пост! Не стесняйтесь обращаться ко мне в любой социальной сети по адресу &lt;strong>@emimontesdeoca&lt;/strong>.&lt;/p>
&lt;h1 id="ресурсы">Ресурсы&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.markupstring">Структура MarkupString&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/security/content-security-policy">Предотвращение Blazor XSS&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>C#</category></item><item><title>Что нового в EF Core 9: функции, которые вам нужно знать</title><link>https://emimontesdeoca.github.io/ru/posts/whats-new-ef-core-9/</link><pubDate>Tue, 18 Nov 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/whats-new-ef-core-9/</guid><description>Всесторонний обзор наиболее эффективных функций Entity Framework Core 9 — от улучшений LINQ и массовых операций до столбцов JSON и поддержки компиляции AOT.</description><content:encoded>&lt;p>Entity Framework Core 9 был выпущен вместе с .NET 9 в ноябре 2024 года, и, потратив немало времени на работу с ним в нескольких проектах, я могу сказать, что это один из самых значимых выпусков за последнее время. Не потому, что он заново изобретает велосипед, а потому, что он оттачивает те области, в которых EF Core исторически вызывал больше всего проблем — преобразование запросов, производительность и работу с современными шаблонами данных.&lt;/p>
&lt;p>В этом посте я расскажу о функциях, которые оказали наибольшее влияние на мою повседневную работу. Если вы все еще используете EF Core 8 (или даже 7), это должно дать вам четкое представление о том, что вас ждет по другую сторону обновления.&lt;/p>
&lt;h2 id="ef-core-9-в-экосистеме-net-9">EF Core 9 в экосистеме .NET 9&lt;/h2>
&lt;p>EF Core 9 ориентирован на .NET 8 и .NET 9, а это значит, что вам не обязательно обновлять все приложение до .NET 9, чтобы воспользоваться большинством этих функций. Тем не менее, некоторые улучшения AOT и производительности тесно связаны с изменениями среды выполнения .NET 9, поэтому вы получите максимальную отдачу, пройдя весь путь.&lt;/p>
&lt;p>Выпуск следует за нечетной/четной периодичностью, установленной Microsoft: выпуски с нечетными номерами (например, .NET 9) имеют стандартную поддержку (STS) с 18 месяцами поддержки, а четные выпуски (например, .NET 8) — долгосрочную поддержку (LTS). Имейте это в виду при планировании графика обновления.&lt;/p>
&lt;h2 id="улучшения-перевода-linq">Улучшения перевода LINQ&lt;/h2>
&lt;p>Именно здесь большинство разработчиков сразу почувствуют разницу. EF Core 9 добился значительных успехов в переводе выражений LINQ в SQL, что действительно имеет смысл.&lt;/p>
&lt;h3 id="лучшие-переводы-groupby">Лучшие переводы GroupBy&lt;/h3>
&lt;p>Если вы когда-либо писали запрос &lt;code>GroupBy&lt;/code> в EF Core и в итоге получали предупреждения при оценке на стороне клиента или странный SQL, вы знаете, что это за боль. EF Core 9 обрабатывает гораздо более широкий набор сценариев &lt;code>GroupBy&lt;/code> непосредственно в SQL.&lt;/p>
&lt;p>[[[ТОК_2]]]&lt;/p>
&lt;p>В предыдущих версиях запросы, включающие агрегирование свойств навигации внутри &lt;code>GroupBy&lt;/code> иногда возвращались к оценке клиента. EF Core 9 аккуратно преобразует это в один SQL-запрос с помощью &lt;code>GROUP BY&lt;/code>, &lt;code>SUM&lt;/code>, &lt;code>AVG&lt;/code> и &lt;code>COUNT&lt;/code>.&lt;/p>
&lt;h3 id="сложные-проекции-и-подзапросы">Сложные проекции и подзапросы&lt;/h3>
&lt;p>Вложенные подзапросы и сложные проекции также получили серьезное обновление. Рассмотрим что-то вроде этого:&lt;/p>
&lt;p>[[[ТОК_8]]]&lt;/p>
&lt;p>EF Core 9 теперь может преобразовать все это выражение в SQL, не запуская оценку на стороне клиента. Сгенерированный запрос использует коррелированные подзапросы и боковые соединения, где это необходимо, а план SQL значительно более эффективен, чем тот, который создавался в более ранних версиях.&lt;/p>
&lt;h3 id="параметризованные-примитивные-коллекции">Параметризованные примитивные коллекции&lt;/h3>
&lt;p>Одним из выдающихся улучшений LINQ является возможность передавать коллекции примитивных значений непосредственно в запросы:&lt;/p>
&lt;p>[[[ТОК_9]]]&lt;/p>
&lt;p>В EF Core 8 это было преобразовано с использованием предложений &lt;code>IN&lt;/code> со встроенными значениями, что означало, что кэш плана запроса нельзя было повторно использовать при изменении списка. EF Core 9 правильно параметризует эти коллекции, отправляя их как структурированный параметр. Это очень важно для кэширования плана запроса на SQL Server и PostgreSQL.## Массовые операции — ExecuteUpdate и ExecuteDelete&lt;/p>
&lt;p>&lt;code>ExecuteUpdate&lt;/code> и &lt;code>ExecuteDelete&lt;/code> были представлены в EF Core 7, но EF Core 9 существенно расширяет возможности, которые вы можете с ними делать.&lt;/p>
&lt;h3 id="более-сложные-выражения-обновления">Более сложные выражения обновления&lt;/h3>
&lt;p>Теперь вы можете использовать более сложные выражения в &lt;code>ExecuteUpdate&lt;/code>, включая ссылки на другие таблицы через свойства навигации:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> context.Products
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(p =&amp;gt; p.Category.IsDiscontinued)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ExecuteUpdateAsync(setters =&amp;gt; setters
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SetProperty(p =&amp;gt; p.IsAvailable, &lt;span style="color:#79c0ff">false&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SetProperty(p =&amp;gt; p.DiscontinuedDate, DateTimeOffset.UtcNow)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SetProperty(p =&amp;gt; p.Price, p =&amp;gt; p.Price * &lt;span style="color:#a5d6ff">0.5&lt;/span>m));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>При этом генерируется один оператор &lt;code>UPDATE&lt;/code> с &lt;code>JOIN&lt;/code> для таблицы категорий — нет необходимости загружать сущности в память, нет накладных расходов на отслеживание изменений.&lt;/p>
&lt;h3 id="условное-массовое-удаление-с-подзапросами">Условное массовое удаление с подзапросами&lt;/h3>
&lt;p>Массовые удаления с помощью фильтров подзапросов теперь полностью поддерживаются:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> context.AuditLogs
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(log =&amp;gt; log.CreatedAt &amp;lt; DateTime.UtcNow.AddYears(-&lt;span style="color:#a5d6ff">2&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(log =&amp;gt; !context.ProtectedRecords
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Any(pr =&amp;gt; pr.AuditLogId == log.Id))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ExecuteDeleteAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это преобразуется в &lt;code>DELETE&lt;/code> с подзапросом &lt;code>NOT EXISTS&lt;/code> — именно то, что вы написали бы вручную. Никаких сущностей не загружено, никаких обращений туда и обратно.&lt;/p>
&lt;h2 id="улучшения-столбца-json">Улучшения столбца JSON&lt;/h2>
&lt;p>Столбцы JSON были одной из самых интересных функций в последних выпусках EF Core, а EF Core 9 развивает их дальше.&lt;/p>
&lt;h3 id="запрос-внутри-json">Запрос внутри JSON&lt;/h3>
&lt;p>Теперь вы можете фильтровать и проецировать данные из столбцов JSON с улучшенной поддержкой перевода:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Order&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> CustomerName { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ShippingAddress Address { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } &lt;span style="color:#8b949e;font-style:italic">// Stored as JSON&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;OrderNote&amp;gt; Notes { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } &lt;span style="color:#8b949e;font-style:italic">// Stored as JSON&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ShippingAddress&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Street { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> City { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> State { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> ZipCode { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Country { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">OrderNote&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime CreatedAt { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Text { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Author { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Конфигурация в вашем &lt;code>DbContext&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnModelCreating(ModelBuilder modelBuilder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> modelBuilder.Entity&amp;lt;Order&amp;gt;(builder =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.OwnsOne(o =&amp;gt; o.Address, ab =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ab.ToJson();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.OwnsMany(o =&amp;gt; o.Notes, nb =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> nb.ToJson();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Теперь вы можете делать запросы непосредственно в JSON:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> newYorkOrders = &lt;span style="color:#ff7b72">await&lt;/span> context.Orders
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(o =&amp;gt; o.Address.State == &lt;span style="color:#a5d6ff">&amp;#34;NY&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(o =&amp;gt; o.Notes.Count)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(o =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> o.CustomerName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> o.Address.City,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> LatestNote = o.Notes
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(n =&amp;gt; n.CreatedAt)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(n =&amp;gt; n.Text)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .FirstOrDefault()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>EF Core 9 генерирует правильные вызовы &lt;code>JSON_VALUE&lt;/code> и &lt;code>JSON_QUERY&lt;/code> для SQL Server (или эквивалента у других поставщиков), а перевод охватывает гораздо более широкий диапазон операций LINQ с элементами JSON, чем раньше.&lt;/p>
&lt;h3 id="обновление-свойств-json">Обновление свойств JSON&lt;/h3>
&lt;p>Одним из проблем в EF Core 8 было то, что обновление одного свойства внутри столбца JSON приводило к переписыванию всего документа JSON. EF Core 9 улучшает эту ситуацию за счет более детального отслеживания изменений для типов, отображаемых в формате JSON, и по возможности генерирует более целевые обновления.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> order = &lt;span style="color:#ff7b72">await&lt;/span> context.Orders.FindAsync(orderId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>order.Address.ZipCode = &lt;span style="color:#a5d6ff">&amp;#34;10001&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> context.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>У поддерживаемых поставщиков это может привести к более целенаправленной модификации JSON, а не к переписыванию всего большого двоичного объекта.&lt;/p>
&lt;h2 id="сложные-типы--объекты-значения-без-идентификатора">Сложные типы — объекты-значения без идентификатора&lt;/h2>
&lt;p>Сложные типы — это одна из функций, которую так ждали специалисты по предметно-ориентированному проектированию. В отличие от принадлежащих типов, сложные типы не имеют идентичности — это чистые объекты значений.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[ComplexType]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Money&lt;/span>(&lt;span style="color:#ff7b72">decimal&lt;/span> Amount, &lt;span style="color:#ff7b72">string&lt;/span> Currency);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[ComplexType]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">DateRange&lt;/span>(DateTime Start, DateTime End);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Project&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Money Budget { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateRange Timeline { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Они сохраняются в виде объединенных столбцов в родительской таблице — &lt;code>Budget_Amount&lt;/code>, &lt;code>Budget_Currency&lt;/code>, &lt;code>Timeline_Start&lt;/code>, &lt;code>Timeline_End&lt;/code> — без необходимости использования отдельной таблицы или какого-либо ключа.&lt;/p>
&lt;p>Ключевое отличие от принадлежащих типов: сложные типы сравниваются по значению, а не по ссылке. Два экземпляра &lt;code>Money&lt;/code> с одинаковыми &lt;code>Amount&lt;/code> и &lt;code>Currency&lt;/code> считаются равными, независимо от того, какой сущности они принадлежат.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> expensiveProjects = &lt;span style="color:#ff7b72">await&lt;/span> context.Projects
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(p =&amp;gt; p.Budget.Amount &amp;gt; &lt;span style="color:#a5d6ff">100_000&lt;/span>m &amp;amp;&amp;amp; p.Budget.Currency == &lt;span style="color:#a5d6ff">&amp;#34;USD&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(p =&amp;gt; p.Budget.Amount)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это напрямую приводит к фильтрации по сглаженным столбцам — чистой, эффективной и именно такой, как вы ожидаете.&lt;/p>
&lt;h2 id="поддержка-hierarchyid-для-sql-server">Поддержка HierarchyId для SQL Server&lt;/h2>
&lt;p>Если вы когда-либо работали с иерархическими данными в SQL Server — организационными диаграммами, деревьями категорий, файловыми системами — вы знаете, что &lt;code>HierarchyId&lt;/code> — это встроенный тип для этого. EF Core 9 обеспечивает первоклассную поддержку.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Employee&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Title { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> HierarchyId PathFromCeo { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Теперь вы можете напрямую запрашивать иерархические отношения:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> managerId = HierarchyId.Parse(&lt;span style="color:#a5d6ff">&amp;#34;/1/3/&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// All direct and indirect reports&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> allReports = &lt;span style="color:#ff7b72">await&lt;/span> context.Employees
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(e =&amp;gt; e.PathFromCeo.IsDescendantOf(managerId))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(e =&amp;gt; e.PathFromCeo != managerId) &lt;span style="color:#8b949e;font-style:italic">// exclude the manager&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderBy(e =&amp;gt; e.PathFromCeo)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Direct reports only (one level down)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> directReports = &lt;span style="color:#ff7b72">await&lt;/span> context.Employees
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(e =&amp;gt; e.PathFromCeo.GetAncestor(&lt;span style="color:#a5d6ff">1&lt;/span>) == managerId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Get an employee&amp;#39;s depth in the hierarchy&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> employeesWithDepth = &lt;span style="color:#ff7b72">await&lt;/span> context.Employees
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(e =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> e.Name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> e.Title,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Level = e.PathFromCeo.GetLevel()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderBy(e =&amp;gt; e.Level)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```Все&lt;/span> &lt;span style="color:#f85149">это&lt;/span> &lt;span style="color:#f85149">преобразуется&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">собственные&lt;/span> &lt;span style="color:#f85149">методы&lt;/span> SQL Server &lt;span style="color:#f85149">`&lt;/span>HierarchyId&lt;span style="color:#f85149">`&lt;/span>. &lt;span style="color:#f85149">Если&lt;/span> &lt;span style="color:#f85149">вы&lt;/span> &lt;span style="color:#f85149">реализуете&lt;/span> &lt;span style="color:#f85149">древовидные&lt;/span> &lt;span style="color:#f85149">структуры&lt;/span> &lt;span style="color:#f85149">с&lt;/span> &lt;span style="color:#f85149">самоссылающимися&lt;/span> &lt;span style="color:#f85149">внешними&lt;/span> &lt;span style="color:#f85149">ключами&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">рекурсивными&lt;/span> CTE, &lt;span style="color:#f85149">это&lt;/span> &lt;span style="color:#f85149">гораздо&lt;/span> &lt;span style="color:#f85149">более&lt;/span> &lt;span style="color:#f85149">чистый&lt;/span> &lt;span style="color:#f85149">подход&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> &lt;span style="color:#f85149">Скомпилированные&lt;/span> &lt;span style="color:#f85149">модели&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">поддержка&lt;/span> AOT
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Разработчики&lt;/span>, &lt;span style="color:#f85149">заботящиеся&lt;/span> &lt;span style="color:#f85149">о&lt;/span> &lt;span style="color:#f85149">производительности&lt;/span>, &lt;span style="color:#f85149">оценят&lt;/span> &lt;span style="color:#f85149">постоянные&lt;/span> &lt;span style="color:#f85149">инвестиции&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">скомпилированные&lt;/span> &lt;span style="color:#f85149">модели&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">поддержку&lt;/span> &lt;span style="color:#f85149">предварительной&lt;/span> &lt;span style="color:#f85149">компиляции&lt;/span> (AOT).
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">###&lt;/span> &lt;span style="color:#f85149">Скомпилированные&lt;/span> &lt;span style="color:#f85149">модели&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Скомпилированные&lt;/span> &lt;span style="color:#f85149">модели&lt;/span> &lt;span style="color:#f85149">предварительно&lt;/span> &lt;span style="color:#f85149">создают&lt;/span> &lt;span style="color:#f85149">метаданные&lt;/span> &lt;span style="color:#f85149">модели&lt;/span>, &lt;span style="color:#f85149">которые&lt;/span> EF Core &lt;span style="color:#f85149">обычно&lt;/span> &lt;span style="color:#f85149">создает&lt;/span> &lt;span style="color:#f85149">при&lt;/span> &lt;span style="color:#f85149">запуске&lt;/span>. &lt;span style="color:#f85149">Для&lt;/span> &lt;span style="color:#f85149">больших&lt;/span> &lt;span style="color:#f85149">моделей&lt;/span> (&lt;span style="color:#f85149">например&lt;/span>, &lt;span style="color:#f85149">сотен&lt;/span> &lt;span style="color:#f85149">объектов&lt;/span>) &lt;span style="color:#f85149">это&lt;/span> &lt;span style="color:#f85149">может&lt;/span> &lt;span style="color:#f85149">значительно&lt;/span> &lt;span style="color:#f85149">сократить&lt;/span> &lt;span style="color:#f85149">время&lt;/span> &lt;span style="color:#f85149">холодного&lt;/span> &lt;span style="color:#f85149">запуска&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>bash
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet ef dbcontext optimize --output-dir CompiledModels --&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">MyApp.CompiledModels&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Затем подключите его:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddDbContext&amp;lt;AppDbContext&amp;gt;(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .UseSqlServer(connectionString)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .UseModel(MyApp.CompiledModels.AppDbContextModel.Instance));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>В EF Core 9 скомпилированные модели являются более полными — они поддерживают больше функций сопоставления и генерируют меньший объем выходных данных. Для модели, содержащей около 400 объектов, время запуска может сократиться с нескольких секунд до почти мгновенного.&lt;/p>
&lt;h3 id="ход-компиляции-aot">Ход компиляции AOT&lt;/h3>
&lt;p>Полная встроенная поддержка AOT для EF Core все еще находится в стадии разработки, но EF Core 9 добился значительных успехов. Многие пути кода с большим количеством отражений были реорганизованы для обеспечения возможности обрезки, а скомпилированные модели являются ключевой частью истории AOT. Если вы ориентируетесь на такие сценарии, как функции Azure или микросервисы, где важен холодный запуск, эти улучшения имеют непосредственное значение.&lt;/p>
&lt;h2 id="обновления-поставщика-cosmo-db">Обновления поставщика Cosmo DB&lt;/h2>
&lt;p>Поставщик Azure Cosmos DB продолжает развиваться с помощью EF Core 9. Некоторые заметные улучшения:&lt;/p>
&lt;h3 id="обработка-ключей-раздела">Обработка ключей раздела&lt;/h3>
&lt;p>Поставщик теперь поддерживает иерархические ключи разделов и более разумно обрабатывает фильтры ключей разделов:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TenantDocument&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> TenantId { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } &lt;span style="color:#8b949e;font-style:italic">// Partition key&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Region { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } &lt;span style="color:#8b949e;font-style:italic">// Sub-partition key&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Content { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// This query now correctly uses the partition key for routing&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> docs = &lt;span style="color:#ff7b72">await&lt;/span> context.TenantDocuments
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(d =&amp;gt; d.TenantId == &lt;span style="color:#a5d6ff">&amp;#34;tenant-42&amp;#34;&lt;/span> &amp;amp;&amp;amp; d.Region == &lt;span style="color:#a5d6ff">&amp;#34;us-east&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(d =&amp;gt; d.Content.Contains(&lt;span style="color:#a5d6ff">&amp;#34;important&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="улучшен-перевод-linq-в-nosql">Улучшен перевод LINQ в NoSQL&lt;/h3>
&lt;p>Больше операций LINQ теперь переводится на диалект SQL Cosmos DB, включая улучшенную поддержку &lt;code>Contains&lt;/code>, &lt;code>Any&lt;/code>, операций с вложенными массивами и математических функций. Запросы, которые раньше возвращались к оценке клиента, теперь обрабатываются на стороне сервера.&lt;/p>
&lt;h3 id="поддержка-векторного-поиска">Поддержка векторного поиска&lt;/h3>
&lt;p>В EF Core 9 на ранней стадии реализована поддержка поиска по сходству векторов с помощью Cosmos DB, что полезно, если вы создаете приложения, которые интегрируются с внедрениями или поиском на основе искусственного интеллекта:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> results = &lt;span style="color:#ff7b72">await&lt;/span> context.Documents
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderBy(d =&amp;gt; EF.Functions.VectorDistance(d.Embedding, queryVector))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Take(&lt;span style="color:#a5d6ff">10&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="улучшения-миграции">Улучшения миграции&lt;/h2>
&lt;p>Миграции получили некоторые улучшения качества жизни, которые делают работу с ними менее болезненной в командной среде.&lt;/p>
&lt;h3 id="временные-таблицы-в-миграции">Временные таблицы в миграции&lt;/h3>
&lt;p>Миграции теперь обрабатывают конфигурацию темпоральной таблицы более изящно, с надлежащей поддержкой столбцов периода и именования таблиц истории:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnModelCreating(ModelBuilder modelBuilder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> modelBuilder.Entity&amp;lt;Employee&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToTable(&lt;span style="color:#a5d6ff">&amp;#34;Employees&amp;#34;&lt;/span>, b =&amp;gt; b.IsTemporal(t =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> t.HasPeriodStart(&lt;span style="color:#a5d6ff">&amp;#34;ValidFrom&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> t.HasPeriodEnd(&lt;span style="color:#a5d6ff">&amp;#34;ValidTo&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> t.UseHistoryTable(&lt;span style="color:#a5d6ff">&amp;#34;EmployeeHistory&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="идемпотентные-сценарии">Идемпотентные сценарии&lt;/h3>
&lt;p>Команда &lt;code>Script-Migration&lt;/code> (и ее эквивалент в CLI) по умолчанию создает более качественные идемпотентные сценарии с улучшенной обработкой крайних случаев, связанных с изменениями схемы, которые зависят от данных, существующих в определенных состояниях.&lt;/p>
&lt;h3 id="пакеты-миграции">Пакеты миграции&lt;/h3>
&lt;p>Пакеты миграции, которые упаковывают ваши миграции в автономный исполняемый файл для развертывания, более надежны в EF Core 9 благодаря улучшенным отчетам об ошибках и логике повторных попыток в случае временных сбоев.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet ef migrations bundle --self-contained -r linux-x64
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>В результате создается двоичный файл, который можно запустить в конвейере CI/CD без необходимости установки .NET SDK на целевой объект развертывания.&lt;/p>
&lt;h2 id="тесты-производительностивот-некоторые-грубые-тесты-из-моего-собственного-тестирования-они-взяты-из-проекта-насчитывающего-примерно-200-сущностей-работающего-на-sql-server-2022-измеренного-с-помощью-benchmarkdotnet-ваши-цифры-будут-различаться-но-относительные-улучшения-должны-быть-одинаковыми">Тесты производительностиВот некоторые грубые тесты из моего собственного тестирования. Они взяты из проекта, насчитывающего примерно 200 сущностей, работающего на SQL Server 2022, измеренного с помощью BenchmarkDotNet. Ваши цифры будут различаться, но относительные улучшения должны быть одинаковыми.&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Сценарий&lt;/th>
&lt;th>EF Core 8&lt;/th>
&lt;th>EF Core 9&lt;/th>
&lt;th>Улучшение&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Сборка модели (холодный старт)&lt;/td>
&lt;td>1850 мс&lt;/td>
&lt;td>320 мс&lt;/td>
&lt;td>~5,8 раз быстрее (скомпилировано)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Простой запрос (один объект по ПК)&lt;/td>
&lt;td>0,42 мс&lt;/td>
&lt;td>0,38 мс&lt;/td>
&lt;td>~10% быстрее&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Сложный запрос (объединение + агрегация)&lt;/td>
&lt;td>3,1 мс&lt;/td>
&lt;td>2,4 мс&lt;/td>
&lt;td>~ на 23% быстрее&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Массовое обновление (10 тыс. строк)&lt;/td>
&lt;td>145 мс&lt;/td>
&lt;td>118 мс&lt;/td>
&lt;td>~ на 19% быстрее&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Запрос столбца JSON&lt;/td>
&lt;td>2,8 мс&lt;/td>
&lt;td>1,9 мс&lt;/td>
&lt;td>~32% быстрее&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>СохранитьИзменения (100 объектов)&lt;/td>
&lt;td>48 мс&lt;/td>
&lt;td>41 мс&lt;/td>
&lt;td>~ на 15 % быстрее&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Улучшение скомпилированной модели является наиболее значительным, но устойчивые улучшения по всем направлениям суммируются, особенно в сценариях с высокой пропускной способностью, когда вы выполняете тысячи запросов в секунду.&lt;/p>
&lt;h2 id="обновление-с-ef-core-8">Обновление с EF Core 8&lt;/h2>
&lt;p>Если вы используете EF Core 8, путь обновления относительно гладкий. Вот контрольный список:&lt;/p>
&lt;p>&lt;strong>1. Обновите свои пакеты:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.EntityFrameworkCore&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;9.0.0&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.EntityFrameworkCore.SqlServer&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;9.0.0&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.EntityFrameworkCore.Tools&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;9.0.0&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>2. Проверьте наличие критических изменений.&lt;/strong> Список EF Core 9 относительно короткий по сравнению с некоторыми предыдущими выпусками. Наиболее примечательные из них:&lt;/p>
&lt;ul>
&lt;li>Некоторые ранее устаревшие API были удалены.&lt;/li>
&lt;li>Изменения в том, как преобразуются определенные запросы &lt;code>GroupBy&lt;/code> (теперь они выполняются на стороне сервера, что меняет поведение, если вы полагались на оценку клиента).&lt;/li>
&lt;li>Незначительные изменения в выходных данных миграции.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>3. Повторно сгенерируйте скомпилированные модели&lt;/strong>, если вы их используете. Формат изменился, поэтому старые скомпилированные модели не будут работать с EF Core 9.&lt;/p>
&lt;p>&lt;strong>4. Запустите набор тестов.&lt;/strong> Обратите особое внимание на запросы, которые ранее оценивались на клиенте — теперь они могут оцениваться на сервере, что обычно лучше, но может выявить различия в данных.&lt;/p>
&lt;p>&lt;strong>5. Проверьте свои запросы Cosmos DB&lt;/strong>, если вы используете этого поставщика. Улучшенные переводы означают, что некоторые запросы будут выполняться по-другому (обычно быстрее), но стоит убедиться, что результаты идентичны.&lt;/p>
&lt;p>Минимальный апгрейд для типичного проекта выглядит так:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.EntityFrameworkCore --version 9.0.0
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.0
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet build
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet test
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Если все компилируется и тесты проходят успешно, возможно, вы в хорошей форме. Если у вас возникнут проблемы, документация по критическим изменениям EF Core 9 содержит подробные инструкции по миграции для каждого изменения.&lt;/p>
&lt;h2 id="подведение-итогов">Подведение итогов&lt;/h2>
&lt;p>EF Core 9 — это не революционный выпуск, а эволюционный, и именно таким он и должен был быть. Одни только улучшения LINQ оправдывают обновление для большинства проектов, а такие функции, как усовершенствования столбцов JSON, сложные типы и поддержка &lt;code>HierarchyId&lt;/code> открывают шаблоны, которые ранее были неуклюжими или невозможными.&lt;/p>
&lt;p>Если бы мне пришлось выбрать три функции, которые оказали наибольшее влияние на мои проекты:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Параметризованные коллекции примитивов&lt;/strong> — поскольку эффективность кэша плана запроса имеет значение в масштабе&lt;/li>
&lt;li>&lt;strong>Усовершенствования столбцов JSON&lt;/strong> — потому что шаблон гибридного реляционного документа невероятно полезен.&lt;/li>
&lt;li>&lt;strong>Скомпилированные модели&lt;/strong> — поскольку время запуска напрямую влияет на продуктивность разработчиков и скорость развертывания.Команда EF Core добилась уверенного прогресса со времен EF Core 5, и версия 9 продолжает эту тенденцию. Если вы уже используете EF Core 8, обновление сопряжено с низким риском и высокой прибылью. Если вы используете что-то старое, сейчас лучшее время, чтобы совершить прыжок.&lt;/li>
&lt;/ol>
&lt;p>Удачного кодирования — и удачных запросов.&lt;/p></content:encoded><category>.NET</category><category>Entity Framework</category><category>Database</category></item><item><title>Начало работы с семантическим ядром: оркестровка ИИ в C#</title><link>https://emimontesdeoca.github.io/ru/posts/getting-started-semantic-kernel/</link><pubDate>Sun, 05 Oct 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/getting-started-semantic-kernel/</guid><description>Узнайте, как использовать семантическое ядро ​​Microsoft для создания приложений на основе искусственного интеллекта на C# — от плагинов и планировщиков до памяти и вызова функций.</description><content:encoded>&lt;p>Если вы создавали .NET-приложения и наблюдали за развитием ИИ-среды, вы, вероятно, задавались вопросом: &lt;em>какой лучший способ интегрировать большие языковые модели в мои проекты на C#, не превращая мою кодовую базу в спагетти?&lt;/em> Именно эту проблему решает семантическое ядро ​​Microsoft, и, потратив последний год на создание производственных приложений с его помощью, я могу вам сказать, что оно стало одним из самых важных инструментов в моем наборе инструментов разработчика.&lt;/p>
&lt;p>В этом посте я расскажу вам обо всем, что вам нужно для начала работы с семантическим ядром — от понимания основных концепций до создания реального помощника ИИ. Если вы только начинаете заниматься разработкой ИИ или ищете структурированный способ организации вызовов LLM в существующих приложениях .NET, это руководство поможет вам.&lt;/p>
&lt;h2 id="что-такое-семантическое-ядро">Что такое семантическое ядро?&lt;/h2>
&lt;p>Семантическое ядро ​​(SK) — это пакет SDK с открытым исходным кодом от Microsoft, который действует как &lt;strong>уровень оркестрации&lt;/strong> между кодом вашего приложения и большими языковыми моделями, такими как GPT-4o, Azure OpenAI или другими службами искусственного интеллекта. Думайте об этом как о легком промежуточном программном обеспечении, которое позволяет вам простым и компонуемым образом сочетать традиционный код C# с возможностями искусственного интеллекта.&lt;/p>
&lt;p>Но почему бы просто не вызвать API OpenAI напрямую? Вы абсолютно можете — и для простых случаев использования это нормально. Но в тот момент, когда вам нужно:&lt;/p>
&lt;ul>
&lt;li>Пусть ИИ решает &lt;strong>какие функции&lt;/strong> вызывать на основе пользовательского ввода.&lt;/li>
&lt;li>Объедините &lt;strong>несколько вызовов ИИ&lt;/strong> с традиционным кодом в конвейере.&lt;/li>
&lt;li>Добавьте &lt;strong>память и контекст&lt;/strong>, чтобы ИИ запоминал предыдущие взаимодействия.&lt;/li>
&lt;li>Создавайте &lt;strong>многоэтапных агентов&lt;/strong>, которые рассуждают посредством сложных задач.&lt;/li>
&lt;/ul>
&lt;p>&amp;hellip;вы изобретаете велосипед. Semantic Kernel предоставляет вам все это «из коробки» с первоклассной поддержкой .NET, интеграцией внедрения зависимостей и архитектурой плагинов, которая кажется естественной любому разработчику C#.&lt;/p>
&lt;p>Проект находится на GitHub в репозитории &lt;code>microsoft/semantic-kernel&lt;/code> и имеет SDK для C#, Python и Java. C# SDK является наиболее зрелым, и именно на нем мы здесь сосредоточимся.&lt;/p>
&lt;h2 id="основные-понятия">Основные понятия&lt;/h2>
&lt;p>Прежде чем писать какой-либо код, давайте разберемся со строительными блоками.&lt;/p>
&lt;h3 id="ядро">Ядро&lt;/h3>
&lt;p>&lt;code>Kernel&lt;/code> — это центральный объект в семантическом ядре. Это оркестратор — то, что связывает воедино ваши службы искусственного интеллекта, плагины и конфигурацию. Вы создаете его, регистрируете в нем свои службы и плагины, а затем используете его для выполнения подсказок или вызова функций. Если вы знакомы с внедрением зависимостей в ASP.NET Core, ядро ​​покажется вам очень знакомым — по сути, это сервисный контейнер со сверхспособностями искусственного интеллекта.&lt;/p>
&lt;h3 id="плагины-и-функции">Плагины и функции&lt;/h3>
&lt;p>&lt;strong>Плагин&lt;/strong> — это набор связанных &lt;strong>функций&lt;/strong>, которые может вызывать ядро. Функции бывают двух видов:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Функции подсказок&lt;/strong> — определяются как шаблоны естественного языка, которые отправляются в LLM.&lt;/li>
&lt;li>&lt;strong>Встроенные функции&lt;/strong> — обычные методы C#, украшенные атрибутами, которые ядро может обнаружить и вызвать.&lt;/li>
&lt;/ul>
&lt;p>Например, у вас может быть &lt;code>WeatherPlugin&lt;/code> со встроенной функцией &lt;code>GetCurrentWeather(string city)&lt;/code> и функцией подсказки, которая в удобной форме суммирует данные о погоде.### AI-коннекторы&lt;/p>
&lt;p>Коннекторы — это то, как семантическое ядро взаимодействует со службами ИИ. Наиболее распространенными из них являются:&lt;/p>
&lt;ul>
&lt;li>&lt;code>AzureOpenAIChatCompletion&lt;/code> — для службы Azure OpenAI.&lt;/li>
&lt;li>&lt;code>OpenAIChatCompletion&lt;/code> — напрямую для API OpenAI.&lt;/li>
&lt;li>Встраивание коннекторов для векторного поиска и памяти&lt;/li>
&lt;/ul>
&lt;p>Вы регистрируете их в ядре при запуске, и все остальное просто работает.&lt;/p>
&lt;h2 id="настройка-вашего-проекта">Настройка вашего проекта&lt;/h2>
&lt;p>Давайте запачкаем руки. Начните с создания нового консольного приложения:&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;p>Теперь добавьте пакеты NuGet семантического ядра:&lt;/p>
&lt;p>[[[ТОК_7]]]&lt;/p>
&lt;p>Если вы используете OpenAI напрямую вместо Azure OpenAI:&lt;/p>
&lt;p>[[[ТОК_8]]]&lt;/p>
&lt;p>Для поддержки памяти и встраивания (мы воспользуемся этим позже):&lt;/p>
&lt;p>[[[ТОК_9]]]&lt;/p>
&lt;p>Ваш &lt;code>.csproj&lt;/code> должен быть ориентирован на .NET 8 или более позднюю версию. Последние версии Semantic Kernel в полной мере используют современные функции .NET.&lt;/p>
&lt;h2 id="ваше-первое-ядро">Ваше первое ядро&lt;/h2>
&lt;p>Начнем с самого простого примера — создания ядра, подключения его к сервису ИИ и задания ему вопроса.&lt;/p>
&lt;p>[[[ТОК_11]]]&lt;/p>
&lt;p>Если вы используете OpenAI напрямую, поменяйте местами регистрацию сервиса:&lt;/p>
&lt;p>[[[ТОК_12]]]&lt;/p>
&lt;p>Вот и все. Запустите его, и вы получите краткое объяснение внедрения зависимостей. Но это лишь поверхностный взгляд.&lt;/p>
&lt;h3 id="использование-шаблонов-подсказок">Использование шаблонов подсказок&lt;/h3>
&lt;p>Шаблоны подсказок позволяют параметризовать подсказки переменными, используя синтаксис в стиле Handlebars:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> prompt = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a technical writer. Write a brief summary of {{&lt;span style="color:#f85149">$&lt;/span>topic}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> aimed at developers with {{&lt;span style="color:#f85149">$&lt;/span>experienceLevel}} experience.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Keep it under &lt;span style="color:#a5d6ff">200&lt;/span> words.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> function = kernel.CreateFunctionFromPrompt(prompt);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> arguments = &lt;span style="color:#ff7b72">new&lt;/span> KernelArguments
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [&amp;#34;topic&amp;#34;] = &lt;span style="color:#a5d6ff">&amp;#34;gRPC in .NET&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [&amp;#34;experienceLevel&amp;#34;] = &lt;span style="color:#a5d6ff">&amp;#34;intermediate&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> kernel.InvokeAsync(function, arguments);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(result);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Именно здесь семантическое ядро начинает проявлять себя — вы можете определять многократно используемые шаблоны подсказок, создавать их версии и объединять их в более крупные рабочие процессы.&lt;/p>
&lt;h2 id="плагины-и-встроенные-функции">Плагины и встроенные функции&lt;/h2>
&lt;p>Плагины — это место, где семантическое ядро устраняет разрыв между искусственным интеллектом и существующим кодом C#. Собственная функция — это обычный метод, который вы предоставляете ядру.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.ComponentModel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TimePlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;get_current_time&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Gets the current date and time in UTC&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetCurrentTime()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> DateTime.UtcNow.ToString(&lt;span style="color:#a5d6ff">&amp;#34;yyyy-MM-dd HH:mm:ss UTC&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;get_time_in_timezone&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Gets the current time in a specific timezone&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetTimeInTimezone(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;The IANA timezone identifier, e.g. &amp;#39;America/New_York&amp;#39;&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> timezone)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> tz = TimeZoneInfo.FindSystemTimeZoneById(timezone);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> time = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> time.ToString(&lt;span style="color:#a5d6ff">&amp;#34;yyyy-MM-dd HH:mm:ss&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Обратите внимание на атрибуты &lt;code>[KernelFunction]&lt;/code> и &lt;code>[Description]&lt;/code>. Это очень важно: описания — это то, что читает ИИ, чтобы понять, когда и как вызывать ваши функции. Хорошие описания определяют разницу между ИИ, который эффективно использует ваши инструменты, и тем, кто запутался.&lt;/p>
&lt;p>Зарегистрируйте плагин в своем ядре:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromType&amp;lt;TimePlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Вы также можете создавать более сложные плагины, внедряющие сервисы. Поскольку семантическое ядро интегрируется с &lt;code>Microsoft.Extensions.DependencyInjection&lt;/code>, ваши плагины могут получать зависимости конструктора, как и любой другой сервис:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">OrderPlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IOrderRepository _repository;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> OrderPlugin(IOrderRepository repository)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _repository = repository;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;get_order_status&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Retrieves the status of an order by its ID&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetOrderStatus(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;The order ID to look up&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> orderId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> order = &lt;span style="color:#ff7b72">await&lt;/span> _repository.GetByIdAsync(orderId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> order &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? &lt;span style="color:#a5d6ff">$&amp;#34;No order found with ID {orderId}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : &lt;span style="color:#a5d6ff">$&amp;#34;Order {orderId}: {order.Status}, placed on {order.CreatedAt:d}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="вызов-функций-и-автовызов">Вызов функций и автовызов&lt;/h2>
&lt;p>Здесь все становится действительно интересно. С помощью &lt;strong>вызова функции&lt;/strong> (также известного как вызов инструмента) вы позволяете модели ИИ решать, какую из ваших зарегистрированных функций вызывать в зависимости от контекста разговора. Модель не выполняет код — она возвращает структурированный запрос «Я хочу вызвать функцию X с этими аргументами», а фактический вызов обрабатывается ядром.&lt;/p>
&lt;p>Вот как включить автоматический вызов функций:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.OpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromType&amp;lt;TimePlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromType&amp;lt;WeatherPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Enable automatic function calling&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> settings = &lt;span style="color:#ff7b72">new&lt;/span> OpenAIPromptExecutionSettings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> kernel.InvokePromptAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;What time is it in Tokyo and what&amp;#39;s the weather like there?&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> KernelArguments(settings)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(result);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>С помощью &lt;code>FunctionChoiceBehavior.Auto()&lt;/code> ядро будет:&lt;/p>
&lt;ol>
&lt;li>Отправьте ИИ запрос вместе с описанием всех доступных функций.&lt;/li>
&lt;li>ИИ решает, что ему необходимо вызвать &lt;code>get_time_in_timezone&lt;/code> и &lt;code>get_weather&lt;/code>&lt;/li>
&lt;li>Ядро автоматически выполняет эти функции.&lt;/li>
&lt;li>Результаты отправляются обратно в ИИ.&lt;/li>
&lt;li>ИИ формирует ответ на естественном языке, используя результаты функции.Этот цикл может происходить несколько раз за один вызов — ИИ может последовательно вызывать несколько функций, чтобы собрать всю необходимую информацию. Вы также можете использовать &lt;code>FunctionChoiceBehavior.Required()&lt;/code>, чтобы заставить ИИ вызвать хотя бы одну функцию или предоставить определенный список функций, которые ему разрешено использовать.&lt;/li>
&lt;/ol>
&lt;h3 id="завершение-чата-с-историей">Завершение чата с историей&lt;/h3>
&lt;p>Для диалоговых приложений вы захотите использовать &lt;code>ChatCompletionService&lt;/code> напрямую с объектом &lt;code>ChatHistory&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> chatService = kernel.GetRequiredService&amp;lt;IChatCompletionService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>history.AddSystemMessage(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a helpful developer assistant. You have access to tools
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">for&lt;/span> checking the time and weather. Be concise and friendly.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">while&lt;/span> (&lt;span style="color:#79c0ff">true&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;You: &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> input = Console.ReadLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(input)) &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(input);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> chatService.GetChatMessageContentAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> executionSettings: settings,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kernel: kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddAssistantMessage(response.Content ?? &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Assistant: {response.Content}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это дает вам полностью интерактивный чат-бот, который сохраняет историю разговоров и может вызывать ваши плагины по мере необходимости.&lt;/p>
&lt;h2 id="память-и-встраивания">Память и встраивания&lt;/h2>
&lt;p>Одним из самых мощных шаблонов в приложениях ИИ является &lt;strong>Расширенная генерация данных (RAG)&lt;/strong> — предоставление ИИ доступа к вашим собственным данным путем их внедрения в векторное пространство и извлечения соответствующих фрагментов во время запроса.&lt;/p>
&lt;p>Семантическое ядро ​​предоставляет абстракции для работы с векторными хранилищами и внедрениями. Вот как настроить векторное хранилище в памяти для разработки:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Extensions.VectorData&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.AzureOpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Embeddings&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#pragma&lt;/span> warning disable SKEXP0010
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Create an embedding generation service&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAITextEmbeddingGeneration(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;text-embedding-ada-002&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> embeddingService = kernel.GetRequiredService&amp;lt;ITextEmbeddingGenerationService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Generate embeddings for your documents&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> documents = &lt;span style="color:#ff7b72">new&lt;/span>[]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Semantic Kernel is an open-source SDK for AI orchestration.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Azure OpenAI provides enterprise-grade AI models.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Plugins in SK allow you to expose C# methods to AI models.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> embeddings = &lt;span style="color:#ff7b72">await&lt;/span> embeddingService.GenerateEmbeddingsAsync(documents);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>В производственных сценариях эти внедрения следует хранить в выделенной векторной базе данных, например Azure AI Search, Qdrant или Pinecone. В семантическом ядре есть соединители для всего этого через абстракции &lt;code>Microsoft.Extensions.VectorData&lt;/code>.&lt;/p>
&lt;p>Типичный процесс RAG выглядит следующим образом:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Всасывание&lt;/strong>: разбивайте документы на части, создавайте вложения и сохраняйте их в векторной базе данных.&lt;/li>
&lt;li>&lt;strong>Извлечь&lt;/strong>: когда пользователь задает вопрос, встройте запрос и найдите наиболее похожие документы.&lt;/li>
&lt;li>&lt;strong>Создать&lt;/strong>: передать полученные документы в качестве контекста в LLM вместе с вопросом пользователя.&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[KernelFunction(&amp;#34;search_knowledge_base&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Description(&amp;#34;Searches the internal knowledge base for relevant information&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; SearchKnowledgeBase(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;The search query&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> query)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> queryEmbedding = &lt;span style="color:#ff7b72">await&lt;/span> _embeddingService.GenerateEmbeddingAsync(query);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> results = &lt;span style="color:#ff7b72">await&lt;/span> _vectorStore.SearchAsync(queryEmbedding, limit: &lt;span style="color:#a5d6ff">3&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>.Join(&lt;span style="color:#a5d6ff">&amp;#34;\n\n&amp;#34;&lt;/span>, results.Select(r =&amp;gt; r.Text));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Предоставляя ваш конвейер RAG как функцию ядра, ИИ может автоматически решать, когда ему нужно выполнить поиск в вашей базе знаний, сохраняя чистоту оркестрации и позволяя модели делать то, что она делает лучше всего.&lt;/p>
&lt;h2 id="планировщики-и-агенты">Планировщики и агенты&lt;/h2>
&lt;p>По мере усложнения ваших приложений ИИ вам понадобится ИИ для &lt;strong>планирования и выполнения многоэтапных задач&lt;/strong>. Именно здесь на помощь приходит структура агента Semantic Kernel.&lt;/p>
&lt;h3 id="основы-агент-завершения-чата">Основы: агент завершения чата&lt;/h3>
&lt;p>Самый простой тип агента включает в себя модель завершения чата с инструкциями и плагинами:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Agents&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#pragma&lt;/span> warning disable SKEXP0110
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> agent = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;DevAssistant&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a senior .NET developer assistant. Help users with code
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reviews, architecture decisions, and debugging. Always provide
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> code examples when relevant. Use your available tools to look up
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current information when needed.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Arguments = &lt;span style="color:#ff7b72">new&lt;/span> KernelArguments(settings)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>history.AddUserMessage(&lt;span style="color:#a5d6ff">&amp;#34;How should I structure a clean architecture project in .NET 8?&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> agent.InvokeAsync(history))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(message.Content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.Add(message);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="многоагентное-сотрудничество">Многоагентное сотрудничество&lt;/h3>
&lt;p>По-настоящему эффектно получается, когда у вас &lt;strong>несколько агентов, работающих вместе&lt;/strong>. Семантическое ядро поддерживает шаблоны групповых чатов, в которых сотрудничают агенты с разными специализациями:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#pragma&lt;/span> warning disable SKEXP0110
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> codeReviewer = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;CodeReviewer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You review C&lt;span style="color:#f85149">#&lt;/span> code &lt;span style="color:#ff7b72">for&lt;/span> bugs, performance issues, and best practices.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Be specific about what you find and suggest concrete fixes.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> securityAuditor = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;SecurityAuditor&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You focus exclusively &lt;span style="color:#ff7b72">on&lt;/span> security vulnerabilities &lt;span style="color:#ff7b72">in&lt;/span> code.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Look &lt;span style="color:#ff7b72">for&lt;/span> injection attacks, authentication issues, data exposure,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> and OWASP Top &lt;span style="color:#a5d6ff">10&lt;/span> violations.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> groupChat = &lt;span style="color:#ff7b72">new&lt;/span> AgentGroupChat(codeReviewer, securityAuditor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ExecutionSettings = &lt;span style="color:#ff7b72">new&lt;/span> AgentGroupChatSettings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TerminationStrategy = &lt;span style="color:#ff7b72">new&lt;/span> MaximumIterationTerminationStrategy(&lt;span style="color:#a5d6ff">4&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>groupChat.AddChatMessage(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ChatMessageContent(AuthorRole.User, &lt;span style="color:#a5d6ff">&amp;#34;Review this code: ...&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> groupChat.InvokeAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;[{message.AuthorName}]: {message.Content}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Этот шаблон невероятно полезен для сложных задач, где необходимо учитывать различные точки зрения или области знаний. Каждый агент работает со своей собственной системной подсказкой и может иметь свой собственный набор плагинов.&lt;/p>
&lt;h2 id="пример-из-реальной-жизни-создание-помощника-по-проектной-документации">Пример из реальной жизни: создание помощника по проектной документации&lt;/h2>
&lt;p>Давайте свяжем все вместе на практическом примере — ИИ-помощнике, который помогает разработчикам понимать кодовую базу, читая файлы и отвечая на вопросы.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.OpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.ComponentModel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Define our plugins&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">FileSystemPlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _rootPath;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> FileSystemPlugin(&lt;span style="color:#ff7b72">string&lt;/span> rootPath)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _rootPath = rootPath;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;read_file&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Reads the contents of a file from the project directory&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ReadFile(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Relative path to the file&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> path)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> fullPath = Path.Combine(_rootPath, path);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!File.Exists(fullPath))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;File not found: {path}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> content = &lt;span style="color:#ff7b72">await&lt;/span> File.ReadAllTextAsync(fullPath);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Truncate very large files&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (content.Length &amp;gt; &lt;span style="color:#a5d6ff">8000&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> content = content[..&lt;span style="color:#a5d6ff">8000&lt;/span>] + &lt;span style="color:#a5d6ff">&amp;#34;\n... [truncated]&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> content;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;list_files&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Lists files in a directory, optionally filtered by extension&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> ListFiles(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Relative directory path&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> directory,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;File extension filter like &amp;#39;.cs&amp;#39; or &amp;#39;.json&amp;#39;&amp;#34;)] &lt;span style="color:#ff7b72">string?&lt;/span> extension = &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> fullPath = Path.Combine(_rootPath, directory);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!Directory.Exists(fullPath))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;Directory not found: {directory}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> files = Directory.GetFiles(fullPath, &lt;span style="color:#a5d6ff">&amp;#34;*.*&amp;#34;&lt;/span>, SearchOption.AllDirectories)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(f =&amp;gt; Path.GetRelativePath(_rootPath, f))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(f =&amp;gt; extension &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span> || f.EndsWith(extension))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Take(&lt;span style="color:#a5d6ff">50&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>.Join(&lt;span style="color:#a5d6ff">&amp;#34;\n&amp;#34;&lt;/span>, files);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">DocumentationPlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;generate_summary&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Generates a structured markdown summary for documentation&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GenerateSummaryTemplate(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Name of the component&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> componentName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Brief description&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> description,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Key responsibilities as comma-separated values&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> responsibilities)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> items = responsibilities.Split(&lt;span style="color:#a5d6ff">&amp;#39;,&amp;#39;&lt;/span>, StringSplitOptions.TrimEntries);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bullets = &lt;span style="color:#ff7b72">string&lt;/span>.Join(&lt;span style="color:#a5d6ff">&amp;#34;\n&amp;#34;&lt;/span>, items.Select(r =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;- {r}&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> ## {componentName}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> {description}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> ### Responsibilities
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> {bullets}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> ---
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> *Generated documentation — review and expand as needed.*
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Wire it all up&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromObject(&lt;span style="color:#ff7b72">new&lt;/span> FileSystemPlugin(&lt;span style="color:#a5d6ff">&amp;#34;./src&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromType&amp;lt;DocumentationPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> chatService = kernel.GetRequiredService&amp;lt;IChatCompletionService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>history.AddSystemMessage(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a codebase documentation assistant. You help developers understand
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> projects &lt;span style="color:#ff7b72">by&lt;/span> reading source files and explaining architecture, patterns,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> and design decisions.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> When asked about code, use your tools to read the actual files rather
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> than guessing. Be specific and reference actual code when possible.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Generate documentation artifacts when asked.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> settings = &lt;span style="color:#ff7b72">new&lt;/span> OpenAIPromptExecutionSettings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;Documentation Assistant ready. Ask me about your codebase!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;Type &amp;#39;exit&amp;#39; to quit.\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">while&lt;/span> (&lt;span style="color:#79c0ff">true&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;You: &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> input = Console.ReadLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(input) || input.Equals(&lt;span style="color:#a5d6ff">&amp;#34;exit&amp;#34;&lt;/span>, StringComparison.OrdinalIgnoreCase))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(input);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> chatService.GetChatMessageContentAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> executionSettings: settings,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kernel: kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;\nAssistant: {response.Content}\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddAssistantMessage(response.Content ?? &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Этот помощник может:- &lt;strong>Список и чтение файлов&lt;/strong> из каталога вашего проекта.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Отвечайте на вопросы&lt;/strong> о кодовой базе, читая реальные исходные файлы.&lt;/li>
&lt;li>&lt;strong>Создать документацию&lt;/strong> в формате уценки.
– &lt;strong>Сохраняйте контекст разговора&lt;/strong>, чтобы дополнительные вопросы работали естественно.&lt;/li>
&lt;/ul>
&lt;p>ИИ автоматически решает, когда вызывать &lt;code>read_file&lt;/code>, &lt;code>list_files&lt;/code> или &lt;code>generate_summary&lt;/code> на основании вашего запроса. Спросите его: «Что делает OrderService?» и он прочитает файл, проанализирует его и объяснит. Попросите его «Создать документацию для модуля аутентификации», и он изучит файлы, поймет структуру и выдаст отформатированное резюме.&lt;/p>
&lt;h2 id="советы-по-производству">Советы по производству&lt;/h2>
&lt;p>Прежде чем вы выпустите свое приложение семантического ядра, я усвоил несколько вещей:&lt;/p>
&lt;p>&lt;strong>Правильно используйте внедрение зависимостей.&lt;/strong> В приложениях ASP.NET Core регистрируйте ядро и службы в контейнере DI, а не создавайте их в режиме реального времени:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddKernel()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: configuration[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:Endpoint&amp;#34;&lt;/span>]!,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: configuration[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:ApiKey&amp;#34;&lt;/span>]!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Plugins.AddFromType&amp;lt;TimePlugin&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddFromType&amp;lt;OrderPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Обрабатывайте ошибки корректно.&lt;/strong> Вызовы LLM могут завершаться с ошибкой, истекать по истечении времени или возвращать неожиданные результаты. Оберните свои вызовы в блоки try-catch и реализуйте политики повторных попыток с помощью Polly или встроенных функций устойчивости.&lt;/p>
&lt;p>&lt;strong>Отслеживайте использование токенов.&lt;/strong> Каждое приглашение, каждое описание функции и каждая часть истории чата потребляют токены. Используйте фильтры для регистрации и отслеживания использования:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>kernel.FunctionInvocationFilters.Add(&lt;span style="color:#ff7b72">new&lt;/span> LoggingFilter());
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Сохраняйте точность описаний функций.&lt;/strong> Расплывчатые описания приводят к неправильному вызову функций ИИ. Проверьте свои описания, задав вопрос: «Если бы я только прочитал описание, знал бы я точно, когда и как использовать эту функцию?»&lt;/p>
&lt;h2 id="заключение">Заключение&lt;/h2>
&lt;p>Семантическое ядро — одна из тех библиотек, которая фундаментально меняет ваше представление о создании приложений. Это не просто оболочка API — это среда оркестровки, которая позволяет объединять возможности ИИ с традиционным кодом таким образом, чтобы его можно было поддерживать, тестировать и готовить к использованию.&lt;/p>
&lt;p>Что мне больше всего в нем нравится, так это то, что он уважает экосистему .NET. Он использует уже знакомые вам шаблоны — внедрение зависимостей, атрибуты, async/await, интерфейсы — и распространяет их на мир ИИ. Вам не нужно изучать совершенно новую парадигму; вы просто добавляете ИИ в качестве еще одной возможности в свой набор инструментов.&lt;/p>
&lt;p>Если вы создаете .NET-приложения и еще не изучили семантическое ядро, сейчас самое время. SDK стабилен, сообщество активно, а шаблоны, которые он обеспечивает — от простой быстрой оркестровки до многоагентной совместной работы — становятся важными навыками для современных разработчиков.&lt;/p>
&lt;p>Начните с малого. Создайте ядро, зарегистрируйте плагин и наблюдайте, как ИИ вызывает ваш код. Как только это произойдет, вы начнете видеть возможности для добавления интеллекта повсюду в ваших приложениях.&lt;/p>
&lt;p>&lt;a href="https://learn.microsoft.com/semantic-kernel/overview/">Официальная документация&lt;/a> и &lt;a href="https://github.com/microsoft/semantic-kernel">репозиторий GitHub&lt;/a> — отличные ресурсы для продолжения вашего путешествия. Приятного строительства!&lt;/p></content:encoded><category>AI</category><category>.NET</category><category>Semantic Kernel</category><category>Azure</category></item><item><title>Наследование компонентов в Blazor</title><link>https://emimontesdeoca.github.io/ru/posts/blazor-inherit-components/</link><pubDate>Thu, 04 Sep 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/blazor-inherit-components/</guid><description>Расширяйте и повторно используйте компоненты Blazor посредством наследования с помощью ComponentBase и общих базовых классов.</description><content:encoded>&lt;p>Я создавал проект, в котором было несколько страниц форм, и каждая из них имела одинаковую логику состояния загрузки, одинаковую обработку ошибок и одинаковые всплывающие уведомления. Копирование всего этого казалось неправильным, поэтому я изучил наследование компонентов в Blazor. Оказывается, это довольно просто, поскольку компоненты Blazor — это всего лишь классы C#.&lt;/p>
&lt;h1 id="основы">Основы&lt;/h1>
&lt;p>Каждый компонент Blazor по умолчанию наследует от &lt;code>ComponentBase&lt;/code> . Вы можете создать свой собственный базовый класс, расширяющий &lt;code>ComponentBase&lt;/code>, а затем наследовать от него свои компоненты.&lt;/p>
&lt;p>Допустим, большинству наших страниц требуется состояние загрузки и обработка ошибок. Мы можем создать базовый класс:&lt;/p>
&lt;p>[[[ТОК_2]]]&lt;/p>
&lt;h1 id="использование-базового-класса">Использование базового класса&lt;/h1>
&lt;p>Теперь в любом компоненте страницы вместо наследования от &lt;code>ComponentBase&lt;/code> мы наследуем от нашего &lt;code>PageBase&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/users&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inherits PageBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@if (IsLoading)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;spinner&amp;#34;&lt;/span>&amp;gt;&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> (ErrorMessage &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;alert alert-danger&amp;#34;&lt;/span>&amp;gt;@ErrorMessage&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ul&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @foreach (&lt;span style="color:#ff7b72">var&lt;/span> user &lt;span style="color:#ff7b72">in&lt;/span> users)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;li&amp;gt;@user.Name&amp;lt;/li&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/ul&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;User&amp;gt; users = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> LoadDataAsync(&lt;span style="color:#ff7b72">async&lt;/span> () =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> users = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;List&amp;lt;User&amp;gt;&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/users&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Директива &lt;code>@inherits PageBase&lt;/code> является ключевой. Он сообщает Blazor использовать наш базовый класс вместо стандартного &lt;code>ComponentBase&lt;/code>. Теперь мы получаем &lt;code>IsLoading&lt;/code>, &lt;code>ErrorMessage&lt;/code> и &lt;code>LoadDataAsync()&lt;/code> бесплатно на каждой странице, которая наследуется от него.&lt;/p>
&lt;h1 id="внедрение-сервисов-в-базовый-класс">Внедрение сервисов в базовый класс&lt;/h1>
&lt;p>Вы также можете внедрить сервисы в базовый класс, чтобы они были доступны всем дочерним компонентам:&lt;/p>
&lt;p>[[[ТОК_11]]]&lt;/p>
&lt;p>Каждый компонент, который наследует от &lt;code>PageBase&lt;/code>, теперь имеет доступ к &lt;code>Navigation&lt;/code>, &lt;code>Toast&lt;/code>, &lt;code>NavigateBack()&lt;/code> и &lt;code>ShowSuccess()&lt;/code> без необходимости чего-либо внедрять.&lt;/p>
&lt;h1 id="углубляемся-в-общие-базовые-классы">Углубляемся в общие базовые классы&lt;/h1>
&lt;p>Вы даже можете создать общие базовые классы для распространенных шаблонов CRUD:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CrudPageBase&lt;/span>&amp;lt;T&amp;gt; : PageBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> List&amp;lt;T&amp;gt; Items { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> T? SelectedItem { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> Task&amp;lt;List&amp;lt;T&amp;gt;&amp;gt; FetchItems();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> Task DeleteItem(T item);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> LoadDataAsync(&lt;span style="color:#ff7b72">async&lt;/span> () =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Items = &lt;span style="color:#ff7b72">await&lt;/span> FetchItems();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnDelete(T item)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> LoadDataAsync(&lt;span style="color:#ff7b72">async&lt;/span> () =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> DeleteItem(item);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Items = &lt;span style="color:#ff7b72">await&lt;/span> FetchItems();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ShowSuccess(&lt;span style="color:#a5d6ff">&amp;#34;Item deleted.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Тогда ваша фактическая страница станет очень чистой:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inherits CrudPageBase&amp;lt;Product&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>* just the markup, all logic lives &lt;span style="color:#ff7b72">in&lt;/span> the &lt;span style="color:#ff7b72">base&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> *&lt;span style="color:#f85149">@&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> Task&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt; FetchItems()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> =&amp;gt; Http.GetFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/products&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> Task DeleteItem(Product item)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> =&amp;gt; Http.DeleteAsync(&lt;span style="color:#a5d6ff">$&amp;#34;api/products/{item.Id}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="когда-его-использовать-а-когда-нет">Когда его использовать, а когда нет&lt;/h1>
&lt;p>Наследование компонентов отлично подходит для общего поведения, такого как состояния загрузки, обработка ошибок, проверки подлинности или шаблоны CRUD. Но не переусердствуйте с глубокими иерархиями наследования — если вы обнаружите, что углубляетесь более чем на два уровня, вам, вероятно, лучше использовать композицию (например, подход &lt;code>LoadingComponent&lt;/code> из предыдущего поста).&lt;/p>
&lt;p>Обычно я придерживаюсь одного базового класса для каждого «типа» страницы: &lt;code>PageBase&lt;/code> для обычных страниц, &lt;code>FormPageBase&lt;/code> для форм, и это все.&lt;/p>
&lt;p>Надеюсь, вам понравился пост! Не стесняйтесь обращаться ко мне в любой социальной сети по адресу &lt;strong>@emimontesdeoca&lt;/strong>.&lt;/p>
&lt;h1 id="ресурсы">Ресурсы&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/#inheritance">Наследование компонентов ASP.NET Core Razor&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>C#</category></item><item><title>.NET Aspire: правильное создание облачных приложений</title><link>https://emimontesdeoca.github.io/ru/posts/dotnet-aspire-cloud-native/</link><pubDate>Wed, 20 Aug 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/dotnet-aspire-cloud-native/</guid><description>Подробное руководство по .NET Aspire — продуманному стеку для создания наблюдаемых, готовых к использованию распределенных приложений на .NET.</description><content:encoded>&lt;p>Если вы когда-либо создавали распределенное приложение на .NET, вы знаете суть. Вы запускаете веб-API, добавляете фонового работника, добавляете Redis для кэширования, PostgreSQL для сохранения, возможно, RabbitMQ для обмена сообщениями — и внезапно вы тратите больше времени на подключение инфраструктуры, чем на написание бизнес-логики. Строки подключения, разбросанные по файлам &lt;code>appsettings.json&lt;/code>, проверки работоспособности, которые вы забыли настроить, наблюдаемость, которая всегда является «проблемой следующего спринта».&lt;/p>
&lt;p>Я был там. Больше раз, чем мне хотелось бы признать.&lt;/p>
&lt;p>Именно для решения этой проблемы был создан &lt;strong>.NET Aspire&lt;/strong>. После нескольких месяцев эксплуатации его в производстве я хочу поделиться тем, что я узнал — хорошим, замечательным и ошибками.&lt;/p>
&lt;h2 id="что-такое-net-aspire">Что такое .NET Aspire?&lt;/h2>
&lt;p>.NET Aspire — это продуманный стек для создания наблюдаемых, готовых к использованию распределенных приложений с помощью .NET. Это не платформа в традиционном смысле — она не заменяет ASP.NET Core и не заставляет вас использовать новую модель программирования. Вместо этого он опирается на то, что вы уже знаете, и заполняет пробелы, которые всегда существовали при создании облачных приложений.&lt;/p>
&lt;p>По своей сути Aspire дает вам четыре вещи:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>AppHost&lt;/strong> — проект, определяющий всю топологию распределенного приложения. Какие сервисы существуют, от чего они зависят и как подключаются.&lt;/li>
&lt;li>&lt;strong>Настройки службы по умолчанию&lt;/strong> – общий проект, который настраивает сквозные задачи, такие как проверки работоспособности, политики устойчивости и OpenTelemetry, – один раз для всех ваших служб.&lt;/li>
&lt;li>&lt;strong>Компоненты&lt;/strong> — пакеты NuGet, обеспечивающие стандартизированную интеграцию с вспомогательными службами, такими как Redis, PostgreSQL, RabbitMQ, Azure Storage и другими.&lt;/li>
&lt;li>&lt;strong>Панель разработчика&lt;/strong> — пользовательский интерфейс, работающий в режиме реального времени, который отображает журналы, трассировки и метрики для всего распределенного приложения во время локальной разработки.&lt;/li>
&lt;/ol>
&lt;p>Философия проста: если каждое облачное приложение .NET нуждается в этих вещах, почему мы все каждый раз реализуем их с нуля?&lt;/p>
&lt;h2 id="настройка-вашего-первого-проекта-aspire">Настройка вашего первого проекта Aspire&lt;/h2>
&lt;p>Начать работу очень просто. Вам потребуется .NET 8 или более поздняя версия и установленная рабочая нагрузка Aspire:&lt;/p>
&lt;p>[[[ТОК_1]]]&lt;/p>
&lt;p>Теперь создайте новый стартовый проект Aspire:&lt;/p>
&lt;p>[[[ТОК_2]]]&lt;/p>
&lt;p>Это генерирует решение с четырьмя проектами:&lt;/p>
&lt;ul>
&lt;li>&lt;code>MyCloudApp.AppHost&lt;/code> — Оркестратор&lt;/li>
&lt;li>&lt;code>MyCloudApp.ServiceDefaults&lt;/code> — Общая конфигурация&lt;/li>
&lt;li>&lt;code>MyCloudApp.ApiService&lt;/code> — пример веб-API.&lt;/li>
&lt;li>&lt;code>MyCloudApp.Web&lt;/code> — интерфейс Blazor&lt;/li>
&lt;/ul>
&lt;p>Запустите AppHost, и вы сразу же увидите в браузере открытую панель управления Aspire, показывающую все ваши службы, их журналы и состояние их работоспособности. Нет файла Docker Compose. Никакого ручного управления портами. Это просто работает.&lt;/p>
&lt;p>[[[ТОК_7]]]&lt;/p>
&lt;p>Именно этот первый опыт меня и зацепил. Менее чем за минуту вы получите полностью оркестрованное распределенное приложение со встроенной возможностью наблюдения.&lt;/p>
&lt;h2 id="шаблон-apphost">Шаблон AppHost&lt;/h2>
&lt;p>AppHost — это место, где живет волшебство. Это небольшое консольное приложение, которое использует шаблон компоновщика для определения топологии вашего распределенного приложения — какие ресурсы существуют и как к ним подключаются службы.&lt;/p>
&lt;p>Вот как выглядит реалистичный AppHost:&lt;/p>
&lt;p>[[[ТОК_8]]]Прочтите этот код вслух. Он практически документирует себя. «API каталога ссылается на PostgreSQL, Redis и RabbitMQ. Интерфейс ссылается на API каталога и API заказа». Это ваша архитектура, выраженная в коде.&lt;/p>
&lt;p>Несколько вещей, которые стоит отметить:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>WithReference&lt;/code>&lt;/strong> выполняет тяжелую работу. Он автоматически вводит строки подключения и URL-адреса служб в потребляющий проект через переменные среды и конфигурацию. Вашим службам не нужно знать, &lt;em>где&lt;/em> работает Redis — Aspire справится с этим.&lt;/li>
&lt;li>&lt;strong>&lt;code>WithPgAdmin()&lt;/code>&lt;/strong> и &lt;strong>&lt;code>WithManagementPlugin()&lt;/code>&lt;/strong> расширяют пользовательские интерфейсы администратора для PostgreSQL и RabbitMQ наряду с реальными службами. Во время разработки они неоценимы.&lt;/li>
&lt;li>&lt;strong>&lt;code>WithExternalHttpEndpoints()&lt;/code>&lt;/strong> отмечает службу как доступную извне, что имеет значение во время развертывания.&lt;/li>
&lt;/ul>
&lt;h3 id="конфигурация-ресурса">Конфигурация ресурса&lt;/h3>
&lt;p>Вы можете настроить ресурсы с детальным контролем:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> postgres = builder.AddPostgres(&lt;span style="color:#a5d6ff">&amp;#34;postgres&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithEnvironment(&lt;span style="color:#a5d6ff">&amp;#34;POSTGRES_MAX_CONNECTIONS&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;200&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithDataVolume(&lt;span style="color:#a5d6ff">&amp;#34;postgres-data&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddDatabase(&lt;span style="color:#a5d6ff">&amp;#34;catalogdb&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> redis = builder.AddRedis(&lt;span style="color:#a5d6ff">&amp;#34;cache&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithDataVolume(&lt;span style="color:#a5d6ff">&amp;#34;redis-data&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Объемы данных гарантируют, что ваши локальные данные разработки выдержат перезапуск контейнера. Маленькие детали, большое улучшение качества жизни.&lt;/p>
&lt;h2 id="сервисные-настройки-по-умолчанию-невоспетый-герой">Сервисные настройки по умолчанию: Невоспетый герой&lt;/h2>
&lt;p>Проект &lt;code>ServiceDefaults&lt;/code> — самая недооцененная часть Aspire. Это общая библиотека, на которую ссылается каждая служба в вашем решении, и она настраивает все сквозные проблемы, которые вы в противном случае забыли бы или реализовали непоследовательно.&lt;/p>
&lt;p>Вот как выглядит типичный &lt;code>Extensions.cs&lt;/code> в ServiceDefaults:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Extensions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IHostApplicationBuilder AddServiceDefaults(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> IHostApplicationBuilder builder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.ConfigureOpenTelemetry();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.AddDefaultHealthChecks();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Services.AddServiceDiscovery();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Services.ConfigureHttpClientDefaults(http =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> http.AddStandardResilienceHandler();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> http.AddServiceDiscovery();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> builder;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IHostApplicationBuilder ConfigureOpenTelemetry(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> IHostApplicationBuilder builder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Logging.AddOpenTelemetry(logging =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logging.IncludeFormattedMessage = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logging.IncludeScopes = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Services.AddOpenTelemetry()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithMetrics(metrics =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> metrics.AddAspNetCoreInstrumentation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddHttpClientInstrumentation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddRuntimeInstrumentation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithTracing(tracing =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tracing.AddAspNetCoreInstrumentation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddHttpClientInstrumentation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddGrpcClientInstrumentation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.AddOpenTelemetryExporters();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> builder;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IHostApplicationBuilder AddDefaultHealthChecks(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> IHostApplicationBuilder builder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Services.AddHealthChecks()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddCheck(&lt;span style="color:#a5d6ff">&amp;#34;self&amp;#34;&lt;/span>, () =&amp;gt; HealthCheckResult.Healthy(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [&amp;#34;live&amp;#34;]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> builder;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Затем в &lt;code>Program.cs&lt;/code> каждой службы одна строка делает все это:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddServiceDefaults();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Your service-specific configuration...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapDefaultEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Этот единственный вызов &lt;code>AddServiceDefaults()&lt;/code> дает вам:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>OpenTelemetry&lt;/strong> со структурированным журналированием, метриками и распределенной трассировкой.&lt;/li>
&lt;li>&lt;strong>Проверки работоспособности&lt;/strong> с указанием конечных точек работоспособности и готовности.&lt;/li>
&lt;li>&lt;strong>Обнаружение служб&lt;/strong>, чтобы службы могли находить друг друга по имени.&lt;/li>
&lt;li>&lt;strong>Политики устойчивости&lt;/strong> для всех исходящих HTTP-вызовов (повторные попытки, автоматические выключатели, тайм-ауты).&lt;/li>
&lt;/ul>
&lt;p>Это то, что отличает демоверсию «работает на моей машине» от готовой к использованию системы. И Aspire делает это значением по умолчанию, а не второстепенным вопросом.&lt;/p>
&lt;h2 id="компоненты-aspire">Компоненты Aspire&lt;/h2>
&lt;p>Компоненты Aspire — это пакеты NuGet, которые стандартизируют способ подключения ваших сервисов к поддерживающей инфраструктуре. Это больше, чем просто клиентские библиотеки — они включают в себя проверку работоспособности, ведение журналов, трассировку и настраиваемую устойчивость.&lt;/p>
&lt;h3 id="редис">Редис&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddServiceDefaults();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddRedisDistributedCache(&lt;span style="color:#a5d6ff">&amp;#34;cache&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Вот и все. Строка подключения поступает от AppHost через &lt;code>WithReference&lt;/code>. Компонент регистрирует &lt;code>IDistributedCache&lt;/code>, поддерживаемый Redis, с уже подключенными проверками работоспособности и инструментами OpenTelemetry.&lt;/p>
&lt;p>Вы также можете использовать Redis для кэширования вывода:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.AddRedisOutputCache(&lt;span style="color:#a5d6ff">&amp;#34;cache&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="postgresql-с-entity-framework-core">PostgreSQL с Entity Framework Core&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.AddNpgsqlDbContext&amp;lt;CatalogDbContext&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;catalogdb&amp;#34;&lt;/span>, settings =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> settings.DisableRetry = &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>При этом ваш &lt;code>DbContext&lt;/code> будет подключен к базе данных &lt;code>catalogdb&lt;/code>, определенной в AppHost. Он включает в себя пул соединений, проверки работоспособности и политики повторных попыток.&lt;/p>
&lt;h3 id="кроликmq">КроликMQ&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.AddRabbitMQClient(&lt;span style="color:#a5d6ff">&amp;#34;messaging&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Регистрирует &lt;code>IConnection&lt;/code> из клиентской библиотеки RabbitMQ, полностью настроенный и проверенный на работоспособность.&lt;/p>
&lt;h3 id="интеграции-azure">Интеграции Azure&lt;/h3>
&lt;p>Aspire также имеет первоклассные компоненты для сервисов Azure:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Azure Blob Storage&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureBlobClient(&lt;span style="color:#a5d6ff">&amp;#34;blobs&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Azure Service Bus&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureServiceBusClient(&lt;span style="color:#a5d6ff">&amp;#34;servicebus&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Azure Key Vault&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureKeyVaultClient(&lt;span style="color:#a5d6ff">&amp;#34;secrets&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```Шаблон&lt;/span> &lt;span style="color:#f85149">всегда&lt;/span> &lt;span style="color:#f85149">один&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">тот&lt;/span> &lt;span style="color:#f85149">же&lt;/span>: &lt;span style="color:#f85149">одна&lt;/span> &lt;span style="color:#f85149">строка&lt;/span> &lt;span style="color:#f85149">в&lt;/span> AppHost &lt;span style="color:#f85149">для&lt;/span> &lt;span style="color:#f85149">определения&lt;/span> &lt;span style="color:#f85149">ресурса&lt;/span>, &lt;span style="color:#f85149">одна&lt;/span> &lt;span style="color:#f85149">строка&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">службе&lt;/span>-&lt;span style="color:#f85149">потребителе&lt;/span> &lt;span style="color:#f85149">для&lt;/span> &lt;span style="color:#f85149">его&lt;/span> &lt;span style="color:#f85149">использования&lt;/span>. &lt;span style="color:#f85149">Детали&lt;/span> &lt;span style="color:#f85149">подключения&lt;/span> &lt;span style="color:#f85149">передаются&lt;/span> &lt;span style="color:#f85149">автоматически&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> &lt;span style="color:#f85149">Панель&lt;/span> &lt;span style="color:#f85149">разработчика&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Панель&lt;/span> &lt;span style="color:#f85149">управления&lt;/span> Aspire &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">одна&lt;/span> &lt;span style="color:#f85149">из&lt;/span> &lt;span style="color:#f85149">тех&lt;/span> &lt;span style="color:#f85149">функций&lt;/span>, &lt;span style="color:#f85149">которая&lt;/span> &lt;span style="color:#f85149">кажется&lt;/span> &lt;span style="color:#f85149">приятной&lt;/span>, &lt;span style="color:#f85149">пока&lt;/span> &lt;span style="color:#f85149">вы&lt;/span> &lt;span style="color:#f85149">ее&lt;/span> &lt;span style="color:#f85149">не&lt;/span> &lt;span style="color:#f85149">используете&lt;/span> &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">тогда&lt;/span> &lt;span style="color:#f85149">вы&lt;/span> &lt;span style="color:#f85149">не&lt;/span> &lt;span style="color:#f85149">сможете&lt;/span> &lt;span style="color:#f85149">представить&lt;/span> &lt;span style="color:#f85149">себе&lt;/span> &lt;span style="color:#f85149">работу&lt;/span> &lt;span style="color:#f85149">без&lt;/span> &lt;span style="color:#f85149">нее&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Когда&lt;/span> &lt;span style="color:#f85149">вы&lt;/span> &lt;span style="color:#f85149">запускаете&lt;/span> AppHost &lt;span style="color:#f85149">локально&lt;/span>, &lt;span style="color:#f85149">запускается&lt;/span> &lt;span style="color:#f85149">панель&lt;/span> &lt;span style="color:#f85149">мониторинга&lt;/span>, &lt;span style="color:#f85149">которая&lt;/span> &lt;span style="color:#f85149">предоставляет&lt;/span> &lt;span style="color:#f85149">вам&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">Обзор&lt;/span> &lt;span style="color:#f85149">ресурсов&lt;/span>** &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">Краткий&lt;/span> &lt;span style="color:#f85149">обзор&lt;/span> &lt;span style="color:#f85149">всех&lt;/span> &lt;span style="color:#f85149">ваших&lt;/span> &lt;span style="color:#f85149">услуг&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">инфраструктуры&lt;/span> &lt;span style="color:#f85149">с&lt;/span> &lt;span style="color:#f85149">индикаторами&lt;/span> &lt;span style="color:#f85149">состояния&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">Структурированные&lt;/span> &lt;span style="color:#f85149">журналы&lt;/span>** &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">потоковая&lt;/span> &lt;span style="color:#f85149">передача&lt;/span> &lt;span style="color:#f85149">журналов&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">реальном&lt;/span> &lt;span style="color:#f85149">времени&lt;/span> &lt;span style="color:#f85149">из&lt;/span> &lt;span style="color:#f85149">каждой&lt;/span> &lt;span style="color:#f85149">службы&lt;/span> &lt;span style="color:#f85149">с&lt;/span> &lt;span style="color:#f85149">возможностью&lt;/span> &lt;span style="color:#f85149">фильтрации&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">поиска&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">Распределенные&lt;/span> &lt;span style="color:#f85149">трассировки&lt;/span>** &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">сквозные&lt;/span> &lt;span style="color:#f85149">трассировки&lt;/span> &lt;span style="color:#f85149">запросов&lt;/span>, &lt;span style="color:#f85149">охватывающие&lt;/span> &lt;span style="color:#f85149">несколько&lt;/span> &lt;span style="color:#f85149">служб&lt;/span>, &lt;span style="color:#f85149">визуализированные&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">виде&lt;/span> &lt;span style="color:#f85149">флейм&lt;/span>-&lt;span style="color:#f85149">диаграмм&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">Метрики&lt;/span>** &lt;span style="color:#f85149">–&lt;/span> &lt;span style="color:#f85149">частота&lt;/span> HTTP-&lt;span style="color:#f85149">запросов&lt;/span>, &lt;span style="color:#f85149">частота&lt;/span> &lt;span style="color:#f85149">ошибок&lt;/span>, &lt;span style="color:#f85149">задержки&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">пользовательские&lt;/span> &lt;span style="color:#f85149">метрики&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">режиме&lt;/span> &lt;span style="color:#f85149">реального&lt;/span> &lt;span style="color:#f85149">времени&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">Журналы&lt;/span> &lt;span style="color:#f85149">консоли&lt;/span>** &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">необработанные&lt;/span> &lt;span style="color:#f85149">данные&lt;/span> stdout/stderr &lt;span style="color:#f85149">из&lt;/span> &lt;span style="color:#f85149">каждого&lt;/span> &lt;span style="color:#f85149">контейнера&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">проекта&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Распределенная&lt;/span> &lt;span style="color:#f85149">трассировка&lt;/span> &lt;span style="color:#f85149">особенно&lt;/span> &lt;span style="color:#f85149">ценна&lt;/span>. &lt;span style="color:#f85149">Когда&lt;/span> &lt;span style="color:#f85149">запрос&lt;/span> &lt;span style="color:#f85149">попадает&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">ваш&lt;/span> &lt;span style="color:#f85149">интерфейс&lt;/span>, &lt;span style="color:#f85149">проходит&lt;/span> &lt;span style="color:#f85149">через&lt;/span> API &lt;span style="color:#f85149">каталога&lt;/span>, &lt;span style="color:#f85149">касается&lt;/span> Redis &lt;span style="color:#f85149">и&lt;/span> PostgreSQL &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">вы&lt;/span> &lt;span style="color:#f85149">видите&lt;/span> &lt;span style="color:#f85149">всю&lt;/span> &lt;span style="color:#f85149">цепочку&lt;/span> &lt;span style="color:#f85149">с&lt;/span> &lt;span style="color:#f85149">указанием&lt;/span> &lt;span style="color:#f85149">времени&lt;/span> &lt;span style="color:#f85149">для&lt;/span> &lt;span style="color:#f85149">каждого&lt;/span> &lt;span style="color:#f85149">перехода&lt;/span>. &lt;span style="color:#f85149">Больше&lt;/span> &lt;span style="color:#f85149">не&lt;/span> &lt;span style="color:#f85149">нужно&lt;/span> &lt;span style="color:#f85149">гадать&lt;/span>, &lt;span style="color:#f85149">где&lt;/span> &lt;span style="color:#f85149">находится&lt;/span> &lt;span style="color:#f85149">узкое&lt;/span> &lt;span style="color:#f85149">место&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Я&lt;/span> &lt;span style="color:#f85149">считаю&lt;/span>, &lt;span style="color:#f85149">что&lt;/span> &lt;span style="color:#f85149">панель&lt;/span> &lt;span style="color:#f85149">мониторинга&lt;/span> &lt;span style="color:#f85149">наиболее&lt;/span> &lt;span style="color:#f85149">полезна&lt;/span> &lt;span style="color:#f85149">во&lt;/span> &lt;span style="color:#f85149">время&lt;/span> &lt;span style="color:#f85149">отладки&lt;/span>. &lt;span style="color:#f85149">Вместо&lt;/span> &lt;span style="color:#f85149">отслеживания&lt;/span> &lt;span style="color:#f85149">нескольких&lt;/span> &lt;span style="color:#f85149">окон&lt;/span> &lt;span style="color:#f85149">терминала&lt;/span> &lt;span style="color:#f85149">или&lt;/span> &lt;span style="color:#f85149">перехода&lt;/span> &lt;span style="color:#f85149">между&lt;/span> &lt;span style="color:#f85149">файлами&lt;/span> &lt;span style="color:#f85149">журналов&lt;/span> &lt;span style="color:#f85149">все&lt;/span> &lt;span style="color:#f85149">находится&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">одном&lt;/span> &lt;span style="color:#f85149">месте&lt;/span> &lt;span style="color:#f85149">с&lt;/span> &lt;span style="color:#f85149">идентификаторами&lt;/span> &lt;span style="color:#f85149">корреляции&lt;/span>, &lt;span style="color:#f85149">связывающими&lt;/span> &lt;span style="color:#f85149">связанные&lt;/span> &lt;span style="color:#f85149">события&lt;/span> &lt;span style="color:#f85149">между&lt;/span> &lt;span style="color:#f85149">службами&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Панель&lt;/span> &lt;span style="color:#f85149">мониторинга&lt;/span> &lt;span style="color:#f85149">также&lt;/span> &lt;span style="color:#f85149">доступна&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">виде&lt;/span> &lt;span style="color:#f85149">автономного&lt;/span> &lt;span style="color:#f85149">контейнера&lt;/span>, &lt;span style="color:#f85149">что&lt;/span> &lt;span style="color:#f85149">означает&lt;/span>, &lt;span style="color:#f85149">что&lt;/span> &lt;span style="color:#f85149">вы&lt;/span> &lt;span style="color:#f85149">можете&lt;/span> &lt;span style="color:#f85149">использовать&lt;/span> &lt;span style="color:#f85149">ее&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">промежуточных&lt;/span> &lt;span style="color:#f85149">средах&lt;/span> &lt;span style="color:#f85149">или&lt;/span> &lt;span style="color:#f85149">конвейерах&lt;/span> CI:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>bash
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker run --rm -p &lt;span style="color:#a5d6ff">18888&lt;/span>:&lt;span style="color:#a5d6ff">18888&lt;/span> \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mcr.microsoft.com/dotnet/aspire-dashboard:latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="развертывание">Развертывание&lt;/h2>
&lt;p>Aspire помогает во время разработки, но как насчет производства? Здесь все становится практичным.&lt;/p>
&lt;h3 id="приложения-контейнеры-azure">Приложения-контейнеры Azure&lt;/h3>
&lt;p>Самый простой путь развертывания — это приложения-контейнеры Azure (ACA), которые имеют первоклассную поддержку Aspire. Вы можете выполнить развертывание напрямую с помощью интерфейса командной строки разработчика Azure:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>azd init
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>azd up
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Команда &lt;code>azd init&lt;/code> обнаруживает ваш Aspire AppHost и генерирует необходимую инфраструктуру в виде кода. &lt;code>azd up&lt;/code> подготавливает все — реестр контейнеров, приложения-контейнеры, базы данных, экземпляры Redis — на основе вашей топологии AppHost.&lt;/p>
&lt;p>Ваш AppHost по сути становится манифестом вашего развертывания. Тот же код, который определяет, что «catalog-api зависит от PostgreSQL и Redis», управляет подготовкой инфраструктуры.&lt;/p>
&lt;h3 id="кубернетес">Кубернетес&lt;/h3>
&lt;p>Для развертываний Kubernetes Aspire не создает манифесты напрямую, но ваша топология AppHost четко сопоставляется с ресурсами Kubernetes. Инструмент сообщества &lt;code>aspirate&lt;/code> (Aspir8) может создавать диаграммы Helm или манифесты Kubernetes из вашего AppHost:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet tool install -g aspirate
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>aspirate generate
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>aspirate apply
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="рекомендации-по-развертыванию">Рекомендации по развертыванию&lt;/h3>
&lt;p>Несколько вещей, которые следует иметь в виду:- &lt;strong>Строки подключения меняются в зависимости от среды.&lt;/strong> Локально Aspire запускает контейнеры и автоматически вводит строки подключения. В рабочей среде вы укажете на управляемые службы. Aspire использует стандартную конфигурацию .NET, поэтому переменные среды и Azure Key Vault работают должным образом.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>AppHost не работает в рабочей среде.&lt;/strong> Это инструмент оркестрации разработки и развертывания. В рабочей среде ваши службы работают независимо и настраиваются с помощью переменных среды и оркестраторов.
— &lt;strong>Ресурсы инфраструктуры становятся управляемыми службами.&lt;/strong> Ваш локальный контейнер PostgreSQL становится базой данных Azure для PostgreSQL. Ваш локальный контейнер Redis становится кэшем Azure для Redis. Потребляющий код не меняется.&lt;/li>
&lt;/ul>
&lt;h2 id="реальные-советы">Реальные советы&lt;/h2>
&lt;p>После некоторого запуска Aspire в производство, вот уроки, которые сэкономили нам время:&lt;/p>
&lt;h3 id="1-используйте-специальные-проверки-жизненного-цикла-ресурсов">1. Используйте специальные проверки жизненного цикла ресурсов&lt;/h3>
&lt;p>Не полагайтесь только на запуск контейнера, чтобы определить, готов ли ресурс. PostgreSQL может принимать TCP-соединения еще до того, как будет готов обслуживать запросы.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> postgres = builder.AddPostgres(&lt;span style="color:#a5d6ff">&amp;#34;postgres&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddDatabase(&lt;span style="color:#a5d6ff">&amp;#34;catalogdb&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithHealthCheck();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Aspire может проверять работоспособность ресурсов и удерживать зависимые службы до тех пор, пока они не будут фактически готовы.&lt;/p>
&lt;h3 id="2-извлечение-общих-шаблонов-в-расширения">2. Извлечение общих шаблонов в расширения&lt;/h3>
&lt;p>Если несколько служб имеют схожие конфигурации, создайте методы расширения:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">AppHostExtensions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IResourceBuilder&amp;lt;ProjectResource&amp;gt; AddWorkerService(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> IDistributedApplicationBuilder builder,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IResourceBuilder&amp;lt;IResourceWithConnectionString&amp;gt; db,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IResourceBuilder&amp;lt;IResourceWithConnectionString&amp;gt; messaging)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> builder.AddProject&amp;lt;Projects.WorkerService&amp;gt;(name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(messaging)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReplicas(&lt;span style="color:#a5d6ff">3&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3-использование-withreplicas-для-нагрузочного-тестирования">3. Использование WithReplicas для нагрузочного тестирования&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> catalogApi = builder.AddProject&amp;lt;Projects.CatalogApi&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;catalog-api&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(postgres)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(redis)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReplicas(&lt;span style="color:#a5d6ff">5&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>WithReplicas&lt;/code> локально запускает несколько экземпляров службы. Это отлично подходит для тестирования балансировки нагрузки, ошибок параллелизма и поведения распределенного кэширования без развертывания в кластере.&lt;/p>
&lt;h3 id="4-используйте-параметры-для-чувствительных-значений">4. Используйте параметры для чувствительных значений&lt;/h3>
&lt;p>Не указывайте учетные данные жестко, даже для локальной разработки:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> dbPassword = builder.AddParameter(&lt;span style="color:#a5d6ff">&amp;#34;db-password&amp;#34;&lt;/span>, secret: &lt;span style="color:#79c0ff">true&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> postgres = builder.AddPostgres(&lt;span style="color:#a5d6ff">&amp;#34;postgres&amp;#34;&lt;/span>, password: dbPassword)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddDatabase(&lt;span style="color:#a5d6ff">&amp;#34;catalogdb&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>При локальном запуске Aspire запросит значение или прочтет его из секретов пользователя. В CI/CD это происходит из переменных среды.&lt;/p>
&lt;h3 id="5-интеграционные-тесты-становятся-тривиальными">5. Интеграционные тесты становятся тривиальными&lt;/h3>
&lt;p>Aspire включает пакет тестирования, который значительно упрощает интеграционное тестирование распределенных приложений:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[Fact]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task CatalogApiReturnsProducts()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> appHost = &lt;span style="color:#ff7b72">await&lt;/span> DistributedApplicationTestingBuilder
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .CreateAsync&amp;lt;Projects.MyCloudApp_AppHost&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> app = &lt;span style="color:#ff7b72">await&lt;/span> appHost.BuildAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> app.StartAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> httpClient = app.CreateHttpClient(&lt;span style="color:#a5d6ff">&amp;#34;catalog-api&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> httpClient.GetAsync(&lt;span style="color:#a5d6ff">&amp;#34;/api/products&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response.EnsureSuccessStatusCode();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> products = &lt;span style="color:#ff7b72">await&lt;/span> response.Content
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ReadFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.NotEmpty(products);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это раскручивает ваше &lt;em>все&lt;/em> распределенное приложение — включая базы данных и брокеры сообщений — запускает ваш тест и разрушает его. Реальные интеграционные тесты с реальной инфраструктурой в вашем конвейере CI. Никаких издевательств.&lt;/p>
&lt;h3 id="6-мониторинг-использования-ресурсов-локально">6. Мониторинг использования ресурсов локально&lt;/h3>
&lt;p>При локальном запуске нескольких контейнеров следите за потреблением ресурсов. Экземпляр PostgreSQL, Redis и RabbitMQ с пользовательским интерфейсом управления может легко занимать 2–3 ГБ ОЗУ. Если вы используете машину с ограниченными возможностями, рассмотрите возможность использования более легких конфигураций ресурсов:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> redis = builder.AddRedis(&lt;span style="color:#a5d6ff">&amp;#34;cache&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithContainerRuntimeArgs(&lt;span style="color:#a5d6ff">&amp;#34;--memory=256m&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="заключение">Заключение&lt;/h2>
&lt;p>.NET Aspire фундаментально изменил мой подход к созданию распределенных приложений. Не потому, что он вводит революционные концепции — проверки работоспособности, OpenTelemetry и оркестровка контейнеров не новы. А потому, что это делает их &lt;em>по умолчанию&lt;/em>. Он принимает сотни небольших решений, которые вам обычно приходится принимать, реализует их с разумными значениями по умолчанию и позволяет вам переопределить их, когда это необходимо.На мой взгляд, шаблон AppHost — это самая большая победа. Если топология распределенного приложения выражена в виде кода, а не разбросана по файлам Docker Compose, манифестам Kubernetes и документам README, это делает систему понятной. Новые члены команды могут открыть &lt;code>Program.cs&lt;/code> в AppHost и понять всю архитектуру за считанные минуты.&lt;/p>
&lt;p>Если вы создаете распределенные приложения с помощью .NET, присмотритесь к Aspire серьезно. Начните с шаблона &lt;code>aspire-starter&lt;/code>, изучите панель мониторинга и постепенно внедряйте компоненты по мере необходимости. Вам не обязательно идти ва-банк в первый же день — Aspire по своей сути аддитивен.&lt;/p>
&lt;p>Дни, когда вы тратили свой первый спринт на подключение шаблона инфраструктуры, прошли. Позвольте Aspire позаботиться о сантехнике, чтобы вы могли сосредоточиться на том, что действительно важно: на вашем приложении.&lt;/p></content:encoded><category>.NET</category><category>Azure</category><category>Cloud</category><category>Docker</category></item><item><title>Аутентификация и авторизация в Blazor: практическое руководство</title><link>https://emimontesdeoca.github.io/ru/posts/blazor-authentication-authorization/</link><pubDate>Sat, 12 Jul 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/blazor-authentication-authorization/</guid><description>Практическое руководство по реализации аутентификации и авторизации в приложениях Blazor — от ASP.NET Identity до OAuth, доступа на основе ролей и компонентов защиты.</description><content:encoded>&lt;p>Если вы работали с ASP.NET MVC или Razor Pages, у вас, вероятно, есть мысленная модель того, как работает аутентификация: промежуточное программное обеспечение перехватывает запрос, проверяет файл cookie или токен, заполняет &lt;code>HttpContext.User&lt;/code>, и вы готовы к гонкам. Blazor меняет эту ментальную модель тонкими, но важными способами — и если вы не поймете эти различия на ранней стадии, вам придется отлаживать проблемы аутентификации, которые кажутся невозможными.&lt;/p>
&lt;p>В этом посте я хочу рассказать, как на самом деле работают аутентификация и авторизация в Blazor, охватывая модели хостинга Server и WebAssembly. Мы пройдем от основ до пользовательских поставщиков, внешнего OAuth и ловушек, которые, как я видел, сбивают с толку даже опытных разработчиков .NET.&lt;/p>
&lt;h2 id="почему-аутентификация-в-blazor-отличается">Почему аутентификация в Blazor отличается&lt;/h2>
&lt;p>В традиционном ASP.NET каждое взаимодействие с пользователем представляет собой HTTP-запрос. Сервер проверяет учетные данные, устанавливает файл cookie, и каждый последующий запрос передает этот файл cookie. Конвейер аутентификации линеен и предсказуем.&lt;/p>
&lt;p>Blazor Server работает через постоянное соединение SignalR. После того как первоначальный HTTP-запрос загружает страницу, все последующие взаимодействия происходят через WebSockets. Для каждого нажатия кнопки не создается новый HTTP-запрос, поэтому промежуточное программное обеспечение не выполняется повторно при каждом взаимодействии. &lt;code>HttpContext&lt;/code> доступен во время первоначального соединения, но полагаться на него на протяжении всего срока службы схемы — верный путь к ошибкам.&lt;/p>
&lt;p>Blazor WebAssembly полностью работает в браузере. Серверной части &lt;code>HttpContext&lt;/code> вообще нет. Состояние аутентификации должно быть получено из API, сохранено на стороне клиента и управляемо с помощью токенов — обычно JWT. Сервер доверяет клиенту только в рамках проверки токена.&lt;/p>
&lt;p>Это означает, что Blazor нужна собственная абстракция для состояния аутентификации, которая работает независимо от модели хостинга. Эта абстракция — &lt;code>AuthenticationStateProvider&lt;/code>.&lt;/p>
&lt;h2 id="состояние-аутентификации-фонд">Состояние аутентификации: Фонд&lt;/h2>
&lt;p>В основе системы аутентификации Blazor лежит &lt;code>AuthenticationStateProvider&lt;/code>. Это абстрактный класс, предоставляющий единственный критический метод:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> Task&amp;lt;AuthenticationState&amp;gt; GetAuthenticationStateAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Объект &lt;code>AuthenticationState&lt;/code> оборачивает &lt;code>ClaimsPrincipal&lt;/code> — ту же модель идентификации, которая используется во всем .NET. Компоненты не взаимодействуют напрямую с файлами cookie или токенами; они запрашивают &lt;code>AuthenticationStateProvider&lt;/code> о текущем состоянии.&lt;/p>
&lt;p>Чтобы сделать это состояние доступным для всего вашего дерева компонентов, Blazor предоставляет &lt;code>CascadingAuthenticationState&lt;/code>. Обычно вы оборачиваете свой маршрутизатор в &lt;code>App.razor&lt;/code> или в свой макет:&lt;/p>
&lt;p>[[[ТОК_11]]]&lt;/p>
&lt;p>&lt;code>AuthorizeRouteView&lt;/code> здесь выполняет двойную функцию: он проверяет, прошел ли пользователь аутентификацию и авторизацию, прежде чем отображать соответствующий компонент страницы, и предоставляет запасной пользовательский интерфейс, если это не так.&lt;/p>
&lt;p>В .NET 8 и более поздних версиях с унифицированной моделью Blazor вы настроите это в своем &lt;code>App.razor&lt;/code> и платформа автоматически обрабатывает параметр каскадирования, когда вы используете &lt;code>AddCascadingAuthenticationState()&lt;/code> при регистрации службы.&lt;/p>
&lt;h2 id="интеграция-удостоверений-aspnet">Интеграция удостоверений ASP.NET&lt;/h2>
&lt;p>Для большинства проектов вам не нужно создавать аутентификацию с нуля. ASP.NET Identity обеспечивает управление пользователями, хеширование паролей, двухфакторную аутентификацию и подтверждение учетной записи «из коробки».Настройка с помощью Blazor начинается в &lt;code>Program.cs&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddDbContext&amp;lt;ApplicationDbContext&amp;gt;(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.UseSqlServer(builder.Configuration.GetConnectionString(&lt;span style="color:#a5d6ff">&amp;#34;DefaultConnection&amp;#34;&lt;/span>)));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddDefaultIdentity&amp;lt;IdentityUser&amp;gt;(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.SignIn.RequireConfirmedAccount = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.Password.RequireDigit = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.Password.RequiredLength = &lt;span style="color:#a5d6ff">8&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddRoles&amp;lt;IdentityRole&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddEntityFrameworkStores&amp;lt;ApplicationDbContext&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddAuthentication(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.DefaultScheme = IdentityConstants.ApplicationScheme;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Благодаря шаблону веб-приложения Blazor в .NET 8+ шаблонный пользовательский интерфейс Identity напрямую использует компоненты Razor. Вы получаете страницы входа, регистрации и управления учетной записью, которые естественным образом интегрируются с остальной частью вашего приложения Blazor — больше не нужно неудобного сочетания страниц Razor и компонентов Blazor.&lt;/p>
&lt;p>&lt;code>ApplicationDbContext&lt;/code> наследуется от &lt;code>IdentityDbContext&lt;/code> и вам потребуется выполнить миграцию для создания таблиц идентификации:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ApplicationDbContext&lt;/span> : IdentityDbContext&amp;lt;IdentityUser&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ApplicationDbContext(DbContextOptions&amp;lt;ApplicationDbContext&amp;gt; options)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : &lt;span style="color:#ff7b72">base&lt;/span>(options) { }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet ef migrations add InitialIdentity
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet ef database update
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="компонент-authorizeview">Компонент AuthorizeView&lt;/h2>
&lt;p>Как только состояние аутентификации проходит через ваше дерево компонентов, &lt;code>AuthorizeView&lt;/code> позволяет вам условно отображать пользовательский интерфейс:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;Welcome, @context.User.Identity?.Name!&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&amp;#34;/account/manage&amp;#34;&amp;gt;Manage Account&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form method=&amp;#34;post&amp;#34; action=&amp;#34;/account/logout&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34;&amp;gt;Log Out&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&amp;#34;/account/login&amp;#34;&amp;gt;Log In&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&amp;#34;/account/register&amp;#34;&amp;gt;Register&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Параметр &lt;code>context&lt;/code> внутри &lt;code>&amp;lt;Authorized&amp;gt;&lt;/code> предоставляет вам доступ к &lt;code>AuthenticationState&lt;/code>, поэтому вы можете проверять утверждения, роли и личность пользователя непосредственно в вашей разметке.&lt;/p>
&lt;p>Вы также можете использовать &lt;code>AuthorizeView&lt;/code> с ролями и политиками:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;AuthorizeView Roles=&amp;#34;Admin,Moderator&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button @onclick=&amp;#34;DeletePost&amp;#34;&amp;gt;Delete Post&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;AuthorizeView Policy=&amp;#34;CanEditArticles&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button @onclick=&amp;#34;EditArticle&amp;#34;&amp;gt;Edit&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Следует иметь в виду одну вещь: &lt;code>AuthorizeView&lt;/code> — это проблема пользовательского интерфейса. Он скрывает или показывает элементы, но не защищает основную логику. Если кто-то может вызвать конечную точку вашего API или вызвать ваш метод напрямую, он полностью обходит &lt;code>AuthorizeView&lt;/code> . Всегда применяйте авторизацию и на стороне сервера.&lt;/p>
&lt;h2 id="атрибут-авторизация">Атрибут [Авторизация]&lt;/h2>
&lt;p>Чтобы защитить всю страницу, примените атрибут &lt;code>[Authorize]&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/admin/dashboard&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@attribute [Authorize(Roles = &amp;#34;Admin&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;Admin Dashboard&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;Only administrators can see this page.&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Когда неаутентифицированный пользователь переходит на эту страницу, срабатывает &lt;code>AuthorizeRouteView&lt;/code> и отображает шаблон &lt;code>&amp;lt;NotAuthorized&amp;gt;&lt;/code>, который вы определили ранее. Вместо этого вы можете перенаправить на страницу входа, обработав случай &lt;code>NotAuthorized&lt;/code> с помощью навигации:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-zed" data-lang="zed">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>NotAuthorized&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">@&lt;/span>if&lt;span style="color:#6e7681"> &lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>context.User.Identity&lt;span style="color:#ff7b72;font-weight:bold">?&lt;/span>.IsAuthenticated&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">??&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>true)&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>{&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>RedirectToLogin&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>}&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>else&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>{&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>You&lt;span style="color:#6e7681"> &lt;/span>don&lt;span style="color:#f85149">&amp;#39;&lt;/span>t&lt;span style="color:#6e7681"> &lt;/span>have&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">permission&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>to&lt;span style="color:#6e7681"> &lt;/span>access&lt;span style="color:#6e7681"> &lt;/span>this&lt;span style="color:#6e7681"> &lt;/span>page.&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>}&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>NotAuthorized&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Простой компонент &lt;code>RedirectToLogin&lt;/code> может выглядеть так:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject NavigationManager &lt;span style="color:#f0883e;font-weight:bold">Navigation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override void OnInitialized()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> returnUrl &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> Uri&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>EscapeDataString(&lt;span style="color:#f0883e;font-weight:bold">Navigation&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Uri);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f0883e;font-weight:bold">Navigation&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NavigateTo(&lt;span style="color:#ff7b72;font-weight:bold">$&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;/account/login?returnUrl={returnUrl}&amp;#34;&lt;/span>, forceLoad: true);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Здесь важен &lt;code>forceLoad: true&lt;/code> — вам нужна настоящая HTTP-навигация, чтобы промежуточное программное обеспечение аутентификации на стороне сервера могло правильно обрабатывать процесс входа в систему.&lt;/p>
&lt;h2 id="авторизация-на-основе-ролей-и-политик">Авторизация на основе ролей и политик&lt;/h2>
&lt;p>Роли — это самая простая модель: назначьте пользователей в группы, например «Администратор» или «Редактор», а затем проверьте членство. Но политика дает вам гораздо больше гибкости.&lt;/p>
&lt;p>Зарегистрируйте политики в &lt;code>Program.cs&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddAuthorizationCore(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.AddPolicy(&lt;span style="color:#a5d6ff">&amp;#34;CanPublish&amp;#34;&lt;/span>, policy =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> policy.RequireClaim(&lt;span style="color:#a5d6ff">&amp;#34;Permission&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Publish&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.AddPolicy(&lt;span style="color:#a5d6ff">&amp;#34;MinimumAge&amp;#34;&lt;/span>, policy =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> policy.Requirements.Add(&lt;span style="color:#ff7b72">new&lt;/span> MinimumAgeRequirement(&lt;span style="color:#a5d6ff">18&lt;/span>)));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.AddPolicy(&lt;span style="color:#a5d6ff">&amp;#34;PremiumUser&amp;#34;&lt;/span>, policy =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> policy.RequireRole(&lt;span style="color:#a5d6ff">&amp;#34;Premium&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .RequireClaim(&lt;span style="color:#a5d6ff">&amp;#34;Subscription&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Active&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Пользовательским требованиям нужен обработчик:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">MinimumAgeRequirement&lt;/span> : IAuthorizationRequirement
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> MinimumAge { &lt;span style="color:#ff7b72">get&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> MinimumAgeRequirement(&lt;span style="color:#ff7b72">int&lt;/span> minimumAge) =&amp;gt; MinimumAge = minimumAge;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">MinimumAgeHandler&lt;/span> : AuthorizationHandler&amp;lt;MinimumAgeRequirement&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> Task HandleRequirementAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AuthorizationHandlerContext context,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MinimumAgeRequirement requirement)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> dateOfBirthClaim = context.User.FindFirst(&lt;span style="color:#a5d6ff">&amp;#34;DateOfBirth&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (dateOfBirthClaim &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> dateOfBirth = DateOnly.Parse(dateOfBirthClaim.Value);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> age = DateOnly.FromDateTime(DateTime.Today).Year - dateOfBirth.Year;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (age &amp;gt;= requirement.MinimumAge)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.Succeed(requirement);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Зарегистрируйте обработчик:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddSingleton&amp;lt;IAuthorizationHandler, MinimumAgeHandler&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>В компонентах также можно проверить авторизацию программно, когда вам нужна динамическая логика:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject IAuthorizationService AuthorizationService
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject AuthenticationStateProvider AuthStateProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">bool&lt;/span> canPublish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> authState &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await AuthStateProvider&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetAuthenticationStateAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await AuthorizationService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>AuthorizeAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> authState&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>User, &lt;span style="color:#a5d6ff">&amp;#34;CanPublish&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> canPublish &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> result&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Succeeded;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="внешние-поставщики-oauth">Внешние поставщики OAuth&lt;/h2>
&lt;p>Поддержка «Войти с помощью Google» или «Войти с помощью GitHub» проста с помощью промежуточного программного обеспечения аутентификации ASP.NET. Они настраиваются на стороне сервера, поскольку поток OAuth требует перенаправления HTTP.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddAuthentication()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddGoogle(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientId = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:Google:ClientId&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientSecret = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:Google:ClientSecret&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.Scope.Add(&lt;span style="color:#a5d6ff">&amp;#34;profile&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddMicrosoftAccount(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientId = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:Microsoft:ClientId&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientSecret = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:Microsoft:ClientSecret&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddGitHub(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientId = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:GitHub:ClientId&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientSecret = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:GitHub:ClientSecret&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Для GitHub вам понадобится пакет NuGet &lt;code>AspNet.Security.OAuth.GitHub&lt;/code> NuGet, поскольку он не включен в библиотеки ASP.NET по умолчанию.&lt;/p>
&lt;p>Пользовательский интерфейс входа затем предоставляет ссылки, которые запускают внешний вызов:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/account/external-login&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h2&amp;gt;Sign in with an external provider&amp;lt;/h2&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;form method=&amp;#34;post&amp;#34; action=&amp;#34;/api/auth/external-login&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34; name=&amp;#34;provider&amp;#34; value=&amp;#34;Google&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Sign in with Google
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34; name=&amp;#34;provider&amp;#34; value=&amp;#34;Microsoft&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Sign in with Microsoft
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34; name=&amp;#34;provider&amp;#34; value=&amp;#34;GitHub&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Sign in with GitHub
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Конечная точка API запускает вызов и обрабатывает обратный вызов:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/api/auth/external-login&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">string&lt;/span> provider, HttpContext context) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> properties = &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationProperties
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RedirectUri = &lt;span style="color:#a5d6ff">&amp;#34;/api/auth/external-callback&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Challenge(properties, [provider]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Внешняя проверка подлинности в Blazor всегда требует полностраничной навигации — вы не можете выполнить перенаправление OAuth внутри схемы SignalR или приложения WebAssembly, минуя сервер.&lt;/p>
&lt;h2 id="аутентификация-на-основе-токенов-в-blazor-webassemblyblazor-webassembly-запускается-на-клиенте-поэтому-проверка-подлинности-на-основе-файлов-cookie-не-применяется-таким-же-образом-вместо-этого-вы-обычно-используете-jwt-хранящиеся-в-памяти-и-прикрепленные-к-исходящим-http-запросам">Аутентификация на основе токенов в Blazor WebAssemblyBlazor WebAssembly запускается на клиенте, поэтому проверка подлинности на основе файлов cookie не применяется таким же образом. Вместо этого вы обычно используете JWT, хранящиеся в памяти и прикрепленные к исходящим HTTP-запросам.&lt;/h2>
&lt;p>Платформа предоставляет &lt;code>AuthorizationMessageHandler&lt;/code> для автоматического прикрепления токенов:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddHttpClient(&lt;span style="color:#a5d6ff">&amp;#34;API&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client =&amp;gt; client.BaseAddress = &lt;span style="color:#ff7b72">new&lt;/span> Uri(&lt;span style="color:#a5d6ff">&amp;#34;https://api.example.com&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddHttpMessageHandler&amp;lt;AuthorizationMessageHandler&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped(sp =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sp.GetRequiredService&amp;lt;IHttpClientFactory&amp;gt;().CreateClient(&lt;span style="color:#a5d6ff">&amp;#34;API&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Для автономных приложений Blazor WASM, которые проходят аутентификацию по собственному API, вы реализуете собственный &lt;code>AuthenticationStateProvider&lt;/code> для анализа JWT:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">JwtAuthenticationStateProvider&lt;/span> : AuthenticationStateProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> ILocalStorageService _localStorage;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> JwtAuthenticationStateProvider(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ILocalStorageService localStorage,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> HttpClient httpClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _localStorage = localStorage;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;AuthenticationState&amp;gt; GetAuthenticationStateAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> token = &lt;span style="color:#ff7b72">await&lt;/span> _localStorage.GetItemAsync&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;authToken&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(token))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity()));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient.DefaultRequestHeaders.Authorization =
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationHeaderValue(&lt;span style="color:#a5d6ff">&amp;#34;Bearer&amp;#34;&lt;/span>, token);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> claims = ParseClaimsFromJwt(token);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> identity = &lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity(claims, &lt;span style="color:#a5d6ff">&amp;#34;jwt&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(identity));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> NotifyAuthStateChanged()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> IEnumerable&amp;lt;Claim&amp;gt; ParseClaimsFromJwt(&lt;span style="color:#ff7b72">string&lt;/span> jwt)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> payload = jwt.Split(&lt;span style="color:#a5d6ff">&amp;#39;.&amp;#39;&lt;/span>)[&lt;span style="color:#a5d6ff">1&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> padded = payload.Length % &lt;span style="color:#a5d6ff">4&lt;/span> &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">2&lt;/span> =&amp;gt; payload + &lt;span style="color:#a5d6ff">&amp;#34;==&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">3&lt;/span> =&amp;gt; payload + &lt;span style="color:#a5d6ff">&amp;#34;=&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ =&amp;gt; payload
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bytes = Convert.FromBase64String(padded);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> json = JsonSerializer.Deserialize&amp;lt;Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, JsonElement&amp;gt;&amp;gt;(bytes);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> json?.Select(kvp =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> Claim(kvp.Key, kvp.Value.ToString()))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ?? Enumerable.Empty&amp;lt;Claim&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>После успешного входа в систему вы сохраняете токен и уведомляете состояние аутентификации:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">AuthService&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> ILocalStorageService _localStorage;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> JwtAuthenticationStateProvider _authStateProvider;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> AuthService(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> HttpClient httpClient,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ILocalStorageService localStorage,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AuthenticationStateProvider authStateProvider)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _localStorage = localStorage;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _authStateProvider = (JwtAuthenticationStateProvider)authStateProvider;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt; LoginAsync(&lt;span style="color:#ff7b72">string&lt;/span> email, &lt;span style="color:#ff7b72">string&lt;/span> password)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _httpClient.PostAsJsonAsync(&lt;span style="color:#a5d6ff">&amp;#34;/api/auth/login&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> { Email = email, Password = password });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!response.IsSuccessStatusCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> response.Content
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ReadFromJsonAsync&amp;lt;LoginResponse&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _localStorage.SetItemAsync(&lt;span style="color:#a5d6ff">&amp;#34;authToken&amp;#34;&lt;/span>, result!.Token);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _authStateProvider.NotifyAuthStateChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task LogoutAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _localStorage.RemoveItemAsync(&lt;span style="color:#a5d6ff">&amp;#34;authToken&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _authStateProvider.NotifyAuthStateChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Предупреждение: хранение JWT в &lt;code>localStorage&lt;/code> подвергает их XSS-атакам. Для приложений с более высоким уровнем безопасности рассмотрите возможность хранения токенов только в памяти и использования токенов обновления или принятия шаблона Backend-for-Frontend (BFF), где сервер управляет токенами, а клиент использует файлы cookie только для HTTP.&lt;/p>
&lt;h2 id="blazor-server-против-webassembly-соображения-безопасности">Blazor Server против WebAssembly: соображения безопасности&lt;/h2>
&lt;p>Модель хостинга фундаментально меняет вашу безопасность.&lt;/p>
&lt;p>&lt;strong>Blazor Server&lt;/strong> хранит всю логику ваших компонентов на сервере. Клиент видит только отображаемые различия HTML через SignalR. Это означает:&lt;/p>
&lt;ul>
&lt;li>Чувствительная логика никогда не покидает сервер&lt;/li>
&lt;li>Вы можете получить доступ к базам данных и внутренним сервисам непосредственно из компонентов.&lt;/li>
&lt;li>Состояние аутентификации поступает от &lt;code>HttpContext&lt;/code> сервера при первоначальном подключении.&lt;/li>
&lt;li>Схема может пережить файл cookie аутентификации — если срок действия файла cookie пользователя истечет, схема останется активной до тех пор, пока не будет отключена.&lt;/li>
&lt;li>Вы должны корректно обрабатывать отключение канала и повторно проверять состояние аутентификации при повторном подключении.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Blazor WebAssembly&lt;/strong> полностью работает в браузере. Это означает:&lt;/p>
&lt;ul>
&lt;li>Весь код вашего компонента можно загрузить и проверить.&lt;/li>
&lt;li>Никогда не помещайте секреты, строки подключения или конфиденциальную бизнес-логику в компоненты WASM.
— Аутентификация применяется только на клиенте для UX; реальное соблюдение требований должно происходить на уровне вашего API.&lt;/li>
&lt;li>Управление токенами — ваша ответственность.
– Рассмотрите возможность использования размещенной модели, в которой серверный проект обрабатывает аутентификацию и обслуживает приложение WASM.&lt;/li>
&lt;/ul>
&lt;p>Шаблон, который я рекомендую для приложений WebAssembly, заключается в том, чтобы рассматривать каждый компонент так, как будто это «ненадежный пользовательский интерфейс», а каждую конечную точку API — так, как если бы он вызывался неизвестным клиентом. Проверяйте все на стороне сервера независимо от того, что проверяет клиент.&lt;/p>
&lt;h2 id="создание-пользовательского-поставщика-authenticationstateprovider">Создание пользовательского поставщика AuthenticationStateProvider&lt;/h2>
&lt;p>Иногда встроенные поставщики не соответствуют вашей архитектуре. Возможно, вы интегрируетесь с устаревшей системой аутентификации или вам нужно опросить изменения состояния аутентификации. Вот более полный пользовательский поставщик для Blazor Server:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CustomAuthStateProvider&lt;/span> : AuthenticationStateProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IHttpContextAccessor _httpContextAccessor;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IUserService _userService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CustomAuthStateProvider(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IHttpContextAccessor httpContextAccessor,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IUserService userService)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpContextAccessor = httpContextAccessor;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _userService = userService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;AuthenticationState&amp;gt; GetAuthenticationStateAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> httpContext = _httpContextAccessor.HttpContext;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (httpContext?.User?.Identity?.IsAuthenticated != &lt;span style="color:#79c0ff">true&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity()));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (userId &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity()));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> user = &lt;span style="color:#ff7b72">await&lt;/span> _userService.GetUserWithClaimsAsync(userId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (user &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity()));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> claims = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;Claim&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span>(ClaimTypes.NameIdentifier, user.Id),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span>(ClaimTypes.Name, user.DisplayName),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span>(ClaimTypes.Email, user.Email)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> claims.AddRange(user.Roles.Select(r =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> Claim(ClaimTypes.Role, r)));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> claims.AddRange(user.Permissions.Select(p =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> Claim(&lt;span style="color:#a5d6ff">&amp;#34;Permission&amp;#34;&lt;/span>, p)));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> identity = &lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity(claims, &lt;span style="color:#a5d6ff">&amp;#34;Custom&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(identity));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> MarkUserAsAuthenticated(&lt;span style="color:#ff7b72">string&lt;/span> userId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> MarkUserAsLoggedOut()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> anonymous = &lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> authState = Task.FromResult(&lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(anonymous));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NotifyAuthenticationStateChanged(authState);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Зарегистрируйте его в &lt;code>Program.cs&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;AuthenticationStateProvider, CustomAuthStateProvider&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;CustomAuthStateProvider&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ключевым моментом здесь является &lt;code>NotifyAuthenticationStateChanged&lt;/code> — его вызов запускает обновление каскадного параметра, который повторно оценивает каждый &lt;code>AuthorizeView&lt;/code> и &lt;code>AuthorizeRouteView&lt;/code> в вашем дереве компонентов. Вот как вы заставляете пользовательский интерфейс реагировать на события входа/выхода без полного обновления страницы.&lt;/p>
&lt;h2 id="распространенные-ошибки-и-решения">Распространенные ошибки и решения&lt;/h2>
&lt;p>После работы с авторизацией Blazor во многих проектах я чаще всего вижу следующие проблемы:&lt;/p>
&lt;h3 id="1-использование-httpcontext-в-серверных-компонентах-blazorhttpcontext-доступен-во-время-первоначального-http-запроса-но-он-null-или-устарел-во-время-взаимодействия-signalr-не-вводите-ihttpcontextaccessor-в-компоненты-которые-запускаются-после-первоначального-рендеринга">1. Использование HttpContext в серверных компонентах Blazor&lt;code>HttpContext&lt;/code> доступен во время первоначального HTTP-запроса, но он &lt;code>null&lt;/code> или устарел во время взаимодействия SignalR. Не вводите &lt;code>IHttpContextAccessor&lt;/code> в компоненты, которые запускаются после первоначального рендеринга.&lt;/h3>
&lt;p>&lt;strong>Решение:&lt;/strong> Во время инициализации захватите все, что вам нужно, из &lt;code>HttpContext&lt;/code> и сохраните его в сервисе с ограниченной областью действия:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">UserContext&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string?&lt;/span> UserId { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string?&lt;/span> AccessToken { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// In a component that renders during the initial HTTP request:&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inject IHttpContextAccessor HttpContextAccessor
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inject UserContext UserContext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnInitialized()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> context = HttpContextAccessor.HttpContext;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> UserContext.UserId = context?.User.FindFirstValue(ClaimTypes.NameIdentifier);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> UserContext.AccessToken = context?.Request.Headers.Authorization
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToString().Replace(&lt;span style="color:#a5d6ff">&amp;#34;Bearer &amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-состояние-аутентификации-не-обновляется-после-входа-в-систему">2. Состояние аутентификации не обновляется после входа в систему&lt;/h3>
&lt;p>Вы вызываете свой API входа в систему, это удается, но пользовательский интерфейс по-прежнему показывает «Войти».&lt;/p>
&lt;p>&lt;strong>Решение:&lt;/strong> Вы должны вызвать &lt;code>NotifyAuthenticationStateChanged&lt;/code> на своем &lt;code>AuthenticationStateProvider&lt;/code> после изменения состояния аутентификации. Платформа не может волшебным образом обнаружить, что токен был сохранен или был установлен файл cookie.&lt;/p>
&lt;h3 id="3-атрибут-авторизации-не-работает-с-компонентами">3. Атрибут авторизации не работает с компонентами&lt;/h3>
&lt;p>Вы добавляете &lt;code>[Authorize]&lt;/code> в компонент, но он не блокирует неаутентифицированных пользователей.&lt;/p>
&lt;p>&lt;strong>Решение.&lt;/strong> Убедитесь, что вы используете &lt;code>AuthorizeRouteView&lt;/code> вместо обычного &lt;code>RouteView&lt;/code> в своем &lt;code>App.razor&lt;/code>. Стандарт &lt;code>RouteView&lt;/code> полностью игнорирует атрибуты авторизации.&lt;/p>
&lt;h3 id="4-предварительный-рендеринг-нарушает-состояние-аутентификации">4. Предварительный рендеринг нарушает состояние аутентификации&lt;/h3>
&lt;p>Во время предварительной отрисовки на стороне сервера в Blazor WebAssembly токен аутентификации недоступен. Компоненты отображаются как не прошедшие проверку подлинности, а затем после загрузки WASM переходят в состояние проверки подлинности.&lt;/p>
&lt;p>&lt;strong>Решение.&lt;/strong> Либо отключите предварительную отрисовку для страниц, чувствительных к аутентификации, с помощью &lt;code>@rendermode InteractiveWebAssembly&lt;/code> (без предварительной отрисовки), либо корректно обработайте состояние загрузки:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;Welcome back, @context.User.Identity?.Name&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorizing&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorizing&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;Sign in&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="5-срок-действия-токена-в-долгоработающих-схемах">5. Срок действия токена в долгоработающих схемах&lt;/h3>
&lt;p>Цепи Blazor Server могут оставаться активными часами. Если срок действия вашего токена или сеанса истекает, пользователь остается «аутентифицированным» в пользовательском интерфейсе, но вызовы API начинают завершаться сбоем.&lt;/p>
&lt;p>&lt;strong>Решение.&lt;/strong> Внедрите периодическую проверку или используйте &lt;code>RevalidatingServerAuthenticationStateProvider&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">RevalidatingAuthStateProvider&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : RevalidatingServerAuthenticationStateProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IServiceScopeFactory _scopeFactory;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> RevalidatingAuthStateProvider(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ILoggerFactory loggerFactory,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IServiceScopeFactory scopeFactory)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : &lt;span style="color:#ff7b72">base&lt;/span>(loggerFactory)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _scopeFactory = scopeFactory;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> TimeSpan RevalidationInterval =&amp;gt; TimeSpan.FromMinutes(&lt;span style="color:#a5d6ff">30&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt; ValidateAuthenticationStateAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AuthenticationState authenticationState, CancellationToken cancellationToken)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> scope = _scopeFactory.CreateAsyncScope();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> userManager = scope.ServiceProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GetRequiredService&amp;lt;UserManager&amp;lt;IdentityUser&amp;gt;&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> user = &lt;span style="color:#ff7b72">await&lt;/span> userManager.GetUserAsync(authenticationState.User);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> user &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это подтверждает, что пользователь все еще существует (и его отметка безопасности не изменилась) каждые 30 минут.&lt;/p>
&lt;h2 id="заключение">Заключение&lt;/h2>
&lt;p>Аутентификация и авторизация в Blazor требуют отхода от традиционного ASP.NET типа «запрос-ответ». Абстракция &lt;code>AuthenticationStateProvider&lt;/code> — это ключ к пониманию того, как все это сочетается друг с другом. Как только вы это усвоите, все остальное вытекает само собой.&lt;/p>
&lt;p>Для большинства приложений начните с ASP.NET Identity и встроенных шаблонов. Они берут на себя тяжелую работу по управлению пользователями, хешированию паролей и генерации токенов. Добавляйте политики и авторизацию на основе утверждений по мере роста ваших требований. Добавляйте внешних поставщиков OAuth, когда ваши пользователи этого ожидают.&lt;/p>
&lt;p>Модель хостинга имеет значение: Blazor Server предлагает более традиционную схему безопасности, при которой код остается на сервере, а WebAssembly подталкивает вас к мышлению, ориентированному на приоритет API, где клиент не заслуживает доверия по своей конструкции. Ни один из них по своей сути не является более безопасным — просто у них разные модели угроз.&lt;/p>
&lt;p>Какой бы подход вы ни выбрали, помните золотое правило: &lt;strong>авторизация в пользовательском интерфейсе предназначена для удобства пользователя, авторизация на сервере — для обеспечения безопасности.&lt;/strong> Всегда применяйте и то, и другое.&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Security</category><category>Web Development</category></item><item><title>Изолированный JavaScript в Blazor с совмещенными файлами JS</title><link>https://emimontesdeoca.github.io/ru/posts/blazor-isolated-js/</link><pubDate>Wed, 18 Jun 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/blazor-isolated-js/</guid><description>Используйте совмещенные файлы JavaScript в Blazor, чтобы ограничить логику JS отдельными компонентами.</description><content:encoded>&lt;p>Если вы раньше работали с Blazor и JS Interop, вы, вероятно, получили огромный файл &lt;code>app.js&lt;/code> со случайными функциями для разных компонентов. Это работает, но быстро портится. К счастью, есть гораздо более простой подход: совместное размещение файлов JavaScript.&lt;/p>
&lt;h1 id="идея">Идея&lt;/h1>
&lt;p>Как и в случае с изоляцией CSS, Blazor позволяет разместить файл &lt;code>.razor.js&lt;/code> рядом с вашим компонентом. Модуль JavaScript загружается по требованию, только тогда, когда компоненту это действительно необходимо. Никаких глобальных сценариев, никакого загрязнения.&lt;/p>
&lt;h1 id="настройка">Настройка&lt;/h1>
&lt;p>Допустим, у нас есть компонент &lt;code>Clipboard.razor&lt;/code> который копирует текст в буфер обмена. Создайте файл с именем &lt;code>Clipboard.razor.js&lt;/code> рядом с ним:&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;p>Обратите внимание на ключевое слово &lt;code>export&lt;/code> — это важно. Blazor загружает его как стандартный модуль ES.&lt;/p>
&lt;h1 id="загрузка-модуля-в-blazor">Загрузка модуля в Blazor&lt;/h1>
&lt;p>В вашем компоненте вы используете &lt;code>IJSRuntime&lt;/code> для импорта модуля. Путь соответствует соглашению: &lt;code>./_content/{ASSEMBLY_NAME}/{COMPONENT_PATH}.razor.js&lt;/code> для библиотек или просто относительный путь для текущего проекта.&lt;/p>
&lt;p>[[[ТОК_8]]]&lt;/p>
&lt;p>Здесь следует отметить несколько вещей:&lt;/p>
&lt;ul>
&lt;li>Мы загружаем модуль в &lt;code>OnAfterRenderAsync&lt;/code>, поскольку JS Interop недоступен во время предварительного рендеринга на стороне сервера.&lt;/li>
&lt;li>Сохраняем ссылку на модуль с помощью &lt;code>IJSObjectReference&lt;/code>&lt;/li>
&lt;li>Мы реализуем &lt;code>IAsyncDisposable&lt;/code> для очистки модуля при уничтожении компонента.&lt;/li>
&lt;/ul>
&lt;h1 id="почему-это-лучше">Почему это лучше&lt;/h1>
&lt;p>Раньше я сбрасывал все в один &lt;code>wwwroot/js/app.js&lt;/code>. Это работало, но поиск функций был трудной задачей, и каждая страница загружала ненужный JavaScript. С совмещенными файлами JS:&lt;/p>
&lt;ul>
&lt;li>Каждый компонент имеет свой собственный JavaScript.&lt;/li>
&lt;li>Модули загружаются лениво, только при необходимости&lt;/li>
&lt;li>Отсутствие конфликтов имен глобальных функций.&lt;/li>
&lt;li>Легче поддерживать и удалять — при удалении компонента вы удаляете вместе с ним и JS.&lt;/li>
&lt;/ul>
&lt;h1 id="ошибка-с-путем">Ошибка с путем&lt;/h1>
&lt;p>Путь, который вы передаете к &lt;code>import&lt;/code> зависит от того, находитесь ли вы в автономном приложении Blazor или в библиотеке классов Razor. Для обычного приложения Blazor путь указывается относительно &lt;code>wwwroot&lt;/code>. Файл &lt;code>.razor.js&lt;/code> копируется туда во время сборки, поэтому вы ссылаетесь на него из местоположения компонента в проекте.&lt;/p>
&lt;p>Если при загрузке модуля вы получаете ошибку 404, дважды проверьте путь и убедитесь, что файл заканчивается на &lt;code>.razor.js&lt;/code>, а не только на &lt;code>.js&lt;/code>.&lt;/p>
&lt;p>Надеюсь, вам понравился пост! Не стесняйтесь обращаться ко мне в любой социальной сети по адресу &lt;strong>@emimontesdeoca&lt;/strong>.&lt;/p>
&lt;h1 id="ресурсы">Ресурсы&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/collocated-js">ASP.NET Core Blazor JavaScript с совмещенным JS&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>JavaScript</category></item><item><title>Интерактивность Blazor в .NET 9 и .NET 10: полное руководство</title><link>https://emimontesdeoca.github.io/ru/posts/blazor-interactivity-dotnet-9-10/</link><pubDate>Sun, 15 Jun 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/blazor-interactivity-dotnet-9-10/</guid><description>Подробное знакомство с режимами рендеринга Blazor, потоковой SSR, улучшенной навигацией и новыми функциями интерактивности в .NET 9 и .NET 10.</description><content:encoded>&lt;p>Если вы создавали веб-приложения с помощью Blazor в течение последних нескольких лет, вы знаете, что эта платформа прошла &lt;em>долгий&lt;/em> путь. То, что началось с выбора между Blazor Server и Blazor WebAssembly, превратилось в унифицированную гибкую модель рендеринга, которая позволяет вам выбрать правильную стратегию интерактивности для каждого компонента вашего приложения.&lt;/p>
&lt;p>В .NET 8 мы получили фундаментальный сдвиг — введение режимов рендеринга и статического рендеринга на стороне сервера (SSR) по умолчанию. Теперь .NET 9 и .NET 10 построены на этой основе с усовершенствованиями, которые делают работу разработчиков более удобной, а работу конечных пользователей — более быстрой.&lt;/p>
&lt;p>В этом посте я хочу познакомить вас с полной картиной интерактивности Blazor в ее нынешнем виде: как работают режимы рендеринга, что дает потоковая SSR, как улучшенная навигация и обработка форм меняют игру и что нового в последних выпусках. Если вы планируете новый проект Blazor или думаете об обновлении, мне хотелось бы иметь это руководство, когда начал во всем этом разбираться.&lt;/p>
&lt;h2 id="быстрый-взгляд-назад-как-мы-сюда-попали">Быстрый взгляд назад: как мы сюда попали&lt;/h2>
&lt;p>До .NET 8 вам приходилось использовать модель хостинга на уровне проекта. Blazor Server означал, что все работало на сервере через соединение SignalR. Blazor WebAssembly означал, что все работает в браузере. У каждого были свои компромиссы, и смешивать их было болезненно.&lt;/p>
&lt;p>.NET 8 изменил правила игры, представив единый шаблон проекта — веб-приложение Blazor — который объединяет обе модели. Ключевой концепцией являются &lt;strong>режимы рендеринга&lt;/strong>: вы решаете &lt;em>для каждого компонента&lt;/em>, как он должен отображаться и где происходит интерактивность. По умолчанию выбран статический SSR, что означает, что компоненты визуализируются на сервере и отправляют в браузер обычный HTML — без SignalR, без WebAssembly, только быстрый HTML.&lt;/p>
&lt;p>В .NET 9 эти концепции были усовершенствованы, улучшены условия для разработчиков и оптимизирована производительность. .NET 10 идет дальше благодаря улучшенной обработке повторного подключения, сохранению состояния компонента в разных режимах рендеринга и улучшениям доставки самого скрипта Blazor.&lt;/p>
&lt;p>Давайте разберем все это.&lt;/p>
&lt;h2 id="режимы-рендеринга-в-net-9">Режимы рендеринга в .NET 9&lt;/h2>
&lt;p>В основе современного Blazor лежит концепция режимов рендеринга. Есть четыре режима, о которых вам следует знать:&lt;/p>
&lt;h3 id="1-статический-ssr-по-умолчанию">1. Статический SSR (по умолчанию)&lt;/h3>
&lt;p>Когда вы создаете новое веб-приложение Blazor, компоненты по умолчанию отображаются статически на сервере. Сервер обрабатывает компонент Razor, генерирует HTML и отправляет его в браузер. Нет постоянного соединения, нет среды выполнения WebAssembly — только традиционный запрос/ответ.&lt;/p>
&lt;p>Это идеально подходит для страниц с большим количеством контента, информационных панелей, которые в основном отображают данные, или любой страницы, где вам не требуется взаимодействие в реальном времени.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Our Products&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> product &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> products)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;product-card&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Name&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Description&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>span &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;price&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;C&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Product&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> products &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> products &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ProductService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetAllAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Этот компонент отображает на сервере, создает HTML и все. Нет постоянной связи. Быстрая начальная загрузка, отлично подходит для SEO, минимальное использование ресурсов сервера.&lt;/p>
&lt;h3 id="2-интерактивный-сервересли-вам-нужна-интерактивность-в-реальном-времени--обработка-нажатий-кнопок-обработка-ввода-пользователя-динамическое-обновление-пользовательского-интерфейса--вы-можете-выбрать-режим-интерактивного-сервера-при-этом-устанавливается-соединение-signalr-между-браузером-и-сервером-и-обновления-пользовательского-интерфейса-происходят-через-это-соединение">2. Интерактивный серверЕсли вам нужна интерактивность в реальном времени — обработка нажатий кнопок, обработка ввода пользователя, динамическое обновление пользовательского интерфейса — вы можете выбрать режим интерактивного сервера. При этом устанавливается соединение SignalR между браузером и сервером, и обновления пользовательского интерфейса происходят через это соединение.&lt;/h3>
&lt;p>[[[ТОК_1]]]&lt;/p>
&lt;p>Директива &lt;code>@rendermode InteractiveServer&lt;/code> — это все, что нужно. Компонент сначала отображается на сервере, а затем устанавливается соединение SignalR для обработки последующих взаимодействий. Обработчики событий C# выполняются на сервере, а различия пользовательского интерфейса отправляются в браузер.&lt;/p>
&lt;p>&lt;strong>Когда его использовать:&lt;/strong> Когда вам нужна интерактивность, вашим компонентам необходим доступ к ресурсам на стороне сервера (базам данных, API, файловым системам), и вам нужна быстрая начальная загрузка, не дожидаясь загрузки WebAssembly.&lt;/p>
&lt;h3 id="3-интерактивная-веб-сборка">3. Интерактивная веб-сборка&lt;/h3>
&lt;p>Если вам нужна интерактивность без поддержания соединения с сервером, Interactive WebAssembly запускает логику вашего компонента непосредственно в браузере, используя среду выполнения .NET WebAssembly.&lt;/p>
&lt;p>[[[ТОК_3]]]&lt;/p>
&lt;p>Компромисс: существует первоначальная стоимость загрузки среды выполнения .NET и ваших сборок. Но после загрузки компонент полностью запускается в браузере без каких-либо обращений к серверу для взаимодействия с пользовательским интерфейсом.&lt;/p>
&lt;p>&lt;strong>Когда его использовать:&lt;/strong> для высокоинтерактивных компонентов, где важна задержка (например, редакторы форматированного текста, инструменты рисования, фильтрация в реальном времени), когда вы хотите снизить нагрузку на сервер или когда вы создаете прогрессивное веб-приложение (PWA).&lt;/p>
&lt;h3 id="4-интерактивный-авто">4. Интерактивный авто&lt;/h3>
&lt;p>Это прагматичная золотая середина и одна из моих любимых особенностей. Interactive Auto начинается с рендеринга на стороне сервера через SignalR для первой загрузки, затем автоматически загружает среду выполнения WebAssembly в фоновом режиме. При последующих посещениях компонент запускается на WebAssembly.&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;p>Это дает вам лучшее из обоих миров: быстрый первый рендеринг (без ожидания загрузки WASM) и, в конечном итоге, компонент запускается на стороне клиента. Пользователь не замечает перехода.&lt;/p>
&lt;p>&lt;strong>Когда его использовать:&lt;/strong> Когда вам нужна как быстрая начальная загрузка, так и возможное выполнение на стороне клиента. Это отличный выбор по умолчанию для многих интерактивных компонентов.&lt;/p>
&lt;h2 id="потоковое-ssr-лучшее-из-обоих-миров">Потоковое SSR: лучшее из обоих миров&lt;/h2>
&lt;p>Потоковая SSR — одна из тех функций, которая звучит просто, но существенно влияет на воспринимаемую производительность. Вот идея: вместо того, чтобы ждать загрузки всех ваших данных перед отправкой любого HTML, сервер немедленно отправляет оболочку страницы, а затем &lt;em>транслирует&lt;/em> обновления контента по мере того, как данные становятся доступными.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/reports&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>attribute [StreamRendering]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Monthly Reports&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (reports is null)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;loading-spinner&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Loading reports&lt;span style="color:#ff7b72;font-weight:bold">...&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>table &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;table&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Month&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Revenue&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Growth&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> report &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> reports)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>report&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Month&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>report&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Revenue&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;C&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@(report.Growth &amp;gt;= 0 ? &amp;#34;&lt;/span>text&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>success&lt;span style="color:#a5d6ff">&amp;#34; : &amp;#34;&lt;/span>text&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>danger&lt;span style="color:#a5d6ff">&amp;#34;)&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>report&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Growth&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;P1&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>table&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>MonthlyReport&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">?&lt;/span> reports;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">//&lt;/span> This might take a couple of seconds
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reports &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ReportService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetMonthlyReportsAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>С атрибутом &lt;code>[StreamRendering]&lt;/code> происходит следующее:&lt;/p>
&lt;ol>
&lt;li>Сервер немедленно отправляет HTML с помощью счетчика загрузки.&lt;/li>
&lt;li>&lt;code>OnInitializedAsync&lt;/code> запускается и извлекает данные.&lt;/li>
&lt;li>Когда данные поступают, сервер передает обновленный HTML (таблицу) в браузер.&lt;/li>
&lt;li>Браузер исправляет DOM без полной перезагрузки страницы.&lt;/li>
&lt;/ol>
&lt;p>Пользователь видит страницу &lt;em>мгновенно&lt;/em> с индикатором загрузки, затем содержимое заполняется. Никакой инфраструктуры JavaScript не требуется. Нет подключения к WebSocket. Просто умное использование потоковой передачи HTTP.&lt;strong>Когда использовать:&lt;/strong> Любая статическая страница SSR, которая извлекает данные во время рендеринга. Страницы со списком продуктов, информационные панели, отчеты — везде первоначальная загрузка данных может занять более нескольких сотен миллисекунд.&lt;/p>
&lt;p>&lt;strong>Когда НЕ использовать:&lt;/strong> Если данные загружаются очень быстро (менее 100 мс), затраты на потоковую передачу того не стоят. Кроме того, потоковая SSR не обеспечивает постоянной интерактивности — для этого вам все равно нужен интерактивный режим рендеринга.&lt;/p>
&lt;h2 id="улучшенная-навигация-и-обработка-форм">Улучшенная навигация и обработка форм&lt;/h2>
&lt;p>Одним из тонких, но мощных улучшений современного Blazor является улучшенная навигация. По умолчанию Blazor перехватывает клики по внутренним ссылкам и отправку форм, получая новое содержимое страницы через &lt;code>fetch&lt;/code> и исправляя DOM вместо полной навигации по браузеру.&lt;/p>
&lt;p>Это означает, что навигация между статическими страницами SSR ощущается как SPA — полная страница не мигает, положение прокрутки может быть сохранено, а работа очень плавная.&lt;/p>
&lt;h3 id="как-это-работает">Как это работает&lt;/h3>
&lt;p>При загрузке скрипта Blazor (&lt;code>blazor.web.js&lt;/code>) он автоматически перехватывает клики по внутренним ссылкам. Вместо традиционной навигации в браузере он:&lt;/p>
&lt;ol>
&lt;li>Делает запрос &lt;code>fetch&lt;/code> к целевому URL.&lt;/li>
&lt;li>Получает ответ в формате HTML.&lt;/li>
&lt;li>Объединяет новый контент с существующим DOM.&lt;/li>
&lt;li>Обновляет URL-адрес и историю браузера.&lt;/li>
&lt;/ol>
&lt;p>Вам не нужно ничего делать, чтобы включить эту функцию — она включена по умолчанию. Но вы можете это контролировать:&lt;/p>
&lt;p>[[[ТОК_11]]]&lt;/p>
&lt;h3 id="улучшенная-обработка-форм">Улучшенная обработка форм&lt;/h3>
&lt;p>Формы обрабатываются одинаково. Когда вы используете &lt;code>EditForm&lt;/code> или стандартный элемент &lt;code>&amp;lt;form&amp;gt;&lt;/code> с обработкой формы Blazor, отправки перехватываются и обрабатываются через &lt;code>fetch&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/contact&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;EditForm Model=&amp;#34;contactModel&amp;#34; OnValidSubmit=&amp;#34;HandleSubmit&amp;#34; FormName=&amp;#34;contact&amp;#34; Enhance&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;DataAnnotationsValidator /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&amp;#34;mb-3&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label for=&amp;#34;name&amp;#34;&amp;gt;Name&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputText id=&amp;#34;name&amp;#34; @bind-Value=&amp;#34;contactModel.Name&amp;#34; class=&amp;#34;form-control&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&amp;#34;() =&amp;gt; contactModel.Name&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&amp;#34;mb-3&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label for=&amp;#34;email&amp;#34;&amp;gt;Email&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputText id=&amp;#34;email&amp;#34; @bind-Value=&amp;#34;contactModel.Email&amp;#34; class=&amp;#34;form-control&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&amp;#34;() =&amp;gt; contactModel.Email&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&amp;#34;mb-3&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label for=&amp;#34;message&amp;#34;&amp;gt;Message&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputTextArea id=&amp;#34;message&amp;#34; @bind-Value=&amp;#34;contactModel.Message&amp;#34; class=&amp;#34;form-control&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&amp;#34;() =&amp;gt; contactModel.Message&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34; class=&amp;#34;btn btn-primary&amp;#34;&amp;gt;Send&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/EditForm&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [SupplyParameterFromForm]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private ContactModel contactModel { get; set; } = new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task HandleSubmit()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> await ContactService.SubmitAsync(contactModel);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> contactModel = new ContactModel();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Атрибут &lt;code>Enhance&lt;/code> в &lt;code>EditForm&lt;/code> указывает Blazor перехватить отправку формы. Форма отправляется на сервер, сервер обрабатывает ее и повторно отображает страницу, а обновленный HTML-код передается обратно — и все это без полноценной навигации по странице. Он кажется интерактивным, но полностью визуализируется на сервере.&lt;/p>
&lt;p>Обратите внимание на атрибут &lt;code>[SupplyParameterFromForm]&lt;/code> — именно так Blazor привязывает опубликованные данные формы к вашей модели в статических сценариях SSR. Это мост между традиционными формами и компонентной моделью Blazor.&lt;/p>
&lt;h2 id="постраничная-и-покомпонентная-интерактивность">Постраничная и покомпонентная интерактивность&lt;/h2>
&lt;p>Одним из наиболее мощных аспектов новой модели является то, что вы можете смешивать режимы рендеринга в одном приложении. Ваша страница со списком продуктов может быть статической SSR, ваша корзина покупок может быть Interactive Server, а конфигуратор вашего продукта может быть Interactive WebAssembly — и все это в одном приложении.&lt;/p>
&lt;h3 id="настройка-режимов-рендеринга-на-уровне-компонента">Настройка режимов рендеринга на уровне компонента&lt;/h3>
&lt;p>Вы можете установить режим рендеринга непосредственно на компоненте с помощью директивы &lt;code>@rendermode&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@* This component is interactive via Server *@
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@rendermode InteractiveServer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h3&amp;gt;Live Chat&amp;lt;/h3&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;!-- chat UI here --&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Или вы можете установить его при использовании компонента из родителя:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/product/{Id:int}&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;@product?.Name&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;@product?.Description&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;!-- This child component gets its own interactive render mode --&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;ProductConfigurator Product=&amp;#34;product&amp;#34; @rendermode=&amp;#34;InteractiveWebAssembly&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;!-- This stays static --&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;ProductReviews ProductId=&amp;#34;Id&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public int Id { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private Product? product;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> product = await ProductService.GetByIdAsync(Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>В этом примере сама страница является статической SSR. Компонент &lt;code>ProductConfigurator&lt;/code> работает на WebAssembly для обеспечения богатой интерактивности на стороне клиента. Компонент &lt;code>ProductReviews&lt;/code> остается статическим, поскольку он просто отображает данные.&lt;/p>
&lt;h3 id="глобальная-настройка-режимов-рендеринга">Глобальная настройка режимов рендеринга&lt;/h3>
&lt;p>Если вы хотите, чтобы все страницы были интерактивными по умолчанию, вы можете установить режим рендеринга на корневом уровне в &lt;code>App.razor&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-mysql" data-lang="mysql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Routes&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">@&lt;/span>rendermode&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;InteractiveServer&amp;#34;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">```&lt;/span>&lt;span style="color:#f85149">Это&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">делает&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">каждую&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">страницу&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">интерактивной&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">посредством&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">рендеринга&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">на&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">сервере&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">Отдельные&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">компоненты&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">все&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">равно&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">могут&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">переопределить&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">это&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">если&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">это&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">необходимо&lt;/span>.&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">### Важные правила, которые следует запомнить
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">При&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">смешивании&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">режимов&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">рендеринга&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">следует&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">учитывать&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">несколько&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">ограничений&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#f85149">Дочерний&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">компонент&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">не&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">может&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">иметь&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">более&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">интерактивный&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">режим&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">рендеринга&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">чем&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">его&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">родительский&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">компонент&lt;/span>.&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">Если&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">родительский&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">компонент&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">является&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">статическим&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">дочерние&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">элементы&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">могут&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">быть&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">статическими&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">или&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">интерактивными&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">Но&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">если&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">родительским&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">элементом&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">является&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Interactive&lt;span style="color:#6e7681"> &lt;/span>Server,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">дочерним&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">элементом&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">не&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">может&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">быть&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Interactive&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#d2a8ff;font-weight:bold">WebAssembly&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>(&lt;span style="color:#f85149">он&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">должен&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">быть&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Server&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">или&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Auto).&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#f85149">Интерактивные&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">компоненты&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">не&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">могут&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">напрямую&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">использовать&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">службы&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">с&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">ограниченной&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">областью&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">действия&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">из&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">статических&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">родительских&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">элементов&lt;/span>.&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">Если&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">компонент&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">работает&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">на&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>WebAssembly,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">он&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">не&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">может&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">напрямую&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">обращаться&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">к&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">серверным&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">экземплярам&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>DbContext&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">—&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">вам&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">понадобится&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">уровень&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>API.&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">—&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#f85149">Состояние&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">не&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">переключается&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">автоматически&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">между&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">режимами&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">рендеринга&lt;/span>.&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">Если&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">компонент&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">выполняет&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">предварительную&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">отрисовку&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">на&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">сервере&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">а&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">затем&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">переключается&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">на&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>WebAssembly,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">вам&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">необходимо&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">явно&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">обрабатывать&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">сохранение&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">состояния&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">—&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">подробнее&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">об&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">этом&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">в&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">разделе&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>.NET&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">10&lt;/span>.&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">## Что нового в .NET 10
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>.NET&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">10&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">продолжает&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">подход&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">постепенного&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">улучшения&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">уделяя&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">особое&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">внимание&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">надежности&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">и&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">опыту&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">разработчиков&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">Вот&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">основные&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">моменты&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">интерактивности&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Blazor:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">### Улучшенный процесс повторного подключения
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">Если&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">вы&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">использовали&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">режим&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">интерактивного&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">сервера&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">в&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">рабочей&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">среде&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">вы&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">вероятно&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">сталкивались&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">с&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">наложением&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">повторного&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">подключения&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">—&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">моментом&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">когда&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">соединение&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>SignalR&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">разрывается&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">и&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">пользователь&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">видит&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">сообщение&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">«Повторное&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">подключение&lt;/span>...&lt;span style="color:#f85149">»&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">В&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>.NET&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">10&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">эта&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">возможность&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">становится&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">значительно&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">лучше&lt;/span>.&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">Логика&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">повторного&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">подключения&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">теперь&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">более&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">разумна&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">в&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">отношении&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">повторных&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">попыток&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">Вместо&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">фиксированного&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">интервала&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">повторных&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">попыток&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">он&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">использует&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">стратегию&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">отсрочки&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">которая&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">адаптируется&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">к&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">ситуации&lt;/span>.&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">Пользовательский&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">интерфейс&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">во&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">время&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">повторного&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">подключения&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">также&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">стал&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">более&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">совершенным&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">и&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">у&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">вас&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">есть&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">лучшие&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">возможности&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">для&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">настройки&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">```&lt;/span>razor&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!&lt;/span>&lt;span style="color:#8b949e;font-style:italic">-- In your App.razor or layout --&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>id&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;components-reconnect-modal&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;reconnect-visible&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Connection&lt;span style="color:#6e7681"> &lt;/span>lost.&lt;span style="color:#6e7681"> &lt;/span>Attempting&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">to&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>reconnect...&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;spinner&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;reconnect-failed&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Could&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">not&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>reconnect&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">to&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>the&lt;span style="color:#6e7681"> &lt;/span>server.&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button&lt;span style="color:#6e7681"> &lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;location.reload()&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Reload&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;reconnect-rejected&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Your&lt;span style="color:#6e7681"> &lt;/span>session&lt;span style="color:#6e7681"> &lt;/span>has&lt;span style="color:#6e7681"> &lt;/span>expired.&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>a&lt;span style="color:#6e7681"> &lt;/span>href&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#ff7b72">Return&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">to&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Home&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>a&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Платформа теперь также более агрессивно пытается переподключиться при временных сбоях и может более надежно восстанавливать состояние схемы. Это означает меньшее количество моментов «пожалуйста, перезагрузите страницу» для ваших пользователей.&lt;/p>
&lt;h3 id="постоянное-состояние-компонента-в-разных-режимах-рендеринга">Постоянное состояние компонента в разных режимах рендеринга&lt;/h3>
&lt;p>Это большой вопрос. В .NET 9, когда компонент выполняет предварительную отрисовку на сервере, а затем переходит в WebAssembly (или переключается между режимами отрисовки), состояние компонента теряется. Компонент эффективно повторно инициализируется, что может привести к дублированию выборок данных и мерцанию пользовательского интерфейса.&lt;/p>
&lt;p>В .NET 10 улучшена служба &lt;code>PersistentComponentState&lt;/code> для более плавной работы при переходах между режимами рендеринга:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/weather&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>rendermode InteractiveWebAssembly
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject PersistentComponentState ApplicationState
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Weather Forecast&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (forecasts is null)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Loading&lt;span style="color:#ff7b72;font-weight:bold">...&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> forecast &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> forecasts)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;forecast-card&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h4&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>forecast&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToShortDateString()&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h4&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>forecast&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Summary &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">@&lt;/span>forecast&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>TemperatureC&lt;span style="color:#f85149">°&lt;/span>C&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>WeatherForecast&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">?&lt;/span> forecasts;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private PersistingComponentStateSubscription persistingSubscription;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> persistingSubscription &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> ApplicationState&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>RegisterOnPersisting(PersistData);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>ApplicationState&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>TryTakeFromJson&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>WeatherForecast&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;gt;&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;weather-forecasts&amp;#34;&lt;/span>, out &lt;span style="color:#ff7b72">var&lt;/span> restored))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">//&lt;/span> Data wasn&lt;span style="color:#a5d6ff">&amp;#39;t persisted from prerendering — fetch it&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> forecasts &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await Http&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetFromJsonAsync&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>WeatherForecast&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;gt;&lt;/span>(&lt;span style="color:#a5d6ff">&amp;#34;api/weather&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> forecasts &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> restored;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private Task PersistData()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ApplicationState&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>PersistAsJson(&lt;span style="color:#a5d6ff">&amp;#34;weather-forecasts&amp;#34;&lt;/span>, forecasts);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Task&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public void Dispose()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> persistingSubscription&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Dispose();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Благодаря улучшениям в .NET 10 этот шаблон работает более надежно. Состояние, сериализованное во время предварительного рендеринга, правильно доступно, когда компонент инициализируется в интерактивном режиме, что позволяет избежать двойной выборки данных и кратковременного мерцания, которое могут видеть пользователи.&lt;/p>
&lt;h3 id="blazor-script-как-статический-веб-ресурс">Blazor Script как статический веб-ресурс&lt;/h3>
&lt;p>В предыдущих версиях файл JavaScript Blazor (&lt;code>blazor.web.js&lt;/code>) обслуживался из внутренней конечной точки платформы. В .NET 10 он поставляется как статический веб-ресурс. Это может показаться небольшим изменением, но оно имеет практические преимущества:– &lt;strong>Улучшенное кеширование.&lt;/strong> Статические веб-ресурсы получают правильные заголовки кэша и URL-адреса с отпечатками пальцев, поэтому браузеры кэшируют их более эффективно.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Удобство для CDN:&lt;/strong> поскольку это обычный статический файл, CDN могут кэшировать и обслуживать его из периферийных расположений.
– &lt;strong>Сжатие.&lt;/strong> Статическое сжатие веб-ресурсов применяется автоматически, уменьшая размер передаваемого сценария.&lt;/li>
&lt;/ul>
&lt;p>Вам не нужно менять способ ссылки на него — платформа автоматически обрабатывает обновленный путь.&lt;/p>
&lt;h2 id="лучшие-практики-выбор-правильного-режима-рендеринга">Лучшие практики: выбор правильного режима рендеринга&lt;/h2>
&lt;p>После работы со всеми этими режимами в нескольких проектах, вот моя схема принятия решений:&lt;/p>
&lt;h3 id="начните-со-статического-ssr">Начните со статического SSR&lt;/h3>
&lt;p>Сделайте статический SSR по умолчанию. Большинство страниц в большинстве приложений предназначены в первую очередь для отображения данных. Страницы продуктов, сообщения в блогах, профили пользователей, страницы настроек — им не нужна интерактивность в реальном времени. Статический SSR обеспечивает наилучшую производительность, минимальное использование ресурсов и простейшую мысленную модель.&lt;/p>
&lt;h3 id="добавляйте-интерактивность-только-там-где-это-необходимо">Добавляйте интерактивность только там, где это необходимо&lt;/h3>
&lt;p>Определите конкретные компоненты, которые должны реагировать на взаимодействия с пользователем в режиме реального времени. Кнопка «Мне нравится», виджет чата, интерфейс с возможностью перетаскивания — все это требует интерактивности. Но страница вокруг них, вероятно, этого не делает.&lt;/p>
&lt;h3 id="используйте-интерактивный-автоматический-режим-в-качестве-интерактивного-режима">Используйте интерактивный автоматический режим в качестве интерактивного режима&lt;/h3>
&lt;p>Если вам действительно нужна интерактивность, Interactive Auto часто является лучшим выбором по умолчанию. Он обеспечивает быструю начальную загрузку (рендеринг на сервере) с последующим выполнением на стороне клиента (WebAssembly). Пользователь получает лучшее из обоих миров, а вы пишете свой код один раз.&lt;/p>
&lt;h3 id="резервный-интерактивный-сервер-для-особых-случаев">Резервный интерактивный сервер для особых случаев&lt;/h3>
&lt;p>Используйте Interactive Server, когда:&lt;/p>
&lt;ul>
&lt;li>Вашему компоненту необходим прямой доступ к ресурсам сервера (базам данных, файловой системе, внутренним API).&lt;/li>
&lt;li>Размер загрузки WebAssembly вызывает беспокойство, и вы не можете использовать Auto.&lt;/li>
&lt;li>Вам необходимо, чтобы компонент всегда запускался на сервере из соображений безопасности (например, для обработки конфиденциальных данных).&lt;/li>
&lt;/ul>
&lt;h3 id="щедро-используйте-потоковую-ssr">Щедро используйте потоковую SSR&lt;/h3>
&lt;p>Если ваши статические страницы SSR извлекают какие-либо данные, добавьте &lt;code>[StreamRendering]&lt;/code>. Затраты минимальны, а ощутимое улучшение производительности значительно. Пользователи видят, что контент появляется постепенно, а не смотрит на пустую страницу.&lt;/p>
&lt;h3 id="осторожно-обращайтесь-с-переходами-состояний">Осторожно обращайтесь с переходами состояний&lt;/h3>
&lt;p>Если вы используете Interactive Auto или совмещаете предварительную визуализацию с WebAssembly, всегда используйте &lt;code>PersistentComponentState&lt;/code> во избежание дублирования выборки данных. Ваши пользователи скажут вам спасибо за отсутствие мерцающего контента.&lt;/p>
&lt;h3 id="помните-о-дереве-компонентов">Помните о дереве компонентов&lt;/h3>
&lt;p>Помните правила иерархии режимов рендеринга. Спланируйте дерево компонентов так, чтобы интерактивные границы имели смысл. Распространенным шаблоном является наличие статического макета с интерактивными «островками», встроенными там, где это необходимо:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@* Layout: Static SSR *@
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;header&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NavMenu /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;UserMenu @rendermode=&amp;#34;InteractiveServer&amp;#34; /&amp;gt; @* Needs real-time auth state *@
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/header&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;main&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Body
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/main&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;footer&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ChatWidget @rendermode=&amp;#34;InteractiveAuto&amp;#34; /&amp;gt; @* Rich interactivity *@
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/footer&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="заключение">Заключение&lt;/h2>
&lt;p>Модель интерактивности Blazor в .NET 9 и .NET 10 представляет собой зрелый, хорошо продуманный подход к созданию веб-приложений. Возможность выбирать режимы рендеринга для каждого компонента, улучшенная плавная навигация, потоковая SSR, а также постоянные улучшения в переподключении и управлении состоянием делают его привлекательным выбором для широкого спектра приложений.Ключевой вывод заключается в том, что &lt;strong>интерактивность — это спектр, а не бинарный выбор&lt;/strong>. Большая часть вашего приложения может быть статической. Некоторым частям требуется интерактивность, управляемая сервером. Некоторым может быть полезно работать в браузере. Blazor теперь позволяет вам сделать этот выбор с максимальной степенью детализации — отдельного компонента — без нарушения структуры.&lt;/p>
&lt;p>Если вы начинаете новый проект, мой совет прост: создайте веб-приложение Blazor, начните со статического SSR, добавьте &lt;code>[StreamRendering]&lt;/code> на страницы с большим объемом данных и переводите отдельные компоненты в интерактивные режимы только тогда, когда они вам нужны. В итоге вы получите быстрое, эффективное и хорошо масштабируемое приложение.&lt;/p>
&lt;p>Удачного программирования. Как всегда, если у вас возникнут вопросы, обращайтесь!&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category></item><item><title>Минимальные API в .NET: создание облегченных HTTP API</title><link>https://emimontesdeoca.github.io/ru/posts/minimal-apis-dotnet/</link><pubDate>Thu, 10 Apr 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/minimal-apis-dotnet/</guid><description>Подробное руководство по созданию простых и быстрых API-интерфейсов HTTP с использованием минимальных API-интерфейсов .NET — от обработчиков маршрутов и привязки параметров до фильтров и интеграции OpenAPI.</description><content:encoded>&lt;p>Если вы какое-то время создавали API с помощью ASP.NET Core, вы, вероятно, хорошо знакомы с подходом на основе контроллера: создайте класс контроллера, украсьте его атрибутами, внедрите свои сервисы через конструктор и соедините свои маршруты. Это работает, и работает хорошо, но иногда кажется, что вы пишете много церемоний, что означает «принять этот запрос, сделать что-нибудь, вернуть ответ».&lt;/p>
&lt;p>Это именно та проблема, которую призваны решить минимальные API. Минимальные API, представленные в .NET 6, позволяют определять конечные точки HTTP с помощью минимального шаблонного кода. Никаких контроллеров, никаких атрибутов, никакой манипуляции с классами запуска — только прямое сопоставление маршрутов с обработчиками в чистом, функциональном стиле.&lt;/p>
&lt;p>В этом посте я хочу познакомить вас со всем, что вам нужно знать о минимальных API: от вашей первой конечной точки до организации больших приложений, обработки аутентификации, проверки, документации OpenAPI и вопросов производительности. Давайте перейдем к этому.&lt;/p>
&lt;h2 id="почему-минимальные-api">Почему минимальные API?&lt;/h2>
&lt;p>Прежде чем мы начнем, давайте поговорим о том, &lt;em>почему&lt;/em> вы предпочитаете минимальные API традиционному подходу на основе контроллера.&lt;/p>
&lt;p>&lt;strong>Контроллеры&lt;/strong> отлично подходят, когда вам нужна структурированная, продуманная структура. Они предоставляют вам привязку модели, фильтры, согласование контента и четкое разделение задач прямо из коробки. Для крупных корпоративных приложений с десятками разработчиков согласованность, которую обеспечивают контроллеры, может оказаться реальным преимуществом.&lt;/p>
&lt;p>С другой стороны, &lt;strong>минимальные API&lt;/strong> сияют, когда вам нужно:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Меньше шаблонов&lt;/strong> — нет классов контроллера, нет атрибутов &lt;code>[ApiController]&lt;/code>, нет отдельной конфигурации запуска.&lt;/li>
&lt;li>&lt;strong>Более быстрый запуск&lt;/strong> — меньше операций, связанных с отражением, во время загрузки.&lt;/li>
&lt;li>&lt;strong>Упрощенная ментальная модель&lt;/strong> — маршрут сопоставляется непосредственно с обработчиком. Вот и все.&lt;/li>
&lt;li>&lt;strong>Удобство для микросервисов&lt;/strong> — если ваш API имеет 5–10 конечных точек, полная настройка контроллера может показаться излишним.&lt;/li>
&lt;/ul>
&lt;p>Хорошей новостью является то, что это не решение «или-или». Вы можете смешивать контроллеры и минимальные API в одном проекте. Но как только вы освоитесь с минимальным подходом, вы, возможно, обнаружите, что будете обращаться к нему чаще, чем ожидали.&lt;/p>
&lt;h2 id="начало-работы">Начало работы&lt;/h2>
&lt;p>Давайте создадим минимальный API с нуля. Если у вас установлен .NET SDK, это так же просто, как:&lt;/p>
&lt;p>[[[ТОК_1]]]&lt;/p>
&lt;p>Это дает вам &lt;code>Program.cs&lt;/code>, который выглядит примерно так:&lt;/p>
&lt;p>[[[ТОК_3]]]&lt;/p>
&lt;p>Вот и все. Это рабочий API. Нет &lt;code>Startup.cs&lt;/code>, нет класса контроллера, нет конфигурации маршрутизации. Вы запускаете &lt;code>dotnet run&lt;/code>, нажимаете &lt;code>http://localhost:5000&lt;/code> и получаете «Hello World!» назад. Вся суть здесь в простоте.&lt;/p>
&lt;p>Давайте сделаем его немного более полезным с помощью классического API todo:&lt;/p>
&lt;p>[[[ТОК_7]]]&lt;/p>
&lt;p>У нас есть полный CRUD менее чем в 30 строках. В этом сила минимального подхода.&lt;/p>
&lt;h2 id="обработчики-маршрутов">Обработчики маршрутов&lt;/h2>
&lt;p>Обработчики маршрутов — это функции, которые выполняются, когда запрос соответствует маршруту. У вас есть несколько вариантов их определения.&lt;/p>
&lt;h3 id="лямбда-выражения">Лямбда-выражения&lt;/h3>
&lt;p>Самый распространенный подход и то, что вы увидите в большинстве примеров:&lt;/p>
&lt;p>[[[ТОК_8]]]&lt;/p>
&lt;h3 id="группы-методов">Группы методов&lt;/h3>
&lt;p>Для более сложной логики вы можете указать именованный метод:&lt;/p>
&lt;p>[[[ТОК_9]]]Это мой предпочтительный подход ко всему, что выходит за рамки однострочника. Он сохраняет раздел сопоставления маршрутов чистым и читабельным — вы можете сразу увидеть, какие конечные точки существуют, не вдаваясь в подробности реализации.&lt;/p>
&lt;h3 id="локальные-функции">Локальные функции&lt;/h3>
&lt;p>Вы также можете использовать локальные функции, что полезно, если вы хотите, чтобы обработчики были близки к определениям их маршрутов:&lt;/p>
&lt;p>[[[ТОК_10]]]&lt;/p>
&lt;h2 id="привязка-параметра">Привязка параметра&lt;/h2>
&lt;p>Одна из вещей, которые мне очень нравятся в минимальных API, — это интуитивно понятное связывание параметров. Платформа определяет, откуда брать значения, в зависимости от контекста и типа.&lt;/p>
&lt;h3 id="параметры-маршрута">Параметры маршрута&lt;/h3>
&lt;p>[[[ТОК_11]]]&lt;/p>
&lt;h3 id="параметры-строки-запроса">Параметры строки запроса&lt;/h3>
&lt;p>[[[ТОК_12]]]&lt;/p>
&lt;p>Типы, допускающие значение NULL, становятся необязательными параметрами. Значения по умолчанию работают именно так, как вы ожидаете.&lt;/p>
&lt;h3 id="тело-запроса">Тело запроса&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/orders&amp;#34;&lt;/span>, (Order order) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// &amp;#39;order&amp;#39; is automatically deserialized from the JSON body&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/orders/{order.Id}&amp;#34;&lt;/span>, order);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="заголовок-и-привязка-службы">Заголовок и привязка службы&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/protected&amp;#34;&lt;/span>, (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [FromHeader(Name = &amp;#34;X-Api-Key&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> apiKey,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [FromServices] ILogger&amp;lt;Program&amp;gt; logger) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Request with API key: {Key}&amp;#34;&lt;/span>, apiKey[..&lt;span style="color:#a5d6ff">4&lt;/span>] + &lt;span style="color:#a5d6ff">&amp;#34;****&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(&lt;span style="color:#a5d6ff">&amp;#34;Authorized&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="httpcontext-и-httprequest">HttpContext и HttpRequest&lt;/h3>
&lt;p>Когда вам нужен доступ более низкого уровня:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/info&amp;#34;&lt;/span>, (HttpContext context) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> userAgent = context.Request.Headers.UserAgent.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> ip = context.Connection.RemoteIpAddress?.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(&lt;span style="color:#ff7b72">new&lt;/span> { userAgent, ip });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="пользовательская-привязка-с-помощью-bindasync">Пользовательская привязка с помощью BindAsync&lt;/h3>
&lt;p>Для сложных типов вы можете реализовать статический метод &lt;code>BindAsync&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">PaginationParams&lt;/span>(&lt;span style="color:#ff7b72">int&lt;/span> Page, &lt;span style="color:#ff7b72">int&lt;/span> PageSize)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> ValueTask&amp;lt;PaginationParams?&amp;gt; BindAsync(HttpContext context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span>.TryParse(context.Request.Query[&lt;span style="color:#a5d6ff">&amp;#34;page&amp;#34;&lt;/span>], &lt;span style="color:#ff7b72">out&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> page);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span>.TryParse(context.Request.Query[&lt;span style="color:#a5d6ff">&amp;#34;pageSize&amp;#34;&lt;/span>], &lt;span style="color:#ff7b72">out&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> pageSize);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">new&lt;/span> PaginationParams(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Page: page &amp;gt; &lt;span style="color:#a5d6ff">0&lt;/span> ? page : &lt;span style="color:#a5d6ff">1&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PageSize: pageSize &amp;gt; &lt;span style="color:#a5d6ff">0&lt;/span> ? Math.Min(pageSize, &lt;span style="color:#a5d6ff">100&lt;/span>) : &lt;span style="color:#a5d6ff">20&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> ValueTask.FromResult&amp;lt;PaginationParams?&amp;gt;(result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/items&amp;#34;&lt;/span>, (PaginationParams pagination, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> items = db.Items
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Skip((pagination.Page - &lt;span style="color:#a5d6ff">1&lt;/span>) * pagination.PageSize)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Take(pagination.PageSize)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToList();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(items);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это невероятно мощно. Вы определяете логику привязки один раз и повторно используете ее на всех своих конечных точках.&lt;/p>
&lt;h2 id="проверка">Проверка&lt;/h2>
&lt;p>Одна из областей, в которой минимальные API не дают вам столько возможностей по сравнению с контроллерами, — это проверка модели. Автоматического применения &lt;code>[Required]&lt;/code> или &lt;code>[StringLength]&lt;/code> не предусмотрено. Но есть четкие шаблоны для решения этой проблемы.&lt;/p>
&lt;h3 id="ручная-проверка">Ручная проверка&lt;/h3>
&lt;p>Самый простой подход — просто валидируем в обработчике:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/users&amp;#34;&lt;/span>, (CreateUserRequest request) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(request.Email))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Error = &lt;span style="color:#a5d6ff">&amp;#34;Email is required&amp;#34;&lt;/span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (request.Name?.Length &amp;gt; &lt;span style="color:#a5d6ff">100&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Error = &lt;span style="color:#a5d6ff">&amp;#34;Name must be 100 characters or less&amp;#34;&lt;/span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create user...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/users/{request.Email}&amp;#34;&lt;/span>, request);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="использование-библиотеки-проверки">Использование библиотеки проверки&lt;/h3>
&lt;p>Для чего-то нетривиального я бы рекомендовал использовать FluentValidation или аналогичную библиотеку:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CreateUserValidator&lt;/span> : AbstractValidator&amp;lt;CreateUserRequest&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CreateUserValidator()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RuleFor(x =&amp;gt; x.Email).NotEmpty().EmailAddress();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RuleFor(x =&amp;gt; x.Name).NotEmpty().MaximumLength(&lt;span style="color:#a5d6ff">100&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RuleFor(x =&amp;gt; x.Age).InclusiveBetween(&lt;span style="color:#a5d6ff">0&lt;/span>, &lt;span style="color:#a5d6ff">150&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Вы можете подключить это к своим конечным точкам через фильтры конечных точек, что подводит нас к следующему разделу.&lt;/p>
&lt;h2 id="фильтры-конечных-точек">Фильтры конечных точек&lt;/h2>
&lt;p>Фильтры конечных точек — одна из лучших функций минимальных API. Думайте о них как о промежуточном программном обеспечении, но ограниченном конкретными конечными точками, а не всем конвейером. Они были представлены в .NET 7 и отлично подходят для решения сквозных задач.&lt;/p>
&lt;h3 id="базовый-фильтр">Базовый фильтр&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, (Todo todo) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Handle the request&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.AddEndpointFilter(&lt;span style="color:#ff7b72">async&lt;/span> (context, next) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> todo = context.GetArgument&amp;lt;Todo&amp;gt;(&lt;span style="color:#a5d6ff">0&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(todo.Title))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Error = &lt;span style="color:#a5d6ff">&amp;#34;Title is required&amp;#34;&lt;/span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">await&lt;/span> next(context);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="фильтр-проверки-с-fluentvalidation">Фильтр проверки с FluentValidation&lt;/h3>
&lt;p>Вот где он становится действительно мощным — многоразовый фильтр проверки:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ValidationFilter&lt;/span>&amp;lt;T&amp;gt; : IEndpointFilter &lt;span style="color:#ff7b72">where&lt;/span> T : &lt;span style="color:#ff7b72">class&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IValidator&amp;lt;T&amp;gt; _validator;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ValidationFilter(IValidator&amp;lt;T&amp;gt; validator)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _validator = validator;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> ValueTask&amp;lt;&lt;span style="color:#ff7b72">object?&lt;/span>&amp;gt; InvokeAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> EndpointFilterInvocationContext context,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> EndpointFilterDelegate next)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> model = context.Arguments
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OfType&amp;lt;T&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .FirstOrDefault();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (model &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Error = &lt;span style="color:#a5d6ff">&amp;#34;Request body is required&amp;#34;&lt;/span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> _validator.ValidateAsync(model);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!result.IsValid)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> errors = result.Errors
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GroupBy(e =&amp;gt; e.PropertyName)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToDictionary(g =&amp;gt; g.Key, g =&amp;gt; g.Select(e =&amp;gt; e.ErrorMessage).ToArray());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.ValidationProblem(errors);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">await&lt;/span> next(context);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Usage&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/users&amp;#34;&lt;/span>, (CreateUserRequest request) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Only reached if validation passes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/users/{request.Email}&amp;#34;&lt;/span>, request);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.AddEndpointFilter&amp;lt;ValidationFilter&amp;lt;CreateUserRequest&amp;gt;&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="фильтр-журналов">Фильтр журналов&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>, (AppDbContext db) =&amp;gt; db.Products.ToList())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddEndpointFilter(&lt;span style="color:#ff7b72">async&lt;/span> (context, next) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> logger = context.HttpContext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .RequestServices.GetRequiredService&amp;lt;ILogger&amp;lt;Program&amp;gt;&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sw = Stopwatch.StartNew();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> next(context);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sw.Stop();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Endpoint executed in {Elapsed}ms&amp;#34;&lt;/span>, sw.ElapsedMilliseconds);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> result;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Вы можете объединить несколько фильтров, и они будут выполняться в том порядке, в котором они добавлены — точно так же, как промежуточное программное обеспечение.&lt;/p>
&lt;h2 id="интеграция-openapiswagger">Интеграция OpenAPI/Swagger&lt;/h2>
&lt;p>Хорошая документация по API больше не является обязательной. К счастью, минимальные API имеют первоклассную поддержку OpenAPI через пакет &lt;code>Microsoft.AspNetCore.OpenApi&lt;/code>.&lt;/p>
&lt;h3 id="базовая-настройка">Базовая настройка&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddOpenApi();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapOpenApi();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, () =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;Todo&amp;gt;())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithName(&lt;span style="color:#a5d6ff">&amp;#34;GetTodos&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithDescription(&lt;span style="color:#a5d6ff">&amp;#34;Retrieves all todo items&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithTags(&lt;span style="color:#a5d6ff">&amp;#34;Todos&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="расширенные-метаданные-конечной-точки">Расширенные метаданные конечной точки&lt;/h3>
&lt;p>Вы можете предоставить подробную информацию о ваших конечных точках:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, (Todo todo) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.WithName(&lt;span style="color:#a5d6ff">&amp;#34;CreateTodo&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.WithDescription(&lt;span style="color:#a5d6ff">&amp;#34;Creates a new todo item&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.WithTags(&lt;span style="color:#a5d6ff">&amp;#34;Todos&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.Accepts&amp;lt;Todo&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;application/json&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.Produces&amp;lt;Todo&amp;gt;(StatusCodes.Status201Created)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.Produces(StatusCodes.Status400BadRequest)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.ProducesValidationProblem();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="использование-withopenapi-для-настройки">Использование WithOpenApi для настройки&lt;/h3>
&lt;p>Для детального контроля над созданным документом OpenAPI:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos/{id:int}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">int&lt;/span> id) =&amp;gt; Results.Ok(&lt;span style="color:#ff7b72">new&lt;/span> Todo(id, &lt;span style="color:#a5d6ff">&amp;#34;Sample&amp;#34;&lt;/span>, &lt;span style="color:#79c0ff">false&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithOpenApi(operation =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.Summary = &lt;span style="color:#a5d6ff">&amp;#34;Get a specific todo&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.Description = &lt;span style="color:#a5d6ff">&amp;#34;Retrieves a single todo item by its unique identifier.&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.Parameters[&lt;span style="color:#a5d6ff">0&lt;/span>].Description = &lt;span style="color:#a5d6ff">&amp;#34;The unique identifier of the todo item&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> operation;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это дает вам тот же уровень управления документацией, который вы получаете с XML-комментариями Swashbuckle к контроллерам, но более явным способом, ориентированным на код.&lt;/p>
&lt;h2 id="аутентификация-и-авторизация">Аутентификация и авторизация&lt;/h2>
&lt;p>Защита конечных точек минимального API следует тем же шаблонам, что и остальная часть ASP.NET Core — вы просто применяете их по-другому.&lt;/p>
&lt;h3 id="базовая-настройкаcsharp">Базовая настройка```csharp&lt;/h3>
&lt;p>var builder = WebApplication.CreateBuilder(args);&lt;/p>
&lt;p>builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =&amp;gt;
{
options.Authority = &amp;ldquo;&lt;a href="https://your-identity-provider.com">https://your-identity-provider.com&lt;/a>&amp;rdquo;;
options.Audience = &amp;ldquo;your-api&amp;rdquo;;
});&lt;/p>
&lt;p>builder.Services.AddAuthorizationBuilder()
.AddPolicy(&amp;ldquo;AdminOnly&amp;rdquo;, policy =&amp;gt; policy.RequireRole(&amp;ldquo;Admin&amp;rdquo;))
.AddPolicy(&amp;ldquo;PremiumUser&amp;rdquo;, policy =&amp;gt; policy.RequireClaim(&amp;ldquo;subscription&amp;rdquo;, &amp;ldquo;premium&amp;rdquo;));&lt;/p>
&lt;p>var app = builder.Build();&lt;/p>
&lt;p>app.UseAuthentication();
app.UseAuthorization();&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>### Применение авторизации к конечным точкам
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>```csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>// Require any authenticated user
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&amp;#34;/profile&amp;#34;, (ClaimsPrincipal user) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> return Results.Ok(new
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = user.FindFirstValue(ClaimTypes.Name),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Email = user.FindFirstValue(ClaimTypes.Email)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}).RequireAuthorization();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>// Require a specific policy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapDelete(&amp;#34;/admin/users/{id}&amp;#34;, (int id) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> // Delete user logic
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> return Results.NoContent();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}).RequireAuthorization(&amp;#34;AdminOnly&amp;#34;);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>// Allow anonymous access
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&amp;#34;/public/health&amp;#34;, () =&amp;gt; Results.Ok(new { Status = &amp;#34;Healthy&amp;#34; }))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AllowAnonymous();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Обратите внимание, как вы можете внедрить &lt;code>ClaimsPrincipal&lt;/code> непосредственно в параметры вашего обработчика — обо всем остальном позаботится инфраструктура. Это одна из тех мелочей, благодаря которым минимальные API выглядят по-настоящему элегантно.&lt;/p>
&lt;h2 id="организация-больших-api">Организация больших API&lt;/h2>
&lt;p>Слон в комнате с минимальными API — это организация. Когда ваш &lt;code>Program.cs&lt;/code> имеет 50 конечных точек, это становится беспорядком. Вот шаблоны, которые я использую, чтобы все было управляемо.&lt;/p>
&lt;h3 id="группы-маршрутов">Группы маршрутов&lt;/h3>
&lt;p>Группы маршрутов (представленные в .NET 7) позволяют совместно использовать конфигурацию между связанными конечными точками:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> todos = app.MapGroup(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithTags(&lt;span style="color:#a5d6ff">&amp;#34;Todos&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .RequireAuthorization();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>todos.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, (AppDbContext db) =&amp;gt; db.Todos.ToListAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>todos.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/{id:int}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">int&lt;/span> id, AppDbContext db) =&amp;gt; db.Todos.FindAsync(id));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>todos.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, (Todo todo, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Add(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Все конечные точки в группе имеют общий префикс &lt;code>/todos&lt;/code>, тег &lt;code>Todos&lt;/code> и требование авторизации. Чистый.&lt;/p>
&lt;h3 id="методы-расширения">Методы расширения&lt;/h3>
&lt;p>Это модель, которая действительно масштабируется. Переместите каждую группу конечных точек в отдельный статический класс:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Endpoints/TodoEndpoints.cs&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TodoEndpoints&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> RouteGroupBuilder MapTodoEndpoints(&lt;span style="color:#ff7b72">this&lt;/span> WebApplication app)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> &lt;span style="color:#ff7b72">group&lt;/span> = app.MapGroup(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>).WithTags(&lt;span style="color:#a5d6ff">&amp;#34;Todos&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, GetAll);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/{id:int}&amp;#34;&lt;/span>, GetById);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, Create);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapPut(&lt;span style="color:#a5d6ff">&amp;#34;/{id:int}&amp;#34;&lt;/span>, Update);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapDelete(&lt;span style="color:#a5d6ff">&amp;#34;/{id:int}&amp;#34;&lt;/span>, Delete);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">group&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; GetAll(AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> =&amp;gt; Results.Ok(&lt;span style="color:#ff7b72">await&lt;/span> db.Todos.ToListAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; GetById(&lt;span style="color:#ff7b72">int&lt;/span> id, AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> =&amp;gt; &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.FindAsync(id) &lt;span style="color:#ff7b72">is&lt;/span> { } todo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? Results.Ok(todo)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : Results.NotFound();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; Create(Todo todo, AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Add(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; Update(&lt;span style="color:#ff7b72">int&lt;/span> id, Todo updated, AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> todo = &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.FindAsync(id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (todo &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>) &lt;span style="color:#ff7b72">return&lt;/span> Results.NotFound();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todo.Title = updated.Title;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todo.IsComplete = updated.IsComplete;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; Delete(&lt;span style="color:#ff7b72">int&lt;/span> id, AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> todo = &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.FindAsync(id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (todo &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>) &lt;span style="color:#ff7b72">return&lt;/span> Results.NotFound();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Remove(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.NoContent();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Program.cs — stays clean&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapTodoEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapUserEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapOrderEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ваш &lt;code>Program.cs&lt;/code> становится оглавлением вашего API. Каждая группа конечных точек находится в своем собственном файле. Именно такой подход я рекомендую для производственных приложений.&lt;/p>
&lt;h3 id="библиотека-картера">Библиотека Картера&lt;/h3>
&lt;p>Если вам нужно еще больше структуры, библиотека Carter предлагает подход на основе модулей:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TodoModule&lt;/span> : ICarterModule
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> AddRoutes(IEndpointRouteBuilder app)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.ToListAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (Todo todo, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Add(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Картер автоматически обнаруживает и регистрирует все модули. Это хорошая золотая середина между необработанным подходом минимального API и полноценными контроллерами.&lt;/p>
&lt;h2 id="типизированные-результаты-и-типы-ответов">Типизированные результаты и типы ответов&lt;/h2>
&lt;p>Начиная с .NET 7, вы можете использовать &lt;code>TypedResults&lt;/code> вместо &lt;code>Results&lt;/code> для типобезопасных ответов. Это может показаться небольшим изменением, но оно имеет реальные преимущества для документирования и тестируемости OpenAPI.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos/{id:int}&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;Results&amp;lt;Ok&amp;lt;Todo&amp;gt;, NotFound&amp;gt;&amp;gt; (&lt;span style="color:#ff7b72">int&lt;/span> id, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> todo = &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.FindAsync(id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> todo &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? TypedResults.Ok(todo)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : TypedResults.NotFound();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Тип возвращаемого значения &lt;code>Results&amp;lt;Ok&amp;lt;Todo&amp;gt;, NotFound&amp;gt;&lt;/code> явно сообщает платформе (и вашей документации OpenAPI), какие именно типы ответов может генерировать эта конечная точка. Больше никаких догадок, никаких ручных вызовов &lt;code>Produces&amp;lt;&amp;gt;()&lt;/code> для основных случаев.&lt;/p>
&lt;p>Для нескольких возможных результатов:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;Results&amp;lt;Created&amp;lt;Todo&amp;gt;, ValidationProblem, Conflict&amp;gt;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Todo todo, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(todo.Title))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> TypedResults.ValidationProblem(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>[]&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> { &lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">new&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;Title is required&amp;#34;&lt;/span> } }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">await&lt;/span> db.Todos.AnyAsync(t =&amp;gt; t.Title == todo.Title))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> TypedResults.Conflict();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Add(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> TypedResults.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Я начал использовать &lt;code>TypedResults&lt;/code> во всех новых проектах. Компилятор выявляет несоответствия между объявленными типами возвращаемых данных и тем, что вы на самом деле возвращаете, что устраняет целый класс неожиданностей во время выполнения.&lt;/p>
&lt;h2 id="вопросы-производительности">Вопросы производительности&lt;/h2>
&lt;p>Одним из преимуществ минимальных API является производительность, и стоит понять, &lt;em>почему&lt;/em> они быстрее.&lt;/p>
&lt;p>&lt;strong>Сокращение затрат при запуске.&lt;/strong> Контроллеры в значительной степени полагаются на отражение при обнаружении конечных точек, привязке моделей и применении фильтров. Минимальные API используют генераторы исходного кода (начиная с .NET 7) для создания кода привязки во время компиляции. Это означает меньше работы при запуске и меньшее выделение памяти на запрос.&lt;/p>
&lt;p>&lt;strong>Нет конвейера MVC.&lt;/strong> API на основе контроллера проходят через полный конвейер MVC: выбор действия, привязка модели, фильтры действий, выполнение результатов. Минимальные API пропускают все это и сразу переходят от маршрутизации к вашему обработчику.&lt;/p>
&lt;p>&lt;strong>Компиляция RequestDelegate.&lt;/strong> Платформа компилирует ваши лямбда-выражения в оптимизированные экземпляры &lt;code>RequestDelegate&lt;/code> . Полученный код очень близок к тому, что вы написали бы вручную, если бы работали напрямую с &lt;code>HttpContext&lt;/code>.&lt;/p>
&lt;p>Вот несколько практических советов по повышению производительности:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Use AsNoTracking for read-only queries&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.Products.AsNoTracking().ToListAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Return results directly — avoid unnecessary allocations&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/count&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.Products.CountAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Use cancellation tokens for long-running operations&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/report&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AppDbContext db, CancellationToken ct) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.Orders
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AsNoTracking()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GroupBy(o =&amp;gt; o.Status)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(g =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> { Status = g.Key, Count = g.Count() })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync(ct));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```Также&lt;/span> &lt;span style="color:#f85149">стоит&lt;/span> &lt;span style="color:#f85149">отметить&lt;/span>, &lt;span style="color:#f85149">что&lt;/span> &lt;span style="color:#f85149">разрыв&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">производительности&lt;/span> &lt;span style="color:#f85149">между&lt;/span> &lt;span style="color:#f85149">контроллерами&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">минимальными&lt;/span> API &lt;span style="color:#f85149">продолжает&lt;/span> &lt;span style="color:#f85149">сокращаться&lt;/span> &lt;span style="color:#f85149">с&lt;/span> &lt;span style="color:#f85149">каждым&lt;/span> &lt;span style="color:#f85149">выпуском&lt;/span> .NET. &lt;span style="color:#f85149">Для&lt;/span> &lt;span style="color:#f85149">большинства&lt;/span> &lt;span style="color:#f85149">приложений&lt;/span> &lt;span style="color:#f85149">разница&lt;/span> &lt;span style="color:#f85149">будет&lt;/span> &lt;span style="color:#f85149">не&lt;/span> &lt;span style="color:#f85149">вашим&lt;/span> &lt;span style="color:#f85149">узким&lt;/span> &lt;span style="color:#f85149">местом&lt;/span> &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">вашими&lt;/span> &lt;span style="color:#f85149">запросами&lt;/span> &lt;span style="color:#f85149">к&lt;/span> &lt;span style="color:#f85149">базе&lt;/span> &lt;span style="color:#f85149">данных&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">вызовами&lt;/span> &lt;span style="color:#f85149">внешних&lt;/span> &lt;span style="color:#f85149">служб&lt;/span>. &lt;span style="color:#f85149">Выбирайте&lt;/span>, &lt;span style="color:#f85149">основываясь&lt;/span> &lt;span style="color:#f85149">на&lt;/span> &lt;span style="color:#f85149">опыте&lt;/span> &lt;span style="color:#f85149">разработчиков&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">потребностях&lt;/span> &lt;span style="color:#f85149">проекта&lt;/span>, &lt;span style="color:#f85149">а&lt;/span> &lt;span style="color:#f85149">не&lt;/span> &lt;span style="color:#f85149">на&lt;/span> &lt;span style="color:#f85149">тестах&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> &lt;span style="color:#f85149">Заключение&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Минимальные&lt;/span> API &lt;span style="color:#f85149">прошли&lt;/span> &lt;span style="color:#f85149">долгий&lt;/span> &lt;span style="color:#f85149">путь&lt;/span> &lt;span style="color:#f85149">с&lt;/span> &lt;span style="color:#f85149">момента&lt;/span> &lt;span style="color:#f85149">их&lt;/span> &lt;span style="color:#f85149">появления&lt;/span> &lt;span style="color:#f85149">в&lt;/span> .NET &lt;span style="color:#a5d6ff">6.&lt;/span> &lt;span style="color:#f85149">То&lt;/span>, &lt;span style="color:#f85149">что&lt;/span> &lt;span style="color:#f85149">начиналось&lt;/span> &lt;span style="color:#f85149">как&lt;/span> &lt;span style="color:#f85149">демонстрационная&lt;/span> &lt;span style="color:#f85149">функция&lt;/span> &lt;span style="color:#f85149">«привет&lt;/span>, &lt;span style="color:#f85149">мир»&lt;/span>, &lt;span style="color:#f85149">превратилось&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">законный&lt;/span> &lt;span style="color:#f85149">выбор&lt;/span> &lt;span style="color:#f85149">для&lt;/span> &lt;span style="color:#f85149">рабочих&lt;/span> API. &lt;span style="color:#f85149">Благодаря&lt;/span> &lt;span style="color:#f85149">фильтрам&lt;/span> &lt;span style="color:#f85149">конечных&lt;/span> &lt;span style="color:#f85149">точек&lt;/span>, &lt;span style="color:#f85149">группам&lt;/span> &lt;span style="color:#f85149">маршрутов&lt;/span>, &lt;span style="color:#f85149">типизированным&lt;/span> &lt;span style="color:#f85149">результатам&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">надежной&lt;/span> &lt;span style="color:#f85149">поддержке&lt;/span> OpenAPI &lt;span style="color:#f85149">у&lt;/span> &lt;span style="color:#f85149">вас&lt;/span> &lt;span style="color:#f85149">есть&lt;/span> &lt;span style="color:#f85149">все&lt;/span> &lt;span style="color:#f85149">необходимое&lt;/span> &lt;span style="color:#f85149">для&lt;/span> &lt;span style="color:#f85149">создания&lt;/span> &lt;span style="color:#f85149">хорошо&lt;/span> &lt;span style="color:#f85149">структурированных&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">удобных&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">обслуживании&lt;/span> &lt;span style="color:#f85149">сервисов&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Моя&lt;/span> &lt;span style="color:#f85149">рекомендация&lt;/span>? &lt;span style="color:#f85149">Если&lt;/span> &lt;span style="color:#f85149">вы&lt;/span> &lt;span style="color:#f85149">начинаете&lt;/span> &lt;span style="color:#f85149">новый&lt;/span> &lt;span style="color:#f85149">проект&lt;/span> API &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">особенно&lt;/span> &lt;span style="color:#f85149">микросервис&lt;/span> &lt;span style="color:#f85149">или&lt;/span> &lt;span style="color:#f85149">специализированный&lt;/span> &lt;span style="color:#f85149">внутренний&lt;/span> API &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">серьезно&lt;/span> &lt;span style="color:#f85149">попробуйте&lt;/span> Minimal API. &lt;span style="color:#f85149">Используйте&lt;/span> &lt;span style="color:#f85149">шаблон&lt;/span> &lt;span style="color:#f85149">метода&lt;/span> &lt;span style="color:#f85149">расширения&lt;/span> &lt;span style="color:#f85149">для&lt;/span> &lt;span style="color:#f85149">организации&lt;/span>, &lt;span style="color:#f85149">используйте&lt;/span> &lt;span style="color:#f85149">фильтры&lt;/span> &lt;span style="color:#f85149">конечных&lt;/span> &lt;span style="color:#f85149">точек&lt;/span> &lt;span style="color:#f85149">для&lt;/span> &lt;span style="color:#f85149">решения&lt;/span> &lt;span style="color:#f85149">сквозных&lt;/span> &lt;span style="color:#f85149">задач&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">используйте&lt;/span> &lt;span style="color:#f85149">`&lt;/span>TypedResults&lt;span style="color:#f85149">`&lt;/span> &lt;span style="color:#f85149">для&lt;/span> &lt;span style="color:#f85149">обеспечения&lt;/span> &lt;span style="color:#f85149">безопасности&lt;/span> &lt;span style="color:#f85149">типов&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Для&lt;/span> &lt;span style="color:#f85149">существующих&lt;/span> &lt;span style="color:#f85149">проектов&lt;/span> &lt;span style="color:#f85149">на&lt;/span> &lt;span style="color:#f85149">базе&lt;/span> &lt;span style="color:#f85149">контроллеров&lt;/span> &lt;span style="color:#f85149">мигрировать&lt;/span> &lt;span style="color:#f85149">не&lt;/span> &lt;span style="color:#f85149">нужно&lt;/span>. &lt;span style="color:#f85149">Оба&lt;/span> &lt;span style="color:#f85149">подхода&lt;/span> &lt;span style="color:#f85149">работают&lt;/span> &lt;span style="color:#f85149">хорошо&lt;/span>, &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">вы&lt;/span> &lt;span style="color:#f85149">даже&lt;/span> &lt;span style="color:#f85149">можете&lt;/span> &lt;span style="color:#f85149">использовать&lt;/span> &lt;span style="color:#f85149">их&lt;/span> &lt;span style="color:#f85149">одновременно&lt;/span>. &lt;span style="color:#f85149">Но&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">следующий&lt;/span> &lt;span style="color:#f85149">раз&lt;/span>, &lt;span style="color:#f85149">когда&lt;/span> &lt;span style="color:#f85149">вам&lt;/span> &lt;span style="color:#f85149">понадобится&lt;/span> &lt;span style="color:#f85149">добавить&lt;/span> &lt;span style="color:#f85149">небольшой&lt;/span> &lt;span style="color:#f85149">сервис&lt;/span> &lt;span style="color:#f85149">или&lt;/span> &lt;span style="color:#f85149">быстрый&lt;/span> &lt;span style="color:#f85149">внутренний&lt;/span> API, &lt;span style="color:#f85149">пропустите&lt;/span> &lt;span style="color:#f85149">контроллеры&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">используйте&lt;/span> &lt;span style="color:#f85149">минимум&lt;/span>. &lt;span style="color:#f85149">Ты&lt;/span> &lt;span style="color:#f85149">можешь&lt;/span> &lt;span style="color:#f85149">не&lt;/span> &lt;span style="color:#f85149">вернуться&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Приятного&lt;/span> &lt;span style="color:#f85149">кодирования&lt;/span>!
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></content:encoded><category>.NET</category><category>API</category><category>Web Development</category></item><item><title>Использование изолированного CSS в компонентах Blazor</title><link>https://emimontesdeoca.github.io/ru/posts/blazor-isolated-css/</link><pubDate>Wed, 12 Mar 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/blazor-isolated-css/</guid><description>Примените стили CSS к отдельным компонентам Blazor, используя изоляцию CSS, чтобы избежать глобальных конфликтов стилей.</description><content:encoded>&lt;p>При работе с Blazor меня всегда беспокоило то, насколько легко случайно нарушить стили разных компонентов. Вы добавляли класс в один компонент, и внезапно что-то еще на совершенно другой странице выглядело не так. Оказывается, у Blazor есть встроенное решение для этой проблемы: изоляция CSS.&lt;/p>
&lt;h1 id="что-такое-изоляция-css">Что такое изоляция CSS?&lt;/h1>
&lt;p>Изоляция CSS позволяет ограничить стили конкретным компонентом. Blazor делает это, генерируя уникальный атрибут для каждого компонента во время сборки и добавляя его к селекторам CSS. Таким образом, ваши стили применяются только к тому компоненту, которому они принадлежат.&lt;/p>
&lt;p>Нет необходимости в БЭМ-именовании, нет необходимости в сумасшедших хаках со спецификой. Просто чистый CSS с ограниченной областью действия.&lt;/p>
&lt;h1 id="как-его-использовать">Как его использовать&lt;/h1>
&lt;p>Допустим, у вас есть компонент под названием &lt;code>Card.razor&lt;/code>:&lt;/p>
&lt;p>[[[ТОК_1]]]&lt;/p>
&lt;p>Чтобы добавить изолированные стили, вы просто создаете файл с тем же именем, но с расширением &lt;code>.razor.css&lt;/code>. В данном случае: &lt;code>Card.razor.css&lt;/code>.&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;p>И все! Blazor автоматически подберет его и распространит эти стили только на компонент &lt;code>Card&lt;/code>. Если у вас есть другой класс &lt;code>.card&lt;/code> где-то еще, это не повлияет.&lt;/p>
&lt;h1 id="как-это-работает-под-капотом">Как это работает под капотом&lt;/h1>
&lt;p>Когда вы создаете свой проект, Blazor переписывает HTML и CSS. Ваш компонент получает уникальный атрибут, например &lt;code>b-3x8qz7k2f1&lt;/code>, а селекторы CSS добавляют этот атрибут:&lt;/p>
&lt;p>[[[ТОК_8]]]&lt;/p>
&lt;p>Сгенерированный пакет CSS обслуживается как &lt;code>{ProjectName}.styles.css&lt;/code>. Убедитесь, что это есть в вашем &lt;code>index.html&lt;/code> или &lt;code>_Host.cshtml&lt;/code>:&lt;/p>
&lt;p>[[[ТОК_12]]]&lt;/p>
&lt;p>Если вам не хватает этой ссылки, ваши изолированные стили не будут отображаться, и вы потратите 30 минут, задаваясь вопросом, что происходит. Поверьте мне, я был там.&lt;/p>
&lt;h1 id="нацеливание-на-дочерние-элементы">Нацеливание на дочерние элементы&lt;/h1>
&lt;p>Следует иметь в виду, что по умолчанию изолированный CSS применяется только к элементам текущего компонента. Если вы хотите стилизовать элементы внутри дочернего компонента, вам понадобится комбинатор &lt;code>::deep&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>::&lt;span style="color:#d2a8ff;font-weight:bold">deep&lt;/span> .&lt;span style="color:#f0883e;font-weight:bold">child-element&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">color&lt;/span>: &lt;span style="color:#79c0ff">red&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это говорит Blazor применять стиль и к совпадающим элементам внутри дочерних компонентов. Очень удобно, когда у вас есть компонент-оболочка, которому нужно стилизовать своих дочерних элементов.&lt;/p>
&lt;h1 id="когда-его-использовать">Когда его использовать&lt;/h1>
&lt;p>Сейчас я использую изоляцию CSS практически для каждого компонента. Это делает вещи чистыми и предсказуемыми. Единственный раз, когда я не использую его, — это для действительно глобальных стилей, таких как сброс, типографика или переменные темы — они хранятся в общем файле CSS.&lt;/p>
&lt;p>Надеюсь, вам понравился пост! Не стесняйтесь обращаться ко мне в любой социальной сети по адресу &lt;strong>@emimontesdeoca&lt;/strong>.&lt;/p>
&lt;h1 id="ресурсы">Ресурсы&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/css-isolation">Изоляция CSS ASP.NET Core Blazor&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>CSS</category></item><item><title>Контроль рождественских расходов с помощью Semantic Kernel</title><link>https://emimontesdeoca.github.io/ru/posts/keeping-christmas-spending-with-semantic-kernel/</link><pubDate>Sat, 28 Dec 2024 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/keeping-christmas-spending-with-semantic-kernel/</guid><description>Анализируйте поступления и отслеживайте рождественские расходы с помощью семантического ядра, Azure OpenAI и Blazor.</description><content:encoded>&lt;p>﻿## Введение&lt;/p>
&lt;p>По мере приближения курортного сезона управление расходами может стать проблемой, особенно с учетом большого количества покупок и покупок подарков. В этой записи блога мы рассмотрим, как использовать искусственный интеллект, чтобы отслеживать ваши рождественские расходы с помощью технологий .NET. Анализируя квитанции с помощью семантического ядра и искусственного интеллекта, мы можем эффективно извлекать ключевые детали, такие как названия магазинов, даты, списки товаров и общие суммы. Это решение позволяет вам легко отслеживать и управлять своими рождественскими расходами, гарантируя, что вы будете оставаться в рамках своего бюджета без необходимости вручную проверять квитанции.&lt;/p>
&lt;h2 id="calendario-de-adviento-de-inteligencia-artificial-2024-на-испанском-языке">Calendario de Adviento de Inteligencia Artificial 2024 на испанском языке&lt;/h2>
&lt;p выравнивание="центр">
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsdkovc8lzahgm8pvsblk.png"/>
&lt;/p>
&lt;p>Этот проект был вдохновлен моим участием в &lt;strong>Calendario de Adviento de Inteligencia Artificial 2024 en Español&lt;/strong>, онлайн-мероприятии, посвященном искусственному интеллекту. Дополнительную информацию о мероприятии можно найти по &lt;a href="https://dev.to/roberto_navarro_mate/calendario-de-adviento-de-inteligencia-artificial-2024-en-espanol-bdb">ссылке Dev.to&lt;/a>.&lt;/p>
&lt;h2 id="проект">Проект&lt;/h2>
&lt;p>В этом проекте мы будем использовать &lt;strong>Azure OpenAI&lt;/strong>, сервис, который позволяет нам использовать мощные модели искусственного интеллекта, такие как GPT-4, для обработки и анализа изображений. Этот процесс включает в себя несколько этапов: от настройки внутренней службы API до интеграции с внешним интерфейсом Blazor для загрузки изображений. Мы также будем использовать &lt;strong>.NET Aspire&lt;/strong>, компонент, который помогает легко соединить все.&lt;/p>
&lt;h2 id="предварительные-условия">Предварительные условия&lt;/h2>
&lt;p>Прежде чем мы углубимся в код, убедитесь, что у вас есть следующие предварительные условия:&lt;/p>
&lt;ul>
&lt;li>.NET 9&lt;/li>
&lt;li>Доступ к Azure OpenAI (ключ API)&lt;/li>
&lt;li>Visual Studio или код Visual Studio&lt;/li>
&lt;li>Базовые знания Blazor, HTTP-клиентов и разработки API.&lt;/li>
&lt;/ul>
&lt;h2 id="решение-visual-studio">Решение Visual Studio&lt;/h2>
&lt;p>В конечном итоге у нас получится что-то вроде этого. Мне нравится, когда все вещи разделены и имеют классные названия, вот как это выглядит:&lt;/p>
&lt;p выравнивание="центр">
&lt;img src="https://imgur.com/gAnGLhM.png">
&lt;/p>
&lt;p>Но давайте шаг за шагом создавать вещи!&lt;/p>
&lt;h2 id="шаг-0-модели">Шаг 0: Модели&lt;/h2>
&lt;p>Ядро приложения Receipt Scanner опирается на несколько ключевых моделей, которые облегчают взаимодействие между внешним интерфейсом, API и службами искусственного интеллекта. Ниже приведены основные модели, использованные в этом проекте:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>АнализReceiptRequest&lt;/strong>&lt;br>
Эта модель представляет собой структуру запроса для анализа чека. Он содержит свойство &lt;code>ImageBytes&lt;/code>, которое содержит массив байтов изображения чека, который будет обработан.&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>ReceiptAnalyzeResult&lt;/strong>&lt;br>
Эта модель фиксирует результат после обработки квитанции. Он содержит структурированные данные, извлеченные из квитанции, такие как название магазина, дата, товары и общая сумма.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ReceiptAnalyzeResult&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime CreatedAt { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ReceiptData Result { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>Данные квитанции&lt;/strong>&lt;br>
Это модель, содержащая структурированные данные квитанции. Он включает в себя свойства названия магазина, даты, списка товаров (с названием и ценой каждого товара) и общую сумму в чеке.&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Элемент получения&lt;/strong>&lt;br>
Каждый товар в чеке представлен этой моделью. Он содержит название товара и его цену.&lt;/p>
&lt;p>[[[ТОК_7]]]Эти модели служат основой для передачи данных между клиентом и сервером, обеспечивая плавный поток информации. API получает изображение квитанции и взамен обрабатывает и возвращает структурированный объект JSON, который может быть легко использован внешним интерфейсом.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="шаг-1-настройка-внутренней-службы-api">Шаг 1. Настройка внутренней службы API&lt;/h2>
&lt;p>Первым шагом в создании этого приложения является настройка службы API для анализа изображений квитанций. Мы будем использовать API &lt;strong>Azure OpenAI&lt;/strong> для извлечения информации из изображений квитанций. Вот разбивка того, как все сочетается друг с другом:&lt;/p>
&lt;h3 id="служба-искусственного-интеллекта--глубокое-погружение">Служба искусственного интеллекта – глубокое погружение&lt;/h3>
&lt;p>Служба искусственного интеллекта лежит в основе нашей системы анализа квитанций. Он отвечает за взаимодействие с API Azure OpenAI для обработки данных изображения и получения значимой информации. Класс &lt;strong>AiApiClient&lt;/strong> — это клиент, который будет обрабатывать все взаимодействия с API Azure OpenAI.&lt;/p>
&lt;h4 id="реализация-ai-клиента">Реализация AI-клиента&lt;/h4>
&lt;p>&lt;code>AiApiClient&lt;/code> — это ключевой компонент, отвечающий за отправку изображения квитанции (в формате массива байтов) в API Azure OpenAI. Он управляет связью, регистрирует ошибки и анализирует данные:&lt;/p>
&lt;p>[[[ТОК_9]]]&lt;/p>
&lt;p>В этой части кода мы определяем метод &lt;code>AnalyzeAsync&lt;/code>, который отвечает за:&lt;/p>
&lt;ol>
&lt;li>Отправка массива байтов изображения в API Azure OpenAI.&lt;/li>
&lt;li>Обработка любых ошибок или неудачных ответов API.&lt;/li>
&lt;li>Анализ возвращенных данных JSON в структурированный результат (&lt;code>ReceiptAnalyzeResult&lt;/code>).&lt;/li>
&lt;/ol>
&lt;p>Преимущества выделения этой функции в выделенную службу (AiApiClient) включают в себя:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Обработка ошибок.&lt;/strong> Централизованная обработка ошибок, таких как проблемы с сетью или неверные ответы.&lt;/li>
&lt;li>&lt;strong>Журналирование:&lt;/strong> Надлежащее протоколирование запросов и ответов для мониторинга поведения системы.&lt;/li>
&lt;/ul>
&lt;p выравнивание="центр">
&lt;img src="https://imgur.com/q3EpCSy.png"/>
&lt;/p>
&lt;h3 id="служба-api--обработка-запросов-и-ответов">Служба API — обработка запросов и ответов&lt;/h3>
&lt;p>&lt;strong>Служба API&lt;/strong> выступает в качестве посредника между интерфейсным приложением Blazor и службой AI. Эта служба отвечает за прием данных изображения, передачу их службе AI и возврат результатов анализа клиенту.&lt;/p>
&lt;h4 id="конечная-точка-api">Конечная точка API&lt;/h4>
&lt;p>На этом этапе мы определяем простую конечную точку API, которая будет принимать изображения квитанций, пересылать их в службу AI для обработки и возвращать результаты клиенту:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">ReceiptScanner.Shared.Clients&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">ReceiptScanner.Shared.Models&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add service defaults &amp;amp; Aspire client integrations.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddServiceDefaults();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add services to the container.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddProblemDetails();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Register AiApiClient with HttpClient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddHttpClient&amp;lt;AiApiClient&amp;gt;(client =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client.BaseAddress = &lt;span style="color:#ff7b72">new&lt;/span> Uri(&lt;span style="color:#a5d6ff">&amp;#34;https+http://aiservice&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddOpenApi();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Configure the HTTP request pipeline.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseExceptionHandler();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">if&lt;/span> (app.Environment.IsDevelopment())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.MapOpenApi();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// POST endpoint to analyze receipt&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/api/analyze-receipt&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AnalyzeReceiptRequest request, AiApiClient aiApiClient, ILogger&amp;lt;Program&amp;gt; logger) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (request.ImageBytes == &lt;span style="color:#79c0ff">null&lt;/span> || request.ImageBytes.Length == &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogWarning(&lt;span style="color:#a5d6ff">&amp;#34;ImageBytes is null or empty.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#a5d6ff">&amp;#34;ImageBytes is required.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Received analyze receipt request with image bytes of length: {Length}&amp;#34;&lt;/span>, request.ImageBytes.Length);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> aiApiClient.AnalyzeAsync(request.ImageBytes);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (result == &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogWarning(&lt;span style="color:#a5d6ff">&amp;#34;Failed to analyze the receipt.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Problem(&lt;span style="color:#a5d6ff">&amp;#34;Unable to process the receipt at this time. Please try again later.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Analysis completed successfully. Result: {Result}&amp;#34;&lt;/span>, result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogError(ex, &lt;span style="color:#a5d6ff">&amp;#34;An error occurred while processing the receipt.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Problem(&lt;span style="color:#a5d6ff">&amp;#34;An error occurred while processing the receipt. Please try again later.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapDefaultEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Эта конечная точка:&lt;/p>
&lt;ol>
&lt;li>Принимает изображение квитанции как часть тела запроса.&lt;/li>
&lt;li>Вызывает метод endopint &lt;code>AiService&lt;/code> для отправки изображения в Azure OpenAI для обработки.&lt;/li>
&lt;li>Возвращает результат анализа обратно клиенту.&lt;/li>
&lt;/ol>
&lt;p выравнивание="центр">
&lt;img src="https://imgur.com/u9mQrpq.png"/>
&lt;/p>
&lt;h2 id="шаг-2-настройка-внешнего-интерфейса-blazor">Шаг 2. Настройка внешнего интерфейса Blazor&lt;/h2>
&lt;p>Теперь, когда у нас настроена серверная часть, давайте обратим внимание на &lt;strong>интерфейс Blazor&lt;/strong>. Здесь пользователи могут загружать изображения своих квитанций для анализа и видеть результаты.&lt;/p>
&lt;h3 id="реализация-страницы-blazor">Реализация страницы Blazor&lt;/h3>
&lt;p>Страница Blazor предоставляет простой интерфейс, в котором пользователи могут загружать несколько изображений квитанций, а затем просматривать результаты анализа, отображаемые в таблице. Вот код страницы:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/analyzer&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using ReceiptScanner&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Shared&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Clients
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using ReceiptScanner&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Shared&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using System&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Globalization
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject ApiServiceClient ApiClient
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject ILogger&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Program&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> Logger
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>attribute [StreamRendering]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>rendermode InteractiveServer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>PageTitle&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Receipt Analyzer&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>PageTitle&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1 &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;text-center my-4&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Receipt Analyzer&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;container&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;lead text-center mb-4&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Upload receipt images below to extract their data&lt;span style="color:#ff7b72;font-weight:bold">.&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!--&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">File&lt;/span> Upload Section &lt;span style="color:#ff7b72;font-weight:bold">--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card mb-4&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-body&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>InputFile OnChange&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;HandleFileSelected&amp;#34;&lt;/span> multiple &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;form-control mb-3&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary w-100&amp;#34;&lt;/span> &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;ProcessReceipts&amp;#34;&lt;/span> disabled&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@(!hasFiles)&amp;#34;&lt;/span> type&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;button&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>span &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@(!processing ? &amp;#34;&lt;/span>d&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>none&lt;span style="color:#a5d6ff">&amp;#34; : &amp;#34;&amp;#34;) spinner-border spinner-border-sm&amp;#34;&lt;/span> role&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;status&amp;#34;&lt;/span> aria&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>hidden&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;lt;/&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (processing)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Processing&lt;span style="color:#ff7b72;font-weight:bold">...&amp;lt;/&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Process Receipts&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!--&lt;/span> Uploaded Images Preview &lt;span style="color:#ff7b72;font-weight:bold">--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (fileBytesList&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card mb-4&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-header&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h5 &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;mb-0&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Uploaded Receipt Images&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h5&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-body&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;row&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> fileBytes &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> fileBytesList)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;col-12 col-md-4 mb-3&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>img src&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@($&amp;#34;&lt;/span>data:image&lt;span style="color:#ff7b72;font-weight:bold">/&lt;/span>jpeg;base64,{Convert&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToBase64String(fileBytes)}&lt;span style="color:#a5d6ff">&amp;#34;)&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;img-fluid rounded&amp;#34;&lt;/span> alt&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;Uploaded receipt&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!--&lt;/span> Processing Indicator &lt;span style="color:#ff7b72;font-weight:bold">--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (processing)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;alert alert-info text-center&amp;#34;&lt;/span> role&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Processing receipts&lt;span style="color:#ff7b72;font-weight:bold">...&amp;lt;/&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> Please wait &lt;span style="color:#ff7b72">while&lt;/span> we analyze the uploaded files&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!--&lt;/span> Analysis Results Section &lt;span style="color:#ff7b72;font-weight:bold">--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (analyzedReceipts &lt;span style="color:#ff7b72;font-weight:bold">!=&lt;/span> null &lt;span style="color:#ff7b72;font-weight:bold">&amp;amp;&amp;amp;&lt;/span> analyzedReceipts&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-header&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h5 &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;mb-0&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Analysis Results&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h5&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-body&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>table &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;table table-striped table-bordered&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Store&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Total&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Items&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> receipt &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> analyzedReceipts)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>(receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Store &lt;span style="color:#f85149">??&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Unknown&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>(receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Date&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString() &lt;span style="color:#f85149">??&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Unknown&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>(receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Total&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;C&amp;#34;&lt;/span>, ci) &lt;span style="color:#f85149">??&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Unknown&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ul &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;list-unstyled&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Items is &lt;span style="color:#ff7b72;font-weight:bold">not&lt;/span> null)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> item &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Items&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>li&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;lt;&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>item&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Name&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span> &lt;span style="color:#f85149">@&lt;/span>item&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Price&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;C&amp;#34;&lt;/span>, ci)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>li&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>ul&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>table&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> (processed &lt;span style="color:#ff7b72;font-weight:bold">&amp;amp;&amp;amp;&lt;/span> (analyzedReceipts &lt;span style="color:#ff7b72;font-weight:bold">==&lt;/span> null &lt;span style="color:#ff7b72;font-weight:bold">||&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>analyzedReceipts&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any()))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;alert alert-warning text-center&amp;#34;&lt;/span> role&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>No results found&lt;span style="color:#ff7b72;font-weight:bold">.&amp;lt;/&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> Please try again with different images &lt;span style="color:#ff7b72;font-weight:bold">or&lt;/span> ensure they are clear &lt;span style="color:#ff7b72;font-weight:bold">and&lt;/span> legible&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">bool&lt;/span> hasFiles;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">bool&lt;/span> processing;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">bool&lt;/span> processed;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>byte[]&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> fileBytesList &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ReceiptAnalyzeResult&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> analyzedReceipts &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CultureInfo ci &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new CultureInfo(&lt;span style="color:#a5d6ff">&amp;#34;es-es&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task HandleFileSelected(InputFileChangeEventArgs e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> try
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fileBytesList&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> foreach (&lt;span style="color:#ff7b72">var&lt;/span> file &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> e&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetMultipleFiles())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> memoryStream &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new MemoryStream();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> await file&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>OpenReadStream()&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>CopyToAsync(memoryStream);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fileBytesList&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Add(memoryStream&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToArray());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> hasFiles &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> fileBytesList&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> catch (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Logger&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>LogError(ex, &lt;span style="color:#a5d6ff">&amp;#34;Error while handling file upload.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task ProcessReceipts()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>hasFiles)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> processing &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> true;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analyzedReceipts&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> try
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> foreach (&lt;span style="color:#ff7b72">var&lt;/span> fileBytes &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> fileBytesList)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ApiClient&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>AnalyzeReceiptAsync(fileBytes);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (result &lt;span style="color:#ff7b72;font-weight:bold">!=&lt;/span> null)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analyzedReceipts&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Add(result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> catch (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Logger&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>LogError(ex, &lt;span style="color:#a5d6ff">&amp;#34;Error while processing receipts.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> finally
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> processing &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> false;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> processed &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> true;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```Эта&lt;/span> &lt;span style="color:#f85149">страница&lt;/span> &lt;span style="color:#f85149">позволяет&lt;/span> &lt;span style="color:#f85149">пользователям&lt;/span> &lt;span style="color:#f85149">загружать&lt;/span> &lt;span style="color:#f85149">квитанции&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">отображает&lt;/span> &lt;span style="color:#f85149">результаты&lt;/span> &lt;span style="color:#f85149">анализа&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">таблице&lt;/span> &lt;span style="color:#f85149">с&lt;/span> &lt;span style="color:#f85149">названиями&lt;/span> &lt;span style="color:#f85149">магазинов&lt;/span>, &lt;span style="color:#f85149">датами&lt;/span>, &lt;span style="color:#f85149">общими&lt;/span> &lt;span style="color:#f85149">суммами&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">списком&lt;/span> &lt;span style="color:#f85149">товаров&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p &lt;span style="color:#f85149">выравнивание&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;центр&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>img src&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;https://imgur.com/BLswKhm.png&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">## Шаг 3: .NET Aspire&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p &lt;span style="color:#f85149">выравнивание&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;центр&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>img src&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;https://imgur.com/ja56RWN.png&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">### Что такое .NET Aspire?&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NET Aspire &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">это&lt;/span> &lt;span style="color:#f85149">набор&lt;/span> &lt;span style="color:#f85149">мощных&lt;/span> &lt;span style="color:#f85149">инструментов&lt;/span>, &lt;span style="color:#f85149">шаблонов&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">пакетов&lt;/span> &lt;span style="color:#f85149">для&lt;/span> &lt;span style="color:#f85149">создания&lt;/span> &lt;span style="color:#f85149">наблюдаемых&lt;/span>, &lt;span style="color:#f85149">готовых&lt;/span> &lt;span style="color:#f85149">к&lt;/span> &lt;span style="color:#f85149">использованию&lt;/span> &lt;span style="color:#f85149">приложений&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NET Aspire &lt;span style="color:#f85149">поставляется&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">виде&lt;/span> &lt;span style="color:#f85149">набора&lt;/span> &lt;span style="color:#f85149">пакетов&lt;/span> NuGet, &lt;span style="color:#f85149">которые&lt;/span> &lt;span style="color:#f85149">решают&lt;/span> &lt;span style="color:#f85149">специфические&lt;/span> &lt;span style="color:#f85149">проблемы&lt;/span> &lt;span style="color:#f85149">облачной&lt;/span> &lt;span style="color:#f85149">среды&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#f85149">Облачные&lt;/span> &lt;span style="color:#f85149">приложения&lt;/span> &lt;span style="color:#f85149">часто&lt;/span> &lt;span style="color:#f85149">состоят&lt;/span> &lt;span style="color:#f85149">из&lt;/span> &lt;span style="color:#f85149">небольших&lt;/span> &lt;span style="color:#f85149">взаимосвязанных&lt;/span> &lt;span style="color:#f85149">частей&lt;/span> &lt;span style="color:#f85149">или&lt;/span> &lt;span style="color:#f85149">микросервисов&lt;/span>, &lt;span style="color:#f85149">а&lt;/span> &lt;span style="color:#f85149">не&lt;/span> &lt;span style="color:#f85149">из&lt;/span> &lt;span style="color:#f85149">единой&lt;/span> &lt;span style="color:#f85149">монолитной&lt;/span> &lt;span style="color:#f85149">базы&lt;/span> &lt;span style="color:#f85149">кода&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#f85149">Облачные&lt;/span> &lt;span style="color:#f85149">приложения&lt;/span> &lt;span style="color:#f85149">обычно&lt;/span> &lt;span style="color:#f85149">используют&lt;/span> &lt;span style="color:#f85149">большое&lt;/span> &lt;span style="color:#f85149">количество&lt;/span> &lt;span style="color:#f85149">сервисов&lt;/span>, &lt;span style="color:#f85149">таких&lt;/span> &lt;span style="color:#f85149">как&lt;/span> &lt;span style="color:#f85149">базы&lt;/span> &lt;span style="color:#f85149">данных&lt;/span>, &lt;span style="color:#f85149">обмен&lt;/span> &lt;span style="color:#f85149">сообщениями&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">кэширование&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#f85149">Информацию&lt;/span> &lt;span style="color:#f85149">о&lt;/span> &lt;span style="color:#f85149">поддержке&lt;/span> &lt;span style="color:#f85149">см&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">Политике&lt;/span> &lt;span style="color:#f85149">поддержки&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NET Aspire&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Распределенное&lt;/span> &lt;span style="color:#f85149">приложение&lt;/span> &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">это&lt;/span> &lt;span style="color:#f85149">приложение&lt;/span>, &lt;span style="color:#f85149">которое&lt;/span> &lt;span style="color:#f85149">использует&lt;/span> &lt;span style="color:#f85149">вычислительные&lt;/span> &lt;span style="color:#f85149">ресурсы&lt;/span> &lt;span style="color:#f85149">на&lt;/span> &lt;span style="color:#f85149">нескольких&lt;/span> &lt;span style="color:#f85149">узлах&lt;/span>, &lt;span style="color:#f85149">например&lt;/span> &lt;span style="color:#f85149">контейнерах&lt;/span>, &lt;span style="color:#f85149">работающих&lt;/span> &lt;span style="color:#f85149">на&lt;/span> &lt;span style="color:#f85149">разных&lt;/span> &lt;span style="color:#f85149">хостах&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#f85149">Такие&lt;/span> &lt;span style="color:#f85149">узлы&lt;/span> &lt;span style="color:#f85149">должны&lt;/span> &lt;span style="color:#f85149">обмениваться&lt;/span> &lt;span style="color:#f85149">данными&lt;/span> &lt;span style="color:#f85149">через&lt;/span> &lt;span style="color:#f85149">границы&lt;/span> &lt;span style="color:#f85149">сети&lt;/span>, &lt;span style="color:#f85149">чтобы&lt;/span> &lt;span style="color:#f85149">доставлять&lt;/span> &lt;span style="color:#f85149">ответы&lt;/span> &lt;span style="color:#f85149">пользователям&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#f85149">Облачное&lt;/span> &lt;span style="color:#f85149">приложение&lt;/span> &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">это&lt;/span> &lt;span style="color:#f85149">особый&lt;/span> &lt;span style="color:#f85149">тип&lt;/span> &lt;span style="color:#f85149">распределенного&lt;/span> &lt;span style="color:#f85149">приложения&lt;/span>, &lt;span style="color:#f85149">которое&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">полной&lt;/span> &lt;span style="color:#f85149">мере&lt;/span> &lt;span style="color:#f85149">использует&lt;/span> &lt;span style="color:#f85149">преимущества&lt;/span> &lt;span style="color:#f85149">масштабируемости&lt;/span>, &lt;span style="color:#f85149">устойчивости&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">управляемости&lt;/span> &lt;span style="color:#f85149">облачных&lt;/span> &lt;span style="color:#f85149">инфраструктур&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Использование&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">**.&lt;/span>NET Aspire&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span> &lt;span style="color:#f85149">для&lt;/span> &lt;span style="color:#f85149">этого&lt;/span> &lt;span style="color:#f85149">проекта&lt;/span> &lt;span style="color:#f85149">дает&lt;/span> &lt;span style="color:#f85149">ряд&lt;/span> &lt;span style="color:#f85149">преимуществ&lt;/span>, &lt;span style="color:#f85149">улучшающих&lt;/span> &lt;span style="color:#f85149">общее&lt;/span> &lt;span style="color:#f85149">качество&lt;/span> &lt;span style="color:#f85149">системы&lt;/span>, &lt;span style="color:#f85149">например&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">### 1. **Централизованное ведение журнала**&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NET Aspire &lt;span style="color:#f85149">автоматически&lt;/span> &lt;span style="color:#f85149">интегрирует&lt;/span> &lt;span style="color:#f85149">ведение&lt;/span> &lt;span style="color:#f85149">журнала&lt;/span> &lt;span style="color:#f85149">во&lt;/span> &lt;span style="color:#f85149">всем&lt;/span> &lt;span style="color:#f85149">приложении&lt;/span>, &lt;span style="color:#f85149">что&lt;/span> &lt;span style="color:#f85149">означает&lt;/span>, &lt;span style="color:#f85149">что&lt;/span> &lt;span style="color:#f85149">вам&lt;/span> &lt;span style="color:#f85149">не&lt;/span> &lt;span style="color:#f85149">придется&lt;/span> &lt;span style="color:#f85149">вручную&lt;/span> &lt;span style="color:#f85149">настраивать&lt;/span> &lt;span style="color:#f85149">ведение&lt;/span> &lt;span style="color:#f85149">журнала&lt;/span> &lt;span style="color:#f85149">для&lt;/span> &lt;span style="color:#f85149">каждой&lt;/span> &lt;span style="color:#f85149">службы&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span> &lt;span style="color:#f85149">Это&lt;/span> &lt;span style="color:#f85149">гарантирует&lt;/span> &lt;span style="color:#f85149">согласованность&lt;/span> &lt;span style="color:#f85149">журналов&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">их&lt;/span> &lt;span style="color:#f85149">централизованное&lt;/span> &lt;span style="color:#f85149">хранение&lt;/span>, &lt;span style="color:#f85149">что&lt;/span> &lt;span style="color:#f85149">значительно&lt;/span> &lt;span style="color:#f85149">упрощает&lt;/span> &lt;span style="color:#f85149">отладку&lt;/span> &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">мониторинг&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">Например&lt;/span>, &lt;span style="color:#f85149">класс&lt;/span> &lt;span style="color:#f85149">`&lt;/span>AiApiClient&lt;span style="color:#f85149">`&lt;/span> &lt;span style="color:#f85149">использует&lt;/span> &lt;span style="color:#f85149">журналирование&lt;/span> &lt;span style="color:#f85149">для&lt;/span> &lt;span style="color:#f85149">записи&lt;/span> &lt;span style="color:#f85149">байтов&lt;/span> &lt;span style="color:#f85149">изображения&lt;/span>, &lt;span style="color:#f85149">отправленных&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">службу&lt;/span> AI, &lt;span style="color:#f85149">ответов&lt;/span> API &lt;span style="color:#f85149">и&lt;/span> &lt;span style="color:#f85149">любых&lt;/span> &lt;span style="color:#f85149">ошибок&lt;/span>, &lt;span style="color:#f85149">возникающих&lt;/span> &lt;span style="color:#f85149">в&lt;/span> &lt;span style="color:#f85149">процессе&lt;/span> &lt;span style="color:#f85149">анализа&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>_logger&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Sending analyze request with image bytes of length: {Length}&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> imageBytes&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Length);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p выравнивание="центр">
&lt;img src="https://imgur.com/5NS416X.png">
&lt;/p>
&lt;h3 id="2-автоматический-сбор-показателей">2. &lt;strong>Автоматический сбор показателей&lt;/strong>&lt;/h3>
&lt;p>.NET Aspire также автоматически отслеживает и сообщает важные показатели приложения, такие как время ответа, количество запросов и частота ошибок. Это поможет вам понять, как работает приложение, и быстро обнаружить любые узкие места или проблемы.&lt;/p>
&lt;p выравнивание="центр">
&lt;img src="https://imgur.com/SGawOY3.png">
&lt;/p>
&lt;h3 id="3-улучшенная-производительность">3. &lt;strong>Улучшенная производительность&lt;/strong>&lt;/h3>
&lt;p>.NET Aspire оптимизирует HTTP-вызовы, что помогает сократить время ответа и сократить ненужное потребление ресурсов. Он предоставляет такие функции, как объединение пулов соединений, повторные попытки запросов и интеллектуальную маршрутизацию.&lt;/p>
&lt;h3 id="4-бесшовная-интеграция">4. &lt;strong>Бесшовная интеграция&lt;/strong>&lt;/h3>
&lt;p>.NET Aspire упрощает интеграцию различных сервисов (например, сервисов искусственного интеллекта и API в этом проекте) и оптимизирует процесс развертывания. Вам не нужно беспокоиться о низкоуровневых конфигурациях, поскольку Aspire позаботится о задачах, связанных с инфраструктурой, за вас.&lt;/p>
&lt;p выравнивание="центр">
&lt;img src="https://imgur.com/OSHhWVb.png">
&lt;/p>
&lt;h3 id="заключениеии-больше-не-является-просто-модным-словечком-или-чем-то-что-мы-видим-в-научно-фантастических-фильмах-сегодня-он-активно-решает-реальные-проблемы-подобные-той-которую-мы-решали-в-этом-проекте--извлечение-структурированных-данных-из-квитанций-с-помощью-azure-openai-net-aspire-и-blazor-мы-можем-автоматизировать-то-что-в-противном-случае-было-бы-трудоемким-и-подверженным-ошибкам-ручным-заданием-ии-не-просто-общается-или-отвечает-на-запросы-такие-как-chatgpt-он-интерпретирует-изображения-извлекает-ценную-информацию-и-дает-нам-ценную-информацию-за-считанные-секунды">ЗаключениеИИ больше не является просто модным словечком или чем-то, что мы видим в научно-фантастических фильмах. Сегодня он активно решает реальные проблемы, подобные той, которую мы решали в этом проекте — извлечение структурированных данных из квитанций. С помощью &lt;strong>Azure OpenAI&lt;/strong>, &lt;strong>.NET Aspire&lt;/strong> и &lt;strong>Blazor&lt;/strong> мы можем автоматизировать то, что в противном случае было бы трудоемким и подверженным ошибкам ручным заданием. ИИ не просто общается или отвечает на запросы, такие как ChatGPT; он интерпретирует изображения, извлекает ценную информацию и дает нам ценную информацию за считанные секунды.&lt;/h3>
&lt;p>Используя &lt;strong>Azure OpenAI&lt;/strong> для анализа квитанций и &lt;strong>.NET Aspire&lt;/strong> для плавной интеграции с журналами и метриками, мы создали мощное и масштабируемое решение. Потенциал искусственного интеллекта для оптимизации бизнес-процессов, автоматизации утомительных задач и повышения точности огромен, и это лишь один пример того, как его можно применить.&lt;/p>
&lt;p>Этот пост является частью &lt;strong>Calendario de Adviento de Inteligencia Artificial 2024 en Español&lt;/strong> — мероприятия, которое демонстрирует реальные приложения искусственного интеллекта и знакомит испаноязычное технологическое сообщество с последними тенденциями. Если вы хотите глубже погрузиться в искусственный интеллект и его возможности, это мероприятие — отличное место для начала.&lt;/p>
&lt;p>ИИ меняет то, как мы работаем, и этот проект — лишь проблеск того, что возможно. Настоящая сила ИИ заключается в его способности решать реальные проблемы — будь то обработка квитанций, анализ изображений или прогнозирование тенденций. Мы только царапаем поверхность.&lt;/p>
&lt;h3 id="исходный-код">Исходный код&lt;/h3>
&lt;p>Полный исходный код этого проекта доступен на &lt;a href="https://github.com/emimontesdeoca/ReceiptScannerPoc">GitHub&lt;/a>. Не стесняйтесь загружать его, изучать, как службы искусственного интеллекта и API работают вместе, и адаптировать его для своих собственных сценариев использования. Если у вас возникнут какие-либо проблемы или у вас есть идеи по улучшению, не стесняйтесь создавать проблему или отправлять запрос на включение. Вклад всегда приветствуется, а ваши отзывы помогут сделать этот проект еще лучше!&lt;/p>
&lt;p>Приятного кодирования!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category><category>Azure</category><category>NuGet</category><category>Docker</category><category>AI</category></item><item><title>Используете контейнеры, чтобы... отслеживать цены на рождественские подарки?</title><link>https://emimontesdeoca.github.io/ru/posts/monitoring-prices-containers/</link><pubDate>Thu, 12 Dec 2024 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/monitoring-prices-containers/</guid><description>Автоматизируйте мониторинг цен на рождественские подарки с помощью контейнеров Docker, Blazor и серверной части .NET API.</description><content:encoded>&lt;p>Рождество не за горами, а вместе с ним и радостная задача найти идеальные подарки для близких. Если вы чем-то похожи на меня, вы, вероятно, любите заключать выгодные сделки, но ориентироваться в стремительно растущих ценах на популярные товары во время курортного сезона может показаться настоящей поездкой на санях — захватывающей, но немного ошеломляющей! В этом году вместо того, чтобы сводить себя с ума ежедневными проверками цен, я решил найти хорошее применение своим техническим навыкам и автоматизировать процесс.&lt;/p>
&lt;p выравнивание="центр">
&lt;img src="https://imgur.com/7K7QC08.png" />
&lt;/p>
&lt;p>Как разработчик программного обеспечения и человек, увлеченный использованием технологий для решения повседневных проблем, я нашел способ автоматизировать процесс проверки цен. Вместо того, чтобы каждый день вручную посещать несколько интернет-магазинов для сравнения цен, я решил создать систему, которая сможет сделать это за меня. Это не только экономит время, но и гарантирует, что я получу лучшие предложения без стресса от ежедневных проверок.&lt;/p>
&lt;h2 id="зачем-автоматизировать-мониторинг-цен">Зачем автоматизировать мониторинг цен?&lt;/h2>
&lt;p>Автоматизировать мониторинг цен — это все равно, что ваша собственная команда эльфов Санты работает круглосуточно, чтобы гарантировать, что вы получите лучшие предложения, не пошевелив и пальцем. Вот почему вам это понравится:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Дорогие предметы&lt;/strong>. Мы все знаем, что рождественские подарки могут оказаться довольно дорогими, особенно если в вашем списке есть гаджеты и игрушки. Экономия на них может быть столь же приятной, как и поиск последней вершины дерева на складе.&lt;/li>
&lt;li>&lt;strong>Ежедневные проверки&lt;/strong>. У кого есть время (или терпение) проверять цены каждый день? Не я!&lt;/li>
&lt;li>&lt;strong>Автоматизация&lt;/strong>. Ощутите праздничный дух щедрости — подарите себе эффективность.&lt;/li>
&lt;li>&lt;strong>Точность&lt;/strong>. Автоматизация гарантирует, что ваши проверки цен будут такими же точными, как у Рудольфа блестящий нос.&lt;/li>
&lt;/ol>
&lt;h2 id="основные-технологии">Основные технологии&lt;/h2>
&lt;p>Чтобы создать своего маленького помощника Санты, я решил использовать несколько ключевых технологий:&lt;/p>
&lt;h3 id="контейнеры">Контейнеры&lt;/h3>
&lt;p>Контейнеры — это своего рода рождественская упаковка для программного обеспечения. Они объединяют ваши приложения со всеми их вкусностями (зависимостями), так что все работает так же гладко, как поездка на санях по свежему снегу. Docker — наш инструмент для создания таких контейнеров.&lt;/p>
&lt;h3 id="блазор">Блазор&lt;/h3>
&lt;p>Blazor — это отличная платформа для создания интерактивных веб-приложений с использованием .NET. Это все равно что заменить обычные рождественские гимны собственным праздничным плейлистом — адаптированным, эффективным и очень веселым.&lt;/p>
&lt;h3 id="docker-compose">Docker-Compose&lt;/h3>
&lt;p>Docker-Compose — менеджер по эксплуатации нашего Северного полюса. Это помогает нам поддерживать идеальную совместную работу всех наших сервисов, таких как API и интерфейс Blazor. Думайте о нем как о дирижере нашей праздничной симфонии.&lt;/p>
&lt;h2 id="пошаговое-руководство">Пошаговое руководство&lt;/h2>
&lt;p>А теперь давайте окунемся в мастерскую Санты и воплотим этот проект в жизнь!&lt;/p>
&lt;h3 id="шаг-1-создание-api">Шаг 1. Создание API&lt;/h3>
&lt;h4 id="11-настройка-проекта">1.1 Настройка проекта&lt;/h4>
&lt;p>Наденьте шляпу Санты-кодировщика и настройте новый проект веб-API ASP.NET Core. Откройте терминал (это немного похоже на открытие адвент-календаря) и запустите:&lt;/p>
&lt;p>[[[ТОК_1]]]&lt;/p>
&lt;p>Эта команда создает новый каталог с именем &lt;code>PriceMonitorApi&lt;/code> и настраивает базовый проект веб-API. Представьте, что это похоже на создание прочной основы для пряничного домика.&lt;/p>
&lt;h4 id="12-добавление-httpclient-и-библиотек-парсингазатем-добавьте-httpclient-и-библиотеку-для-анализа-html-это-будут-наши-верные-сани-для-получения-и-чтения-данных-о-ценах">1.2 Добавление HttpClient и библиотек парсингаЗатем добавьте &lt;code>HttpClient&lt;/code> и библиотеку для анализа HTML. Это будут наши верные сани для получения и чтения данных о ценах.&lt;/h4>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;p>HtmlAgilityPack — это эльф, который помогает нам анализировать HTML-документы.&lt;/p>
&lt;h4 id="13-создание-моделей">1.3 Создание моделей&lt;/h4>
&lt;p>Модели — это как чертежи наших игрушек. Давайте создадим модель для представления информации о продукте:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorApi.Models&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ProductInfo&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Title { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal&lt;/span> Price { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime Date { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Мы только что создали идеальный шаблон для наших данных.&lt;/p>
&lt;h4 id="14-реализация-службы-парсера">1.4 Реализация службы парсера&lt;/h4>
&lt;p>Наш парсерный сервис похож на маленького помощника Санты: он собирает и обрабатывает для нас информацию:&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;p>Этот фрагмент превращает наш &lt;code>HtmlAgilityPack&lt;/code> в праздничную волшебную палочку.&lt;/p>
&lt;h4 id="15-создание-api-контроллера">1.5 Создание API-контроллера&lt;/h4>
&lt;p>Давайте создадим контроллер, который будет действовать как привратник наших данных:&lt;/p>
&lt;p>[[[ТОК_8]]]&lt;/p>
&lt;p>Наш контроллер обеспечивает доставку данных быстрее, чем Санта в дымоход.&lt;/p>
&lt;h4 id="16-регистрация-сервисов-в-di-контейнере">1.6 Регистрация сервисов в DI-контейнере&lt;/h4>
&lt;p>Наконец, зарегистрируйте свой &lt;code>ScraperService&lt;/code>, чтобы он был доступен при необходимости.&lt;/p>
&lt;p>[[[ТОК_10]]]&lt;/p>
&lt;p>Теперь ваш API готов — как сани Санты в канун Рождества!&lt;/p>
&lt;hr>
&lt;h3 id="шаг-2-создание-приложения-blazor">Шаг 2. Создание приложения Blazor&lt;/h3>
&lt;p>Blazor помогает нам украсить наш проект как рождественскую елку, сделав его визуально привлекательным и интерактивным.&lt;/p>
&lt;h4 id="21-настройка-проекта-blazor">2.1 Настройка проекта Blazor&lt;/h4>
&lt;p>Далее мы создадим проект Blazor, который будет служить интерфейсом для нашей поездки на санях для мониторинга цен.&lt;/p>
&lt;p>[[[ТОК_11]]]&lt;/p>
&lt;p>Эта команда добавляет немного праздничного волшебства для настройки базового проекта Blazor WebAssembly.&lt;/p>
&lt;h4 id="22-добавление-моделей">2.2 Добавление моделей&lt;/h4>
&lt;p>Так же, как и при настройке орнаментов, добавьте модель &lt;code>ProductInfo&lt;/code> в проект Blazor:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Models&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ProductInfo&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Title { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal&lt;/span> Price { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime Date { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="23-создание-службы-для-вызовов-api">2.3 Создание службы для вызовов API&lt;/h4>
&lt;p>Создайте сервис для получения данных из нашего API — думайте о нем как о нашем приятеле по онлайн-шоппингу:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Models&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Net.Http&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Net.Http.Json&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Threading.Tasks&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ScraperService&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ScraperService(HttpClient httpClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ProductInfo&amp;gt; GetProductInfoAsync(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _httpClient.GetFromJsonAsync&amp;lt;ProductInfo&amp;gt;(&lt;span style="color:#a5d6ff">$&amp;#34;https://localhost:5001/api/scraper/productinfo?url={url}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="24-регистрация-служб-в-di-контейнере">2.4 Регистрация служб в DI-контейнере&lt;/h4>
&lt;p>Убедитесь, что &lt;code>ScraperService&lt;/code> зарегистрирован, чтобы мы могли внедрить его в наши компоненты.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;ScraperService&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="25-создание-компонента-blazor">2.5 Создание компонента Blazor&lt;/h3>
&lt;p>Обновите &lt;code>Pages/Index.razor&lt;/code> для включения интересного и интерактивного интерфейса:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using PriceMonitorBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using PriceMonitorBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Services
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject ScraperService ScraperService
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Price Monitor&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>label &lt;span style="color:#ff7b72">for&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;urlInput&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Product URL:&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>label&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>input id&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;urlInput&amp;#34;&lt;/span> &lt;span style="color:#f85149">@&lt;/span>bind&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;productUrl&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;FetchProductInfo&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Fetch Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>table &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;table&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Title&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> product &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> productInfos)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Title&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>table&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private string productUrl;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ProductInfo&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> productInfos &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ProductInfo&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task FetchProductInfo()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>string&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>IsNullOrEmpty(productUrl))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> productInfo &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ScraperService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetProductInfoAsync(productUrl);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Add(productInfo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это похоже на праздничную презентацию — сделать наше приложение интерактивным и восхитительным.&lt;/p>
&lt;hr>
&lt;h3 id="шаг-3-соединяем-все-с-помощью-docker-compose">Шаг 3. Соединяем все с помощью Docker-Compose&lt;/h3>
&lt;p>Теперь давайте соединим все с помощью Docker-Compose, превратив наш проект в отлаженную поездку на санях.&lt;/p>
&lt;h4 id="31-создание-dockerfiles">3.1 Создание Dockerfiles&lt;/h4>
&lt;p>Создайте файлы Dockerfile для проектов API и Blazor:&lt;/p>
&lt;p>&lt;strong>PriceMonitorApi/Dockerfile&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> mcr.microsoft.com/dotnet/aspnet:6.0 AS base&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /app&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">EXPOSE&lt;/span>&lt;span style="color:#a5d6ff"> 80&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">EXPOSE&lt;/span>&lt;span style="color:#a5d6ff"> 443&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> mcr.microsoft.com/dotnet/sdk:6.0 AS build&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /src&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">[&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi/PriceMonitorApi.csproj&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi/&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">]&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet restore &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi/PriceMonitorApi.csproj&amp;#34;&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> . .&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> &amp;#34;/src/PriceMonitorApi&amp;#34;&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet build &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi.csproj&amp;#34;&lt;/span> -c Release -o /app/build&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> build AS publish&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet publish &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi.csproj&amp;#34;&lt;/span> -c Release -o /app/publish&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> base AS final&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /app&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> --from&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>publish /app/publish .&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">ENTRYPOINT&lt;/span> [&lt;span style="color:#a5d6ff">&amp;#34;dotnet&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi.dll&amp;#34;&lt;/span>]&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>PriceMonitorBlazor/Dockerfile&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> mcr.microsoft.com/dotnet/aspnet:6.0 AS base&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /app&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">EXPOSE&lt;/span>&lt;span style="color:#a5d6ff"> 80&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">EXPOSE&lt;/span>&lt;span style="color:#a5d6ff"> 443&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> mcr.microsoft.com/dotnet/sdk:6.0 AS build&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /src&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">[&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor/PriceMonitorBlazor.csproj&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor/&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">]&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet restore &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor/PriceMonitorBlazor.csproj&amp;#34;&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> . .&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> &amp;#34;/src/PriceMonitorBlazor&amp;#34;&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet build &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor.csproj&amp;#34;&lt;/span> -c Release -o /app/build&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> build AS publish&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet publish &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor.csproj&amp;#34;&lt;/span> -c Release -o /app/publish&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> base AS final&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /app&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> --from&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>publish /app/publish .&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">ENTRYPOINT&lt;/span> [&lt;span style="color:#a5d6ff">&amp;#34;dotnet&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor.dll&amp;#34;&lt;/span>]&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="32-создание-docker-composeyml">3.2 Создание docker-compose.yml&lt;/h4>
&lt;p>Соедините все точки (рождественские гирлянды) одним файлом &lt;code>docker-compose.yml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">version&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#39;3.4&amp;#39;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">services&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">api&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">image&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">pricemonitorapi&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">build&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">context&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">./PriceMonitorApi&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">dockerfile&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Dockerfile&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">ports&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">&amp;#34;5000:80&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">networks&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">price-monitor-network&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">blazor&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">image&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">pricemonitorblazor&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">build&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">context&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">./PriceMonitorBlazor&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">dockerfile&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Dockerfile&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">ports&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">&amp;#34;5001:80&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">depends_on&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">api&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">networks&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">price-monitor-network&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">networks&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">price-monitor-network&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">driver&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">bridge&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="диаграммы">Диаграммы&lt;/h3>
&lt;p>Ниже приведены диаграммы, помогающие визуализировать различные компоненты и поток данных. Это поможет нам понять, что на самом деле происходит в нашем мире Санты:&lt;/p>
&lt;h4 id="схема-архитектуры-системы">Схема архитектуры системы&lt;/h4>
&lt;p>&lt;img src="https://imgur.com/nE0hSJ4.png" alt="Изображение">&lt;/p>
&lt;h4 id="схема-потока-данных">Схема потока данных&lt;/h4>
&lt;p>&lt;img src="https://imgur.com/hJXv4Jw.png" alt="Изображение">&lt;/p>
&lt;h4 id="диаграмма-последовательности">Диаграмма последовательности&lt;/h4>
&lt;p>&lt;img src="https://imgur.com/FH6VzuH.png" alt="Изображение">&lt;/p>
&lt;h4 id="схема-компонентов-для-настройки-docker">Схема компонентов для настройки Docker&lt;/h4>
&lt;p>&lt;img src="https://imgur.com/efQ9WuT.png" alt="Изображение">&lt;/p>
&lt;h3 id="шаг-4-управление-url-адресами-обновление-и-автоматическое-периодическое-обновлениев-этой-демонстрации-мы-просто-сохраним-его-в-памяти-но-в-реальном-приложении-вы-будете-использовать-базу-данных-в-этом-забавном-проекте-давайте-остановимся-на-хранении-в-памяти">Шаг 4. Управление URL-адресами, обновление и автоматическое периодическое обновлениеВ этой демонстрации мы просто сохраним его в памяти, но в реальном приложении вы будете использовать базу данных. В этом забавном проекте давайте остановимся на хранении в памяти.&lt;/h3>
&lt;h4 id="41-изменение-службы-для-управления-url-адресами">4.1 Изменение службы для управления URL-адресами&lt;/h4>
&lt;p>Во-первых, мы убедимся, что наш сервис может обрабатывать добавление и получение URL-адресов, а также получение информации о продукте:&lt;/p>
&lt;p>Обновление &lt;code>Services/ScraperService.cs&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Models&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Collections.Generic&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Net.Http&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Net.Http.Json&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Threading.Tasks&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ScraperService&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; urls = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ScraperService(HttpClient httpClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ProductInfo&amp;gt; GetProductInfoAsync(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _httpClient.GetFromJsonAsync&amp;lt;ProductInfo&amp;gt;(&lt;span style="color:#a5d6ff">$&amp;#34;http://localhost:5000/api/scraper/productinfo?url={url}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> AddUrl(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!urls.Contains(url))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urls.Add(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetUrls()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> urls;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ClearUrls()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urls.Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="42-обновление-компонента-blazor-для-управления-url-адресами-и-обновления">4.2 Обновление компонента Blazor для управления URL-адресами и обновления&lt;/h4>
&lt;p>Затем обновите &lt;code>Pages/Index.razor&lt;/code>, чтобы добавить управление URL-адресами, обновление и автоматическое периодическое обновление:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using PriceMonitorBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using PriceMonitorBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Services
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject ScraperService ScraperService
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Price Monitor&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>label &lt;span style="color:#ff7b72">for&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;urlInput&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Product URL:&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>label&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>input id&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;urlInput&amp;#34;&lt;/span> &lt;span style="color:#f85149">@&lt;/span>bind&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;productUrl&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;AddUrl&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Add URL&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;RefreshPrices&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Refresh All Prices&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>table &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;table&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Title&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> product &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> productInfos)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Title&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>table&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private string productUrl;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ProductInfo&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> productInfos &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ProductInfo&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">Timer&lt;/span> _timer;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override void OnInitialized()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StartTimer();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private void StartTimer()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">//&lt;/span> Set up the timer to call RefreshPrices every minute (&lt;span style="color:#a5d6ff">60000&lt;/span> ms)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _timer &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new &lt;span style="color:#f0883e;font-weight:bold">Timer&lt;/span>(async _ &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> await InvokeAsync(RefreshPrices), null, TimeSpan&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Zero, TimeSpan&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>FromMinutes(&lt;span style="color:#a5d6ff">1&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private void AddUrl()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>string&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>IsNullOrEmpty(productUrl))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ScraperService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>AddUrl(productUrl);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> productUrl &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> string&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task RefreshPrices()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> urls &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> ScraperService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetUrls();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> foreach (&lt;span style="color:#ff7b72">var&lt;/span> url &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> urls)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> productInfo &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ScraperService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetProductInfoAsync(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Add(productInfo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>В этом обновленном компоненте:&lt;/p>
&lt;ul>
&lt;li>Мы используем &lt;code>Timer&lt;/code> для периодического вызова метода &lt;code>RefreshPrices&lt;/code> каждую минуту.&lt;/li>
&lt;li>Метод &lt;code>StartTimer&lt;/code> инициализирует таймер, который запускается немедленно, а затем срабатывает каждые 60 секунд.&lt;/li>
&lt;li>Метод жизненного цикла &lt;code>OnInitialized&lt;/code> вызывает &lt;code>StartTimer&lt;/code> при инициализации компонента для запуска периодического обновления.&lt;/li>
&lt;/ul>
&lt;h4 id="43-запуск-решения">4.3 Запуск решения&lt;/h4>
&lt;p>Чтобы запустить обновленное приложение Blazor с новыми функциями, перестройте и перезапустите контейнеры Docker:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>docker-compose build
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker-compose up
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>После загрузки &lt;code>http://localhost:5001&lt;/code> в браузере приложение Blazor теперь должно автоматически обновлять цены на продукты каждую минуту, а также разрешать ручное обновление и управление URL-адресами.&lt;/p>
&lt;h2 id="заключение">Заключение&lt;/h2>
&lt;p>Создание этой системы мониторинга цен было настоящим праздником! Это не только избавило меня от стресса, связанного с ежедневными проверками цен, но и продемонстрировало волшебство современных веб-технологий.&lt;/p>
&lt;h1 id="праздничный-технический-календарь-на-2024-год">Праздничный технический календарь на 2024 год&lt;/h1>
&lt;p выравнивание="центр">
&lt;img src="https://festivetechcalendar.com/assets/images/Heading.png" />
&lt;/p>
&lt;p>Я создал этот пост в рамках мероприятия &lt;strong>Festive Tech Calendar 2024&lt;/strong>, которое объединяет энтузиастов технологий, новаторов и цифровых мечтателей, чтобы поделиться знаниями и отпраздновать слияние праздничного духа и технологических чудес. Эта инициатива направлена не только на обучение и общение, но и на отдачу.&lt;/p>
&lt;p>&lt;strong>Праздничный технологический календарь на 2024 год&lt;/strong> в этом году поддерживает благотворительную организацию Beatson Cancer Charity. Благотворительная организация Beatson Cancer Charity занимается поддержкой людей, страдающих раком, их семей и медицинских работников, которые о них заботятся. Более подробную информацию об их невероятной работе можно найти на сайте &lt;a href="https://www.beatsoncancercharity.org/">https://www.beatsoncancercharity.org/&lt;/a>.&lt;/p>
&lt;p>Посетите веб-сайт Festive Tech Calendar по адресу &lt;a href="https://festivetechcalendar.com">https://festivetechcalendar.com&lt;/a>, где можно найти часто задаваемые вопросы и получить более подробную информацию о мероприятии.&lt;/p>
&lt;h1 id="хо-хо-хо">&lt;strong>ХО ХО ХО!&lt;/strong>&lt;/h1>
&lt;p>Создание этого проекта стало прекрасным способом внести свой вклад в праздничное технологическое сообщество и в то же время поддержать великое дело.&lt;/p>
&lt;p>Я надеюсь, что это руководство оказалось для вас полезным и вдохновило вас на изучение новых способов использования технологий для упрощения повседневных задач.&lt;/p>
&lt;p>Если у вас есть какие-либо вопросы или вам нужна дополнительная помощь, пожалуйста, не стесняйтесь обращаться к нам.&lt;/p>
&lt;p>Приятного кодирования!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category><category>Docker</category><category>API</category></item><item><title>Пользовательская проверка ValidationAttribute и Blazor</title><link>https://emimontesdeoca.github.io/ru/posts/custom-validationattribute-blazor/</link><pubDate>Fri, 29 Mar 2024 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/custom-validationattribute-blazor/</guid><description>Создайте повторно используемые пользовательские классы ValidationAttribute для проверки формы Blazor с аннотациями данных.</description><content:encoded>&lt;p>﻿# Все на заказ&lt;/p>
&lt;p>Как вы, наверное, заметили во всех моих постах, я действительно стараюсь держать все как можно более чистым, поскольку я уже писал посты, касающиеся пользовательских атрибутов, пользовательской обработки исключений, внедрения коллекций сервисов и т. д.&lt;/p>
&lt;p>Со временем я понял, что такой способ кодирования дает мне и моей команде возможность совершенствоваться со временем, легче находить проблемы и максимально разделять код.&lt;/p>
&lt;p>Да, после этой крутой истории, которую я только что рассказал вам, в последнее время я, как обычно, работаю над Blazor и обнаружил, что после многих лет разработки вы можете создавать собственные атрибуты проверки.&lt;/p>
&lt;p>Да, это забавно, спустя столько лет&amp;hellip;&lt;/p>
&lt;h1 id="пользовательские-атрибуты-проверки">Пользовательские атрибуты проверки&lt;/h1>
&lt;p>На самом деле идея пришла из работы: мы всегда выполняем проверку во всех местах, но у меня были некоторые поля, которые требовали одного и того же процесса проверки, поэтому я подумал, что там может быть что-то&amp;hellip; например, пользовательские атрибуты проверки!&lt;/p>
&lt;p>Итак, я открыл документацию Microsoft и обнаружил, что да, вы можете создавать собственные атрибуты проверки и назначать их свойствам, вот так:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">StringLengthRangeAttribute&lt;/span> : ValidationAttribute
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Minimum { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Maximum { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> StringLengthRangeAttribute()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span>.Minimum = &lt;span style="color:#a5d6ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span>.Maximum = &lt;span style="color:#ff7b72">int&lt;/span>.MaxValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> IsValid(&lt;span style="color:#ff7b72">object&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> strValue = &lt;span style="color:#ff7b72">value&lt;/span> &lt;span style="color:#ff7b72">as&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(strValue))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span> len = strValue.Length;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> len &amp;gt;= &lt;span style="color:#ff7b72">this&lt;/span>.Minimum &amp;amp;&amp;amp; len &amp;lt;= &lt;span style="color:#ff7b72">this&lt;/span>.Maximum;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>и используйте его в простом классе, например:&lt;/p>
&lt;p>[[[ТОК_1]]]&lt;/p>
&lt;h1 id="пользовательский-валидатор">Пользовательский валидатор&lt;/h1>
&lt;p>Итак, у меня есть этот валидатор, который мне нужен для какого-то конкретного бизнес-кейса, который будет содержать 20 первых символов, которые будут дополнять 9 цифр и дефис и заканчиваться 2 символами, которые обычно будут кодом страны, что-то вроде этого: &lt;strong>123456789-123456789-ES&lt;/strong>&lt;/p>
&lt;p>В итоге я пришел к чему-то вроде этого, это действительно просто, но работает:&lt;/p>
&lt;p>[[[ТОК_2]]]&lt;/p>
&lt;h2 id="тесты">Тесты&lt;/h2>
&lt;p>Для них я тоже написал тест, на всякий случай:&lt;/p>
&lt;p>[[[ТОК_3]]]&lt;/p>
&lt;p>И когда я запустил, у меня были такие результаты:&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;h1 id="пользовательская-проверка-и-blazor">Пользовательская проверка и Blazor&lt;/h1>
&lt;p>Итак, теперь, когда я знаю, что его можно использовать, очевидно, что реализовать его в Blazor — хорошая идея, верно?&lt;/p>
&lt;p>Предположим, у меня есть эта форма, которая будет использовать модель &lt;code>Person&lt;/code>, которую я показывал ранее:&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;p>Как только мы запустим это, мы получим следующие ошибки:&lt;/p>
&lt;img src="https://i.imgur.com/psfpzdr.png">
&lt;p>А если просто поставить то, что хотим, то получим следующую вещь, все понятно:&lt;/p>
&lt;img src="https://i.imgur.com/RPoUq02.png">
&lt;p>Честно говоря, для меня совершенно очевидно, что мы должны переместить логику, по крайней мере, для проверки этих форм, в пользовательские атрибуты проверки. Это дает нам свободу хранить код для этого входа в одном месте, и мы можем использовать его позже для API или другого приложения.&lt;/p>
&lt;p>Надеюсь, вам понравилось. Если у вас есть какие-либо вопросы или вы хотите связаться со мной, не стесняйтесь и свяжитесь со мной!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category></item><item><title>Пользовательская обработка исключений в .NET API</title><link>https://emimontesdeoca.github.io/ru/posts/custom-exception-handler-api/</link><pubDate>Sun, 01 Oct 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/custom-exception-handler-api/</guid><description>Создайте собственное промежуточное программное обеспечение для обработки исключений, чтобы возвращать точные ответы об ошибках из API .NET.</description><content:encoded>&lt;p>﻿Исключения – это плохо, мы знаем, верно? Но что, если нам придется с ними справиться?&lt;/p>
&lt;p>Что происходит, когда у нас есть исключение, например в API, оно отображает сообщение стека, содержащее много информации, которую мы, возможно, захотим удалить из ответа, который получают наши пользователи.&lt;/p>
&lt;p>Для демонстрации я создал API dotnet и добавил метод, который выдает исключение:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Route(&amp;#34;GetWithoutExceptionHandler&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> Task GetWithoutExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;This is a custom exception!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Если мы создадим исключение, это будет выглядеть так:&lt;/p>
&lt;p>[[[ТОК_1]]]&lt;/p>
&lt;p>Это выглядит нормально, это то, что вы получаете из исключения, но для меня это отображение большого количества информации. Если у вас есть библиотеки и прочее, оно может показать полезную информацию о вашем клиенте, вашем проекте или других вещах тому, кто может пытаться что-то увидеть.&lt;/p>
&lt;p>Вот как это выглядит на Swagger:&lt;/p>
&lt;img src="https://imgur.com/fUujR3s.png"/>
&lt;h3 id="создание-собственного-исключения">Создание собственного исключения&lt;/h3>
&lt;p>Этого шага можно было бы избежать, поскольку мы знаем, какое исключение будет выдано, в данном случае &lt;code>Exception&lt;/code>, но для меня лучше иметь собственные исключения, поскольку у вас больше контроля над тем, что вы выбрасываете.&lt;/p>
&lt;p>В данном случае я только что создал объект &lt;code>CustomException&lt;/code>, который наследуется от &lt;code>Exception&lt;/code>:&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;p>После того, как мы создали собственное исключение, давайте обновим наш метод, чтобы он выдавал &lt;code>CustomException&lt;/code> вместо &lt;code>Exception&lt;/code>:&lt;/p>
&lt;p>[[[ТОК_9]]]&lt;/p>
&lt;p>На данный момент это ничего не изменит, но трассировка стека покажет, что созданный объект является &lt;code>CustomException&lt;/code> вместо &lt;code>Exception&lt;/code>, взгляните на начало трассировки стека:&lt;/p>
&lt;p>[[[ТОК_12]]]&lt;/p>
&lt;h3 id="создание-атрибута-exceptionfilterattribute">Создание атрибута ExceptionFilterAttribute&lt;/h3>
&lt;p>Microsoft предоставила нам способ обработки исключений после их возникновения. Дополнительную информацию можно найти &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.exceptionfilterattribute?view=aspnetcore-7.0">здесь&lt;/a>.&lt;/p>
&lt;p>Но на данный момент документация, которую они нам предоставляют, такова:&lt;/p>
&lt;blockquote>
&lt;p>Абстрактный фильтр, который запускается асинхронно после того, как действие вызвало исключение. Подклассы должны переопределять OnException(ExceptionContext) или OnExceptionAsync(ExceptionContext), но не оба одновременно.&lt;/p>&lt;/blockquote>
&lt;p>Итак, давайте создадим один, мы собираемся создать &lt;code>CustomExceptionFilterAttribute&lt;/code> в котором мы собираемся переопределить &lt;code>OnException&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc.Filters&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.Exceptions&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.ExceptionFilterAttributes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CustomExceptionFilterAttribute&lt;/span> : ExceptionFilterAttribute
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// OnException&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;ExceptionContext&amp;#34; name=&amp;#34;context&amp;#34;&amp;gt;Parameter for context&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnException(ExceptionContext context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (context.Exception &lt;span style="color:#ff7b72">is&lt;/span> CustomException)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.HttpContext.Response.StatusCode = &lt;span style="color:#a5d6ff">500&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.Result = &lt;span style="color:#ff7b72">new&lt;/span> ObjectResult(context.Exception.Message);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Как видите, мы рассматриваем &lt;code>ExceptionContext&lt;/code>, когда исключение является типом &lt;code>CustomException&lt;/code>, мы что-то делаем.&lt;/p>
&lt;p>Это что-то обновляет ответ и код состояния того, что мы собираемся вернуть.&lt;/p>
&lt;p>Чтобы обновить код состояния, мы должны обновить &lt;code>context.HttpContext.Response.StatusCode&lt;/code> и чтобы вернуть результат, мы должны обновить &lt;code>context.Result&lt;/code>, передав ему объект, унаследованный от &lt;code>ActionResult&lt;/code>.&lt;/p>
&lt;p>Это фильтр, поэтому нам нужно что-то добавить, добавив &lt;code>[CustomExceptionFilter]&lt;/code>.&lt;/p>
&lt;h3 id="использование-фильтра">Использование фильтра&lt;/h3>
&lt;p>Теперь давайте воспроизведем имеющийся у нас метод и добавим этот фильтр, чтобы он действовал. Конечная точка нашего API будет выглядеть следующим образом:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.ExceptionFilterAttributes&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.Exceptions&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.Controllers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ApiController]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;[controller]&lt;span style="color:#a5d6ff">&amp;#34;)]
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">WeatherForecastController&lt;/span> : ControllerBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> WeatherForecastController()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;GetWithoutExceptionHandler&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Task GetWithoutExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> CustomException(&lt;span style="color:#a5d6ff">&amp;#34;This is a custom exception!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;GetWithExceptionHandler&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [CustomExceptionFilter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Task GetWithExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> CustomException(&lt;span style="color:#a5d6ff">&amp;#34;This is a custom exception!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Как видите, у нас есть новый метод под названием &lt;code>GetWithExceptionHandler&lt;/code>, который имеет ту же логику, что и &lt;code>GetWithoutExceptionHandler&lt;/code>, но в данном случае мы добавили фильтр &lt;code>[CustomExceptionFilter]&lt;/code> к методу.&lt;/p>
&lt;p>Результат следующий: после запуска метода я отобразю изображение, поскольку оно больше не показывает трассировку стека:&lt;/p>
&lt;p>&lt;img src="https://imgur.com/a8TSAy2.png"/>Таким образом, мы создали собственное исключение, фильтр для переопределения того, что происходит, когда мы выбрасываем исключение и используем его в методе.&lt;/p>
&lt;p>Это можно использовать для множества вещей, таких как ведение журнала и знание того, что, когда и где происходит ошибка.&lt;/p></content:encoded><category>.NET</category><category>API</category></item><item><title>Более приятный способ внедрения вещей</title><link>https://emimontesdeoca.github.io/ru/posts/custom-iservicecollection-services/</link><pubDate>Mon, 04 Sep 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/custom-iservicecollection-services/</guid><description>Организуйте регистрацию внедрения зависимостей .NET, используя чистые методы расширения IServiceCollection.</description><content:encoded>&lt;p>﻿Всякий раз, когда я создаю такие вещи, как сервисы, репозитории, атрибуты или что-то еще, чтобы внедрить их в свои приложения, мы должны сделать этот шаг, который фактически добавляет сервисы в приложение.&lt;/p>
&lt;p>Это всегда одно и то же: вы переходите к &lt;code>Program.cs&lt;/code>, затем в какой-то части файла добавляете &lt;code>builder.Services.AddScoped&amp;lt;MyService&amp;gt;();&lt;/code> для внедрения сервиса.&lt;/p>
&lt;p>Что-то вроде этого:&lt;/p>
&lt;p>[[[ТОК_2]]]&lt;/p>
&lt;p>Я имею в виду, это работает, но я придирчив, и мне это не очень нравится.&lt;/p>
&lt;p>Допустим, у нас есть несколько внедрений зависимостей, которые мы хотим сделать, и на самом деле это не одно и то же. В моем случае это может быть что-то вроде библиотеки, содержащей все репозитории, другой библиотеки, содержащей все сервисы, и, наконец, еще одной библиотеки, содержащей атрибуты.&lt;/p>
&lt;p>В таком случае, можете ли вы представить себе, сколько строк нам придется добавить в &lt;code>Program.cs&lt;/code>.&lt;/p>
&lt;p>Допустим, у нас есть библиотека, содержащая некоторые сервисы. Если мы хотим включить все наши сервисы, нам придется работать с &lt;code>IServiceCollection&lt;/code>.&lt;/p>
&lt;p>Итак, мы собираемся создать &lt;code>static class&lt;/code> с методом &lt;code>static&lt;/code> под названием &lt;code>AddServices&lt;/code>, который возвращает &lt;code>IServiceCollection&lt;/code>.&lt;/p>
&lt;p>В этом случае он будет называться &lt;code>IServiceCollectionServicesExtensions&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// IServiceCollectionServicesExtensions class&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">IServiceCollectionServicesExtensions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// AddCoreServices&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;IServiceCollection&amp;#34; name=&amp;#34;services&amp;#34;&amp;gt;Parameter for &amp;lt;see cref=&amp;#34;IServiceCollection&amp;#34;/&amp;gt;&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;returns&amp;gt;An object of type &amp;lt;see cref=&amp;#34;IServiceCollection&amp;#34;/&amp;gt;&amp;lt;/returns&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IServiceCollection AddServices(&lt;span style="color:#ff7b72">this&lt;/span> IServiceCollection services)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> services
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;AService&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;BService&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;CService&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;DService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> services;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Мало того, у нас также есть еще одна библиотека, включающая несколько репозиториев, используемых этими сервисами, поэтому давайте сделаем то же самое.&lt;/p>
&lt;p>[[[ТОК_11]]]&lt;/p>
&lt;p>Теперь у нас есть репозитории и методы сервисов для внедрения, но как их использовать?&lt;/p>
&lt;p>Давайте вернемся к нашему &lt;code>Program.cs&lt;/code> и добавим следующее:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add services to the container.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddRazorPages();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Repositories&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddRepositories(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddServices(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Configure the HTTP request pipeline.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">if&lt;/span> (!app.Environment.IsDevelopment())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseExceptionHandler(&lt;span style="color:#a5d6ff">&amp;#34;/Error&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseHsts();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseHttpsRedirection();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseStaticFiles();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseRouting();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseAuthorization();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapRazorPages();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это выглядит намного чище, не так ли? Итак, мы успешно внедрили некоторые сервисы и репозитории, но теперь это выглядит лучше, и у нас действительно есть то, что мы внедрили, во внешней библиотеке.&lt;/p></content:encoded><category>.NET</category><category>Docker</category></item><item><title>Внедрение зависимостей с помощью атрибутов в .NET API</title><link>https://emimontesdeoca.github.io/ru/posts/api-di-attributes/</link><pubDate>Tue, 22 Aug 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/api-di-attributes/</guid><description>Включите внедрение зависимостей в фильтрах действий API .NET, используя TypeFilterAttribute вместо ActionAttribute.</description><content:encoded>&lt;p>﻿Внедрение зависимостей, вероятно, является одной из лучших функций, которые мы имеем в .NET на данный момент. Ни в коем случае вы не сможете его не использовать, поэтому, если вы похожи на меня, вам наверняка захочется добавить его во все создаваемые вами реализации.&lt;/p>
&lt;p>Фильтры согласно официальной [документации] Microsoft(&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.1%29">https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.1)&lt;/a>:&lt;/p>
&lt;blockquote>
&lt;p>Фильтры в ASP.NET Core позволяют запускать код до или после определенных этапов конвейера обработки запросов.&lt;/p>
&lt;p>Встроенные фильтры решают такие задачи, как:&lt;/p>
&lt;blockquote>
&lt;p>Авторизация, предотвращающая доступ к ресурсам, к которым пользователь не авторизован.&lt;/p>&lt;/blockquote>
&lt;ul>
&lt;li>Кэширование ответов, замыкание конвейера запросов для возврата кэшированного ответа.&lt;/li>
&lt;/ul>
&lt;p>Могут быть созданы пользовательские фильтры для решения сквозных задач. Примеры сквозных проблем включают обработку ошибок, кэширование, настройку, авторизацию и ведение журнала. Фильтры позволяют избежать дублирования кода.&lt;/p>&lt;/blockquote>
&lt;p>Я много работаю с API, и есть некоторые вещи, которые должны выполнять каждый отдельный запрос или почти все из них, поэтому в идеале мы хотим работать с ним плюс&amp;hellip; внедрение зависимостей!&lt;/p>
&lt;p>Но иногда это небольшая хитрость: это не работает так, как мы хотим, если мы хотим наследовать от &lt;code>ActionAttribute&lt;/code> поэтому нам нужно работать с &lt;code>TypeFilterAttribute&lt;/code>, что позволяет нам делать что-то при переопределении &lt;code>OnActionExecutionAsync&lt;/code>.&lt;/p>
&lt;p>Обычно я создаю эти фильтры для ведения журналов, поэтому мы будем использовать их в качестве примера:&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;p>Логика довольно проста: мы получаем тело, обращаясь к объекту &lt;code>context&lt;/code> с помощью &lt;code>context.ActionArguments.First().Value&lt;/code>, а также получаем вызов метода с помощью &lt;code>context.HttpContext.Request.Path.Value&lt;/code>.&lt;/p>
&lt;p>Затем мы просто вызываем наш метод из нашего сервиса, в данном случае это &lt;code> _loggingService.LogCustomEvent(call)&lt;/code>.&lt;/p>
&lt;p>Затем мы должны вызвать &lt;code>await next();&lt;/code>, потому что конвейер должен продолжить работу.&lt;/p>
&lt;p>Это атрибут, теперь мы должны включить этот атрибут в метод.&lt;/p>
&lt;p>[[[ТОК_10]]]&lt;/p>
&lt;p>Надеюсь, вам понравилось. Если у вас есть какие-либо вопросы или вы хотите связаться со мной, не стесняйтесь и свяжитесь со мной!&lt;/p></content:encoded><category>.NET</category></item><item><title>Документация Swagger с внешними библиотеками</title><link>https://emimontesdeoca.github.io/ru/posts/swagger-libraries-documentation/</link><pubDate>Fri, 17 Feb 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/swagger-libraries-documentation/</guid><description>Включите Swagger для отображения XML-документации из моделей, определенных во внешних библиотеках классов .NET.</description><content:encoded>&lt;p>﻿Использование нескольких библиотек для разделения кода для будущих случаев — это хорошо. Лично мне нравится это делать, если я могу разделить свою логику, чтобы ее можно было повторно использовать в других проектах или частях проектов.&lt;/p>
&lt;p>По крайней мере, для меня необходимо разделить модели данных, включая &lt;code>Entity Framework&lt;/code>, чтобы на них можно было ссылаться и использовать в &lt;code>Console application&lt;/code>, &lt;code>Blazor application&lt;/code> или &lt;code>API&lt;/code>.&lt;/p>
&lt;p>Давайте посмотрим на пример того, как я это делаю. Это снимок экрана проекта, в котором есть несколько приложений и общая библиотека со всеми моделями.&lt;/p>
&lt;img src="https://imgur.com/gdg2nn3.png">
&lt;p>Это пример класса, который я перенес из проекта &lt;code>API&lt;/code> с документацией с &lt;code>summary&lt;/code> по каждому свойству и классу.&lt;/p>
&lt;p>[[[ТОК_7]]]&lt;/p>
&lt;p>Если мы ссылаемся на &lt;code>Console application&lt;/code> и начинаем использовать модель, мы можем увидеть документацию, это стандартная функция в Visual Studio, поэтому она работает нормально, как вы можете видеть здесь:&lt;/p>
&lt;img src="https://imgur.com/Djf1MeO.png">
&lt;p>Но затем, если мы ссылаемся на &lt;code>API&lt;/code> и переходим к &lt;code>Swagger&lt;/code>, сводной документации не будет.&lt;/p>
&lt;img src="https://imgur.com/3Dg2Xpm.png">
&lt;p>Как нам это исправить?&lt;/p>
&lt;p>Во-первых, нам нужно включить XML-комментарии в библиотеке, для этого вам нужно обновить настройки проектов и включить ``:&lt;/p>
&lt;img src="https://imgur.com/R1j8SfX.png">
&lt;p>Это создаст в папке сборки несколько файлов, которые заканчиваются расширением &lt;code>xml&lt;/code>, как вы можете видеть здесь:&lt;/p>
&lt;img src="https://imgur.com/O8ywjr2.png">
&lt;p>Теперь, когда мы это сделали, нам нужно добавить кое-что в &lt;code>Program.cs&lt;/code>, чтобы можно было читать эти файлы, потому что по умолчанию он загружает только определение XML проекта, в котором он находится.&lt;/p>
&lt;p>Он использует метод, который есть внутри &lt;code>AddSwaggerGen&lt;/code> и называется &lt;code>IncludeXmlComments&lt;/code>.&lt;/p>
&lt;p>Идея состоит в том, что если у нас есть все файлы &lt;code>xml&lt;/code> мы принудительно загрузим их в Swagger.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddSwaggerGen(s =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Comments&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> allXmlFiles = Directory.GetFiles(AppContext.BaseDirectory, &lt;span style="color:#a5d6ff">&amp;#34;*.xml&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span> xmlFiles &lt;span style="color:#ff7b72">in&lt;/span> allXmlFiles)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s.IncludeXmlComments(xmlFiles);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это прямо сказано: мы получаем файлы &lt;code>xml&lt;/code> из каталога сборки и добавляем их с помощью метода &lt;code>IncludeXmlComments&lt;/code>.&lt;/p>
&lt;p>Теперь мы снова загружаем &lt;code>API&lt;/code> и проверяем, можем ли мы увидеть документацию.&lt;/p>
&lt;img src="https://imgur.com/uNVNswn.png">
&lt;p>И вы видите, что мы видим документацию!&lt;/p>
&lt;p>Я надеюсь, что это помогло вам, если у вас есть какие-либо вопросы, не стесняйтесь обращаться ко мне!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category><category>API</category></item><item><title>Очистка локальных веток в Git</title><link>https://emimontesdeoca.github.io/ru/posts/cleanup-local-branches/</link><pubDate>Mon, 30 Jan 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/cleanup-local-branches/</guid><description>Удалите все локальные ветки Git, кроме основной, с помощью одной команды PowerShell.</description><content:encoded>&lt;p>﻿Вы когда-нибудь доходили до того, что у вас было слишком много местных филиалов? Со мной это случается часто, поскольку мы используем ветки для каждой функции, ошибки или работы.&lt;/p>
&lt;p>В итоге у меня получается что-то вроде этого, и через некоторое время оно становится очень грязным&lt;/p>
&lt;img src="https://imgur.com/W3OGJE7.png">
&lt;p>Меня это очень раздражает, поэтому я просто поделюсь быстрой командой, которую вы можете запустить на консоли, чтобы выполнить эту очистку!&lt;/p>
&lt;p>Я нашел эту команду в Stack Overflow в следующем &lt;a href="https://stackoverflow.com/a/56671336/7823470">ответе&lt;/a> &lt;a href="https://stackoverflow.com/users/529612/robert-corvus">Роберта Корвуса&lt;/a>, версии, работающей на Powerhsell.&lt;/p>
&lt;p>&lt;strong>Пожалуйста, будьте осторожны при выполнении этой команды, потому что вы можете потерять изменения&lt;/strong>&lt;/p>
&lt;p>Прежде чем запускать его, не забудьте обновить &lt;code>MY_MASTER_BRANCH_NAME&lt;/code> до вашей основной ветки, это может быть &lt;code>master&lt;/code>, как я использую, или новые, которые по умолчанию называются &lt;code>main&lt;/code>.&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;p>После запуска этой команды вы получите такой вывод&lt;/p>
&lt;img src="https://imgur.com/VJn89OZ.png">
&lt;p>Надеюсь, это будет полезно для вас!&lt;/p></content:encoded><category>Git</category></item><item><title>Обновление маршрутов Identity в ASP.NET Core 7</title><link>https://emimontesdeoca.github.io/ru/posts/identity-url-change/</link><pubDate>Mon, 23 Jan 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/identity-url-change/</guid><description>Настройте URL-адреса входа и регистрации ASP.NET Core по умолчанию, создав страницы удостоверений.</description><content:encoded>&lt;p>﻿Даже интересно, как Microsoft называет такие редкие вещи? Я всегда думал, что на самом деле они делают это не так уж и хорошо, но, ну, вот что это такое!&lt;/p>
&lt;p>Хорошая вещь в этом то, что вы можете изменить практически все, пока разрабатываете!&lt;/p>
&lt;p>Вы когда-нибудь заходили на страницу и сразу понимали, что это веб-проект ASP.NET, просто выполнив процесс регистрации или входа в систему?&lt;/p>
&lt;img src="https://imgur.com/8NMNHGp.png">
&lt;p>Со мной это часто случалось, потому что по умолчанию в создаваемом вами проекте у вас есть эти URL-адреса для входа в систему и регистрации (а также у вас есть много других страниц).&lt;/p>
&lt;p>В этом уроке показан способ обновить эти URL-адреса, чтобы ваш проект выглядел лучше!&lt;/p>
&lt;h2 id="поведение-по-умолчанию">Поведение по умолчанию&lt;/h2>
&lt;p>Когда мы создаем проект Blazor и решаем использовать его с Identity, он отображает что-то вроде этого:&lt;/p>
&lt;img align="center" src="https://i.imgur.com/2W8Oou9.png">
&lt;p>И когда мы пытаемся выполнить вход или регистрацию, мы выбираем либо &lt;code>/Identity/Account/Login&lt;/code> либо &lt;code>/Identity/Account/Register&lt;/code>.&lt;/p>
&lt;p>Но что, если я скажу вам, что вы можете действительно обновить эти URL-адреса, чтобы они стали другими?&lt;/p>
&lt;h2 id="формирование-страниц-login--и-register">Формирование страниц &lt;code>Login &lt;/code> и &lt;code>Register&lt;/code>&lt;/h2>
&lt;p>Чтобы обновить эти страницы, Microsoft скрывает их, но вы можете быстро создать их и внести нужные изменения!&lt;/p>
&lt;p>Для этого вам нужно зайти и &lt;code>Add Scaffolded Item&lt;/code> в контекстном меню проекта, вот так:&lt;/p>
&lt;img src="https://imgur.com/F3C4C9b.png">
&lt;p>Затем появится модальное окно, и вам придется дважды выбрать &lt;code>Identity&lt;/code> и нажать &lt;code>Add&lt;/code>:&lt;/p>
&lt;img src="https://imgur.com/tZUqUlY.png">
&lt;p>После этого появится еще одно модальное окно, в котором вы сможете выбрать, какие страницы из всей идентичности вы хотите обновить. Есть много страниц, которые вы можете обновить, но мы сосредоточимся на &lt;code>Account/Login&lt;/code> и &lt;code>Account/Register&lt;/code>:&lt;/p>
&lt;img src="https://imgur.com/BS6ZLas.png">
&lt;p>Теперь дайте ему немного поработать, а затем проверьте обозреватель решений, вы найдете несколько новых файлов:&lt;/p>
&lt;img src="https://imgur.com/5lWHLyI.png">
&lt;p>Эти новые файлы представляют собой страницу входа и регистрации, которую ASP.NET добавляет в ваш проект, когда вы выбираете ее для добавления Identity!&lt;/p>
&lt;h2 id="обновление-url-адресов">Обновление URL-адресов&lt;/h2>
&lt;p>Как вы, наверное, заметили, эти файлы являются Razor-файлами, поскольку их расширение — &lt;code>cshtml&lt;/code>, поэтому мы просто собираемся использовать директиву для обновления URL-адреса страницы:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/login&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@model LoginModel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ViewData[&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>] = &lt;span style="color:#a5d6ff">&amp;#34;Log in&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;@ViewData[&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>]&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;row&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;col-md-4&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form id=&lt;span style="color:#a5d6ff">&amp;#34;account&amp;#34;&lt;/span> method=&lt;span style="color:#a5d6ff">&amp;#34;post&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h2&amp;gt;Use a local account to log &lt;span style="color:#ff7b72">in&lt;/span>.&amp;lt;/h2&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;hr /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div asp-validation-summary=&lt;span style="color:#a5d6ff">&amp;#34;ModelOnly&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span> role=&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>&amp;gt;&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;username&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;name@example.com&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt;Email&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;current-password&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;password&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt;Password&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;checkbox mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.RememberMe&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input class=&lt;span style="color:#a5d6ff">&amp;#34;form-check-input&amp;#34;&lt;/span> asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.RememberMe&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Html.DisplayNameFor(m =&amp;gt; m.Input.RememberMe)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button id=&lt;span style="color:#a5d6ff">&amp;#34;login-submit&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;w-100 btn btn-lg btn-primary&amp;#34;&lt;/span>&amp;gt;Log &lt;span style="color:#ff7b72">in&lt;/span>&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a id=&lt;span style="color:#a5d6ff">&amp;#34;forgot-password&amp;#34;&lt;/span> asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./ForgotPassword&amp;#34;&lt;/span>&amp;gt;Forgot your password?&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./Register&amp;#34;&lt;/span> asp-route-returnUrl=&lt;span style="color:#a5d6ff">&amp;#34;@Model.ReturnUrl&amp;#34;&lt;/span>&amp;gt;Register &lt;span style="color:#ff7b72">as&lt;/span> a &lt;span style="color:#ff7b72">new&lt;/span> user&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a id=&lt;span style="color:#a5d6ff">&amp;#34;resend-confirmation&amp;#34;&lt;/span> asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./ResendEmailConfirmation&amp;#34;&lt;/span>&amp;gt;Resend email confirmation&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;col-md-6 col-md-offset-2&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h3&amp;gt;Use another service to log &lt;span style="color:#ff7b72">in&lt;/span>.&amp;lt;/h3&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;hr /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> ((Model.ExternalLogins?.Count ?? &lt;span style="color:#a5d6ff">0&lt;/span>) == &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> There are no external authentication services configured. See &lt;span style="color:#ff7b72">this&lt;/span> &amp;lt;a href=&lt;span style="color:#a5d6ff">&amp;#34;https://go.microsoft.com/fwlink/?LinkID=532715&amp;#34;&lt;/span>&amp;gt;article
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> about setting up &lt;span style="color:#ff7b72">this&lt;/span> ASP.NET application to support logging &lt;span style="color:#ff7b72">in&lt;/span> via external services&amp;lt;/a&amp;gt;.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form id=&lt;span style="color:#a5d6ff">&amp;#34;external-account&amp;#34;&lt;/span> asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./ExternalLogin&amp;#34;&lt;/span> asp-route-returnUrl=&lt;span style="color:#a5d6ff">&amp;#34;@Model.ReturnUrl&amp;#34;&lt;/span> method=&lt;span style="color:#a5d6ff">&amp;#34;post&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-horizontal&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @foreach (&lt;span style="color:#ff7b72">var&lt;/span> provider &lt;span style="color:#ff7b72">in&lt;/span> Model.ExternalLogins!)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary&amp;#34;&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;provider&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;@provider.Name&amp;#34;&lt;/span> title=&lt;span style="color:#a5d6ff">&amp;#34;Log in using your @provider.DisplayName account&amp;#34;&lt;/span>&amp;gt;@provider.DisplayName&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@section Scripts {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#ff7b72">partial&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;_ValidationScriptsPartial&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Как вы можете видеть, большая часть материала осталась прежней, но если вы посмотрите на самую первую строку в классе, я обновил то, что у нас было раньше:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>чтобы&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/login&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ну, это было легко, не так ли? Теперь давайте проведем быстрый тест и проверим, работает ли это.&lt;/p>
&lt;p>Прежде всего, давайте перейдем на страницу по умолчанию, которая у нас есть изначально, чтобы проверить, работает ли она еще:&lt;/p>
&lt;img src="https://imgur.com/ngbRNaG.png">
&lt;p>Но это не так! Итак, теперь давайте проверим наш новый URL-адрес: &lt;code>/login&lt;/code>:&lt;/p>
&lt;img src="https://imgur.com/R067PnF.png">
&lt;p>И это работает!!&lt;/p>
&lt;p>Теперь давайте сделаем то же самое для реестра: обновим его страницу и добавим нужный путь в директиву &lt;code>@page&lt;/code> и проведем тест!&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/register&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@model RegisterModel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ViewData[&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>] = &lt;span style="color:#a5d6ff">&amp;#34;Register&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;@ViewData[&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>]&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;row&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;col-md-4&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form id=&lt;span style="color:#a5d6ff">&amp;#34;registerForm&amp;#34;&lt;/span> asp-route-returnUrl=&lt;span style="color:#a5d6ff">&amp;#34;@Model.ReturnUrl&amp;#34;&lt;/span> method=&lt;span style="color:#a5d6ff">&amp;#34;post&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h2&amp;gt;Create a &lt;span style="color:#ff7b72">new&lt;/span> account.&amp;lt;/h2&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;hr /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div asp-validation-summary=&lt;span style="color:#a5d6ff">&amp;#34;ModelOnly&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span> role=&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>&amp;gt;&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;username&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;name@example.com&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span>&amp;gt;Email&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;new-password&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;password&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span>&amp;gt;Password&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.ConfirmPassword&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;new-password&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;password&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.ConfirmPassword&amp;#34;&lt;/span>&amp;gt;Confirm Password&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.ConfirmPassword&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button id=&lt;span style="color:#a5d6ff">&amp;#34;registerSubmit&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;w-100 btn btn-lg btn-primary&amp;#34;&lt;/span>&amp;gt;Register&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;col-md-6 col-md-offset-2&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h3&amp;gt;Use another service to register.&amp;lt;/h3&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;hr /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> ((Model.ExternalLogins?.Count ?? &lt;span style="color:#a5d6ff">0&lt;/span>) == &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> There are no external authentication services configured. See &lt;span style="color:#ff7b72">this&lt;/span> &amp;lt;a href=&lt;span style="color:#a5d6ff">&amp;#34;https://go.microsoft.com/fwlink/?LinkID=532715&amp;#34;&lt;/span>&amp;gt;article
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> about setting up &lt;span style="color:#ff7b72">this&lt;/span> ASP.NET application to support logging &lt;span style="color:#ff7b72">in&lt;/span> via external services&amp;lt;/a&amp;gt;.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form id=&lt;span style="color:#a5d6ff">&amp;#34;external-account&amp;#34;&lt;/span> asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./ExternalLogin&amp;#34;&lt;/span> asp-route-returnUrl=&lt;span style="color:#a5d6ff">&amp;#34;@Model.ReturnUrl&amp;#34;&lt;/span> method=&lt;span style="color:#a5d6ff">&amp;#34;post&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-horizontal&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @foreach (&lt;span style="color:#ff7b72">var&lt;/span> provider &lt;span style="color:#ff7b72">in&lt;/span> Model.ExternalLogins!)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary&amp;#34;&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;provider&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;@provider.Name&amp;#34;&lt;/span> title=&lt;span style="color:#a5d6ff">&amp;#34;Log in using your @provider.DisplayName account&amp;#34;&lt;/span>&amp;gt;@provider.DisplayName&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@section Scripts {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#ff7b72">partial&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;_ValidationScriptsPartial&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Я обновил верхнюю часть с помощью &lt;code>@page &amp;quot;/login&amp;quot;&lt;/code> и теперь проверяем, работает ли она:&lt;/p>
&lt;img src="https://imgur.com/M7KakaF.png">
&lt;p>Тоже работает!!&lt;/p>
&lt;h2 id="вот-и-всенадеюсь-вы-научились-обновлять-эти-url-адреса-главным-образом-потому-что-в-некоторых-проектах-когда-вы-делаете-url-адреса-определенным-образом-а-затем-identity-выглядит-по-другому-это-отстой-ха-ха">Вот и всеНадеюсь, вы научились обновлять эти URL-адреса, главным образом потому, что в некоторых проектах, когда вы делаете URL-адреса определенным образом, а затем Identity выглядит по-другому, это отстой, ха-ха!&lt;/h2>
&lt;p>Если вам что-нибудь понадобится, просто напишите мне в Твиттере или отправьте электронное письмо, и я постараюсь помочь!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category></item><item><title>Пользовательские атрибуты в .NET 6 Core API</title><link>https://emimontesdeoca.github.io/ru/posts/custom-attributes-net-6-core-api/</link><pubDate>Fri, 09 Dec 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/custom-attributes-net-6-core-api/</guid><description>Создайте пользовательские классы ActionFilterAttribute для проверки заголовков запросов в API .NET 6 Core.</description><content:encoded>&lt;p>﻿Пользовательские атрибуты действительно полезны, я начал использовать их совсем недавно, потому что они позволяют мне создавать один из них и повторно использовать их либо в контроллере, либо в классе, либо в самом методе.&lt;/p>
&lt;p>Они действительно помогают, когда вы хотите выполнить некоторые действия по обеспечению безопасности, например проверить заголовки или проверить значение параметра, который вам определенно нужен.&lt;/p>
&lt;p>В моем случае мы собираемся использовать его в проекте API .NET Core, где мы собираемся проверить, содержат ли все запросы определенный заголовок.&lt;/p>
&lt;h1 id="атрибут-проверки-заголовка">Атрибут проверки заголовка&lt;/h1>
&lt;p>Итак, после того как мы создали наш классный API .NET Core, давайте создадим папку для хранения наших материалов, потому что нам нравится использовать папки.&lt;/p>
&lt;img src="https://i.imgur.com/i2VKbZN.png"/>
&lt;p>А затем мы добавим логику в наш класс &lt;code>HeaderCheckAttribute&lt;/code>.&lt;/p>
&lt;p>[[[ТОК_2]]]&lt;/p>
&lt;p>По сути, логика такова: сначала он проверяет заголовок с ключом &lt;code>x-dotnet-6-custom-attribute&lt;/code> и, если он там, проверяет, есть ли у него значения.&lt;/p>
&lt;p>Если оба этих выражения истинны, он повторно отправит &lt;code>BadRequestObjectResult&lt;/code> с определенным сообщением.&lt;/p>
&lt;h1 id="добавляем-его-в-контроллер">Добавляем его в контроллер&lt;/h1>
&lt;p>Мы можем добавить эту логику в несколько мест, мы можем добавить ее непосредственно ко всему контроллеру или мы можем добавить ее к некоторым методам, мы собираемся добавить ее сначала к методам, а затем ко всему контроллеру.&lt;/p>
&lt;p>Итак, давайте украсим ими класс &lt;code>WeatherForecastController&lt;/code> .&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;p>Давайте запустим проект!&lt;/p>
&lt;img src="https://i.imgur.com/ZvO4LnE.png"/>
&lt;p>У нас есть две функции: &lt;code>GetWeatherForecastWithCheck&lt;/code> и &lt;code>GetWeatherForecastWithoutCheck&lt;/code>, одна из них завершится неудачей, а другая — нет, но давайте проверим это на Swagger!&lt;/p>
&lt;img src="https://i.imgur.com/x1yRb9j.png"/>
&lt;img src="https://i.imgur.com/XiO4GC9.png">
&lt;p>Как вы можете видеть, один из наших сообщений возвращает ошибку 400, а другой возвращает значения. Теперь, чтобы полностью проверить это, давайте запустим Postman и добавим заголовок, чтобы мы также видели данные, используя &lt;code>GetWeatherForecastWithCheck&lt;/code>.&lt;/p>
&lt;h1 id="почтальон">Почтальон&lt;/h1>
&lt;p>Теперь, работая в Postman, мы добавляем заголовок и видим, что сообщение об ошибке изменилось, поскольку теперь мы предоставляем заголовок, но он не имеет никакого значения.&lt;/p>
&lt;img src="https://i.imgur.com/JHP8ZXZ.png"/>
&lt;p>Если мы добавим к нему значение, мы наконец получим значения!&lt;/p>
&lt;img src="https://i.imgur.com/jxt6xFe.png"/>
&lt;h1 id="вот-и-все">Вот и все&lt;/h1>
&lt;p>Ну вот и все! Довольно просто, правда? Что ж, теперь вы знаете, как создать атрибут и назначить его методам и контроллерам!&lt;/p>
&lt;p>Веселитесь с ними!&lt;/p>
&lt;h1 id="код">Код&lt;/h1>
&lt;p>Весь этот проект находится на Github, и вы можете найти его &lt;a href="https://github.com/emimontesdeoca/dotnet-6-attribute-post">здесь&lt;/a>!&lt;/p>
&lt;p>Если у вас есть какие-либо проблемы или вопросы, не стесняйтесь обращаться ко мне в любой социальной сети по адресу @emimontesdeoca (в Твиттере на самом деле это &lt;code>@emimontesdeocaa&lt;/code> с двумя &lt;code>aa&lt;/code> в конце). Большую часть моих соцсетей вы также можете найти в шапке блога.&lt;/p>
&lt;p>Надеюсь, вам понравился пост! Сайя!&lt;/p></content:encoded><category>.NET</category><category>API</category></item><item><title>Обработка загрузки компонентов в Blazor</title><link>https://emimontesdeoca.github.io/ru/posts/loading-component-blazor/</link><pubDate>Tue, 19 Jul 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/loading-component-blazor/</guid><description>Создайте многоразовый компонент-оболочку счетчика загрузки в Blazor, используя RenderFragment и ChildContent.</description><content:encoded>&lt;p>﻿Blazor потрясающий, действительно потрясающий, особенно когда мы делаем асинхронные вещи, такие как загрузка ящиков, и это выглядит просто хорошо.&lt;/p>
&lt;p>Я пытался придумать несколько способов обработки загрузки страниц, состояний, компонентов и т. д. И я думаю, что наконец нашел идеальный способ сделать это так, как я хочу.&lt;/p>
&lt;h1 id="идея">Идея&lt;/h1>
&lt;p>Вместо того, чтобы переписывать логику загрузки каждой страницы или компонента, мы создаем родительский компонент с помощью &lt;code>ChildComponent&lt;/code>, это даст нам возможность просто повторно использовать его несколько раз.&lt;/p>
&lt;h1 id="код-загрузки-компонента">Код загрузки компонента&lt;/h1>
&lt;p>Хотя код довольно прост, делать не так уж и много: базовый &lt;code>if&lt;/code> со свойством загрузки, функцией переключения внутри и все готово!&lt;/p>
&lt;p>[[[ТОК_2]]]&lt;/p>
&lt;h1 id="использование">Использование&lt;/h1>
&lt;p>Использование довольно простое: для тестирования мы собираемся использовать новую страницу и поместить наш контент внутрь только что созданного компонента &lt;code>LoadingComponent&lt;/code> .&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;p>Вот как это выглядит&lt;/p>
&lt;img src="https://i.gyazo.com/cb892d796f396d43d5c54e30e1e87568.gif"/>
&lt;h1 id="еще-забавный-пример">Еще забавный пример&lt;/h1>
&lt;p>Допустим, у нас есть несколько компонентов, каждый из которых имеет свое время загрузки, и на основе этого мы можем создать что-то, что будет хорошо выглядеть!&lt;/p>
&lt;p>Давайте создадим фиктивный загрузочный компонент, который мы сможем использовать повторно, под названием &lt;code>FakeLoadingComponent&lt;/code>.&lt;/p>
&lt;p>[[[ТОК_7]]]&lt;/p>
&lt;p>Затем мы просто обновляем страницу &lt;code>Loading&lt;/code> несколькими компонентами &lt;code>FakeLoadingComponent&lt;/code> и проверяем результат!!&lt;/p>
&lt;p>[[[ТОК_10]]]&lt;/p>
&lt;p>Теперь это выглядит намного лучше&lt;/p>
&lt;img src="https://i.gyazo.com/e427ca31af410314762446979d1c3739.gif">
&lt;p>И все!&lt;/p>
&lt;p>Если у вас есть какие-либо проблемы или вопросы, не стесняйтесь обращаться ко мне в любой социальной сети по адресу @emimontesdeoca (в Твиттере на самом деле это &lt;code>@emimontesdeocaa&lt;/code> с двумя &lt;code>aa&lt;/code> в конце). Большую часть моих соцсетей вы также можете найти в шапке блога.&lt;/p>
&lt;p>Надеюсь, вам понравился пост!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category></item><item><title>Получение срока действия сертификата</title><link>https://emimontesdeoca.github.io/ru/posts/expiration-date-certificate/</link><pubDate>Fri, 27 May 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/expiration-date-certificate/</guid><description>Программное получение дат истечения срока действия сертификата SSL с помощью C# HttpClient и X509Certificate2.</description><content:encoded>&lt;p>﻿Есть несколько веб-приложений, которые помогут вам проверить текущий статус сертификатов используемых нами доменов, и у нас их несколько.&lt;/p>
&lt;p>Я создал функцию Azure, которая запускается один раз в день и проверяет несколько доменов, которые мне нужно просмотреть. Это очень простое консольное приложение, которое, если срок действия сертификата меньше 30 дней, отправляет электронное письмо.&lt;/p>
&lt;p>Саму логику работы функции показывать не буду, хотелось бы показать функцию, которая выполняет базовую проверку сертификата и какие данные мы получаем.&lt;/p>
&lt;h2 id="функция">Функция&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;X509Certificate2&amp;gt; CheckCertificateAsync(&lt;span style="color:#ff7b72">string&lt;/span> urlPath)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> certificate = &lt;span style="color:#ff7b72">new&lt;/span> X509Certificate2();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> httpClientHandler = &lt;span style="color:#ff7b72">new&lt;/span> HttpClientHandler
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ServerCertificateCustomValidationCallback = (request, cert, chain, policyErrors) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> certificate = &lt;span style="color:#ff7b72">new&lt;/span> X509Certificate2(cert);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">HttpClient&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient(httpClientHandler);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> httpClient.SendAsync(&lt;span style="color:#ff7b72">new&lt;/span> HttpRequestMessage(HttpMethod.Head, urlPath));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> certificate;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Этот метод &lt;code>CheckCertificateAsync&lt;/code> вернет нам сертификат &lt;code>X509Certificate2&lt;/code> который позволит нам сделать множество вещей, включая проверку даты истечения срока действия.&lt;/p>
&lt;h2 id="сериализованный-результат">Сериализованный результат&lt;/h2>
&lt;p>Это сериализованное значение объекта &lt;code>certificate&lt;/code>:&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;h2 id="срок-годности">Срок годности&lt;/h2>
&lt;p>Чтобы узнать срок действия, нам нужно взглянуть на &lt;code>NotAfter&lt;/code> и &lt;code>NotBefore&lt;/code>, которые находятся внутри этого объекта:&lt;/p>
&lt;p>[[[ТОК_7]]]&lt;/p>
&lt;h2 id="консольное-приложение">Консольное приложение&lt;/h2>
&lt;p>Следующий фрагмент представляет собой простое консольное приложение, созданное на базе .NET 6, которое выдаст следующий результат, в котором вы можете проверить любой сертификат, который вам нужен:&lt;/p>
&lt;p>[[[ТОК_8]]]&lt;/p>
&lt;h2 id="демо-проект">Демо-проект&lt;/h2>
&lt;p>Вы можете найти консольное приложение на моем Github, в репозитории под названием &lt;a href="https://github.com/emimontesdeoca/expiration-date-certificate">expiration-date-certificate&lt;/a>.&lt;/p></content:encoded><category>.NET</category><category>Azure</category></item><item><title>Фокусировка элемента в Blazor</title><link>https://emimontesdeoca.github.io/ru/posts/focus-element-blazor/</link><pubDate>Thu, 05 May 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/focus-element-blazor/</guid><description>Установите фокус на элементы HTML в компонентах Blazor, используя JavaScript Interop и ссылки на элементы.</description><content:encoded>&lt;p>﻿После недавней работы над игрой Wordlzor мне нужно было добавить довольно простую функциональность: фокусировать всю игру при входе.&lt;/p>
&lt;p>Это необходимо было сделать, потому что пользователь действительно мог печатать в игре, а не только использовать экранную клавиатуру.&lt;/p>
&lt;h2 id="файл-javascript">Файл Javascript&lt;/h2>
&lt;p>Для этого нам нужно создать файл Javascript с именем &lt;code>app.js&lt;/code>, который будет содержать функцию&lt;/p>
&lt;p>[[[ТОК_1]]]&lt;/p>
&lt;p>После того, как мы это сделали, нам нужно включить скрипт в файл &lt;code>index.html&lt;/code>:&lt;/p>
&lt;p>[[[ТОК_3]]]&lt;/p>
&lt;h2 id="компонент-blazor">Компонент Blazor&lt;/h2>
&lt;p>После инициализации сценария нам нужно найти элемент, на котором можно сосредоточиться, поэтому в любом из наших компонентов нам нужно ссылаться на этот элемент на объект.&lt;/p>
&lt;p>Итак, в наш компонент blazor давайте добавим div с &lt;code> Свойство @ref&lt;/code>:&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;p>Затем в логике компонента вы можете использовать это свойство как &lt;code>ElementReference&lt;/code>:&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;h2 id="внедрение-jsinterop">Внедрение JSInterop&lt;/h2>
&lt;p>Теперь, чтобы вызвать функцию, которая есть в нашем файле Javascript, нам нужно использовать &lt;code>JSInterop&lt;/code>.&lt;/p>
&lt;p>Прежде всего, мы должны внедрить его в компонент со следующим синтаксисом:&lt;/p>
&lt;p>[[[ТОК_8]]]&lt;/p>
&lt;p>После внедрения сервиса мы теперь можем вызывать любой из имеющихся у него методов, например &lt;code>InvokeVoidAsync&lt;/code>, который будет вызывать функцию:&lt;/p>
&lt;p>[[[ТОК_10]]]&lt;/p>
&lt;p>И когда вы вызываете функцию &lt;code>Focus()&lt;/code>, она фокусирует элемент, который мы создали, очевидно, вы можете провести рефакторинг и передать сам элемент в качестве параметра.&lt;/p>
&lt;p>Если вы хотите посмотреть, как я это реализовал, взгляните на исходный код &lt;a href="https://github.com/emimontesdeoca/Wordlzor">Wordlzor&lt;/a>, я использовал его для оповещений и фокусировки при закрытии модального окна инструкций.&lt;/p></content:encoded><category>.NET</category><category>Blazor</category></item><item><title>Создание файлов *.dacpac из проекта базы данных VS в GitHub Actions</title><link>https://emimontesdeoca.github.io/ru/posts/generate-dacpacs-github-actions/</link><pubDate>Mon, 18 Apr 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/generate-dacpacs-github-actions/</guid><description>Автоматизируйте создание файлов dacpac из проектов базы данных Visual Studio с помощью конвейеров действий GitHub.</description><content:encoded>&lt;p>﻿Если вам нужно работать с базами данных и вы хотите заниматься гибкой разработкой, у вас должен быть поток, в котором всякий раз, когда вы добавляете или изменяете таблицу, SP, функцию, он компилирует их и генерирует файл развертывания для базы данных.&lt;/p>
&lt;p>Я использую этот подход уже давно: мы вносим изменения локально, сравниваем их с нашим проектом базы данных, а затем, когда мы фиксируем и отправляем наши изменения, они генерируют файл bacpac, который будет отправлен в базу данных.&lt;/p>
&lt;p>Это избавляет от необходимости делать это вручную, что может привести к человеческой ошибке, а в базе данных это действительно плохие новости.&lt;/p>
&lt;h1 id="проект-базы-данных">Проект базы данных&lt;/h1>
&lt;p>Когда мы создадим проект базы данных в Visual Studio и импортируем базу данных, в конечном итоге все будет так:&lt;/p>
&lt;img src="https://i.gyazo.com/c0e11b14c707db66b8dbb591031cc527.png" />
&lt;p>Когда мы запустим компиляцию этого проекта, он сгенерирует файл &lt;code>dacpac&lt;/code>, который будет включать всю нашу структуру.&lt;/p>
&lt;img src="https://i.gyazo.com/c883f3e329c7deb033564f7b5e9be7d4.png" />
&lt;h1 id="конвейер-github">конвейер Github&lt;/h1>
&lt;p>Теперь, когда наш код опубликован в репозитории, нам нужно создать действие, которое скомпилирует этот проект, сгенерирует этот файл &lt;code>dacpac&lt;/code> и поместит его куда-нибудь, чтобы мы могли его загрузить или использовать для другого шага.&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;p>Это действие выполнит ряд действий: создаст решение и поместит результат в папку &lt;code>artifacts&lt;/code>, созданную заранее, а затем загрузит файлы из этой папки в артефакты.&lt;/p>
&lt;h1 id="запуск-конвейера">Запуск конвейера&lt;/h1>
&lt;p>Теперь запустите сборку, результат должен быть следующим&lt;/p>
&lt;img src="https://i.gyazo.com/9fe0b7a44be0f07bcc41fe0862183a54.png" />
&lt;p>Если мы на самом деле загрузим артефакт и посмотрим на содержимое, это то, что нам понадобится в будущем, когда мы реализуем этап непрерывного развертывания, файл &lt;code>dacpac&lt;/code>!&lt;/p>
&lt;img src="https://i.gyazo.com/287a392df0c5e966958262644e335149.png" />
&lt;h1 id="код">Код&lt;/h1>
&lt;p>Весь этот проект находится на Github, и вы можете найти его &lt;a href="https://github.com/emimontesdeoca/dacpac-github-actions">здесь&lt;/a>!&lt;/p>
&lt;p>Если у вас есть какие-либо проблемы или вопросы, не стесняйтесь обращаться ко мне в любой социальной сети по адресу @emimontesdeoca (в Твиттере на самом деле это &lt;code>@emimontesdeocaa&lt;/code> с двумя &lt;code>aa&lt;/code> в конце). Большую часть моих соцсетей вы также можете найти в шапке блога.&lt;/p>
&lt;p>Надеюсь, вам понравился пост! Сайя!&lt;/p></content:encoded><category>CI/CD</category></item><item><title>Переключение тем с помощью Javascript Interop в Blazor</title><link>https://emimontesdeoca.github.io/ru/posts/blazor-toggle-darkmode/</link><pubDate>Fri, 01 Apr 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/blazor-toggle-darkmode/</guid><description>Реализуйте переключение светлой и темной темы в Blazor с помощью JavaScript Interop и атрибутов данных CSS.</description><content:encoded>&lt;p>﻿Для таких людей, как я, которые страдают от воспоминаний каждый раз, когда я открываю веб-страницу, я придумал очень простое решение, как переключаться между светлым и темным режимами с помощью Javascript и вызывать его из Blazor с помощью Javascript Interop.&lt;/p>
&lt;img src="https://media-exp1.licdn.com/dms/image/C4D22AQEYLTFA1e7i9A/feedshare-shrink_800/0/1641493691714?e=1651708800&amp;v=beta&amp;t=j6RxTrY--qUwxOcbt8Xh4QE9nYYF1zlJBMZP9dDLuC8" />
&lt;h1 id="установка-родительского-атрибута">Установка родительского атрибута&lt;/h1>
&lt;p>Прежде всего, мы должны идентифицировать родительский элемент с помощью идентификатора, потому что мы меняем цвета в зависимости от значения этого идентификатора.&lt;/p>
&lt;p>Чтобы сделать это в Javascript, нам нужно будет запустить эту команду&lt;/p>
&lt;p>[[[ТОК_1]]]&lt;/p>
&lt;p>В нашем случае мы хотим иметь два идентификатора для каждого типа стиля: &lt;code>light&lt;/code> и &lt;code>dark&lt;/code>.&lt;/p>
&lt;h2 id="создание-файла-javascript-для-обработки-логики">Создание файла Javascript для обработки логики&lt;/h2>
&lt;p>Теперь у нас есть функция для обновления идентификатора, теперь давайте создадим файл Javscript с функцией, которая выполняет эту логику. Наш файл будет называться &lt;code>app.js&lt;/code>&lt;/p>
&lt;img src="https://i.gyazo.com/c481aa0ed8329e8592832e9da2921cea.png" />
&lt;p>В этом файле у нас будет функция, которая будет вызывать код, о котором мы упоминали ранее.&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;h2 id="добавление-файла-javascript-в-blazor">Добавление файла Javascript в Blazor&lt;/h2>
&lt;p>Добавьте сценарий в приложение Blazor, добавив его туда, где расположены сценарии.&lt;/p>
&lt;p>[[[ТОК_7]]]&lt;/p>
&lt;h1 id="создание-сервиса">Создание сервиса&lt;/h1>
&lt;p>Теперь, когда у нас есть код Javascript, нам нужно создать сервис, который будет называться &lt;code>ThemeToggleService&lt;/code>.&lt;/p>
&lt;img src="https://i.gyazo.com/e6480acaf46eef68f65b16a0738683ce.png" />
&lt;p>Этот сервис будет обрабатывать логику переключения между темами &lt;code>light&lt;/code> и &lt;code>dark&lt;/code>.&lt;/p>
&lt;h2 id="внедрение-jsinterop">Внедрение JSInterop&lt;/h2>
&lt;p>Чтобы вызвать любой Javascipt, нам нужно вызвать JSInterop, поэтому нам нужно создать свойство, которое будет выполнять внедрение.&lt;/p>
&lt;p>[[[ТОК_12]]]&lt;/p>
&lt;h2 id="функция-для-установки-идентификатора">Функция для установки идентификатора&lt;/h2>
&lt;p>Теперь, когда у нас есть служба, давайте создадим функцию для установки идентификатора. Эта функция будет просто обновлять &lt;code>data-theme&lt;/code> на странице.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Components&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.JSInterop&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">BlazorDarkmodeToggle.Data&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ThemeToggleService&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IJSRuntime jSRuntime;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ThemeToggleService(IJSRuntime jSRuntime)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span>.jSRuntime = jSRuntime;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> IsLightTheme { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetIdentifier =&amp;gt; IsLightTheme ? &lt;span style="color:#a5d6ff">&amp;#34;light&amp;#34;&lt;/span> : &lt;span style="color:#a5d6ff">&amp;#34;dark&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task ToggleTheme() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IsLightTheme = !IsLightTheme;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> jSRuntime.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;toggleTheme&amp;#34;&lt;/span>, GetIdentifier);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="добавляем-сервис-в-автозагрузку">Добавляем сервис в автозагрузку&lt;/h2>
&lt;p>Чтобы сделать сервис доступным для всех компонентов и страниц, нам необходимо добавить его в файл &lt;code>Program.cs&lt;/code>.&lt;/p>
&lt;p>&lt;code>builder.Services.AddSingleton&amp;lt;ThemeToggleService&amp;gt;();&lt;/code>&lt;/p>
&lt;p>В итоге файл будет таким&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">BlazorDarkmodeToggle.Data&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Components&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Components.Web&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add services to the container.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddRazorPages();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddServerSideBlazor();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddSingleton&amp;lt;WeatherForecastService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddSingleton&amp;lt;ThemeToggleService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Configure the HTTP request pipeline.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">if&lt;/span> (!app.Environment.IsDevelopment())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseExceptionHandler(&lt;span style="color:#a5d6ff">&amp;#34;/Error&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseHsts();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseHttpsRedirection();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseStaticFiles();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseRouting();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapBlazorHub();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapFallbackToPage(&lt;span style="color:#a5d6ff">&amp;#34;/_Host&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Имейте в виду, что это может измениться в зависимости от того, используете ли вы Blazor &lt;code>Server&lt;/code> или Blazor &lt;code>WebAssembly&lt;/code>.&lt;/p>
&lt;h1 id="запекание-css">Запекание CSS&lt;/h1>
&lt;p>Теперь, когда у нас есть готовая часть кода, нам нужно начать работать над CSS. Что нам нужно сделать, так это установить все свойства CSS, которые обрабатывают цвета CSS, фон и т. д., в переменную.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>:&lt;span style="color:#d2a8ff;font-weight:bold">root&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#79c0ff">--background-color&lt;/span>:&lt;span style="color:#a5d6ff">#ffffff&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#79c0ff">--text-color&lt;/span>:&lt;span style="color:#a5d6ff">#000000&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Как только мы это сделаем, используя идентификатор, мы сможем легко переключаться между идентификаторами, и он будет использовать любое другое значение.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">[&lt;/span>&lt;span style="color:#7ee787">data-theme&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;dark&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">]&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#79c0ff">--background-color&lt;/span>: &lt;span style="color:#a5d6ff">#000000&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#79c0ff">--text-color&lt;/span>: &lt;span style="color:#a5d6ff">#ffffff&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Для крутого затухания анимации мы просто добавим ко всем свойства &lt;code>transition&lt;/code> , чтобы все выглядело хорошо.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">*&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">transition&lt;/span>: &lt;span style="color:#79c0ff">all&lt;/span> &lt;span style="color:#a5d6ff">250&lt;/span>&lt;span style="color:#ff7b72">ms&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Теперь мы назначим эти переменные некоторым классам CSS, чтобы мы могли видеть их на странице Blazor.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#f0883e;font-weight:bold">app-background&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">background-color&lt;/span>: &lt;span style="color:#d2a8ff;font-weight:bold">var&lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">--&lt;/span>background&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#79c0ff">color&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">width&lt;/span>: &lt;span style="color:#a5d6ff">200&lt;/span>&lt;span style="color:#ff7b72">px&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">height&lt;/span>: &lt;span style="color:#a5d6ff">200&lt;/span>&lt;span style="color:#ff7b72">px&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#f0883e;font-weight:bold">app-text&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">color&lt;/span>: &lt;span style="color:#d2a8ff;font-weight:bold">var&lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">--&lt;/span>&lt;span style="color:#79c0ff">text&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#79c0ff">color&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Не забудьте добавить файл в приложение Blazor, добавив его в &lt;code>head&lt;/code>.&lt;/p>
&lt;p>&lt;code>&amp;lt;link href=&amp;quot;css/app.css&amp;quot; rel=&amp;quot;stylesheet&amp;quot; /&amp;gt;&lt;/code>&lt;/p>
&lt;h1 id="звонок-от-blazor">Звонок от Blazor&lt;/h1>
&lt;p>Итак, как мы собираемся это проверить? Для простоты мы просто добавим элемент div с классом &lt;code>app-background&lt;/code> и внутри него будет &lt;code>p&lt;/code> с классом &lt;code>app-text&lt;/code>.Итак, давайте создадим страницу с именем &lt;code>Theme.razor&lt;/code> в папке &lt;code>Pages&lt;/code> и добавим для нее немного кода.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/theme&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@using BlazorDarkmodeToggle.Data
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inject ThemeToggleService ThemeToggleService
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;button class=&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary&amp;#34;&lt;/span> @onclick=Toggle&amp;gt;Toggle theme&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;br /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;app-background&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p class=&lt;span style="color:#a5d6ff">&amp;#34;app-text&amp;#34;&lt;/span>&amp;gt;Hello world!&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task Toggle()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">this&lt;/span>.ThemeToggleService.ToggleTheme();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Мы могли бы проверить это, перейдя в &lt;code>/theme&lt;/code>, но давайте также добавим его на панель навигации.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;nav-item px-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NavLink class=&lt;span style="color:#a5d6ff">&amp;#34;nav-link&amp;#34;&lt;/span> href=&lt;span style="color:#a5d6ff">&amp;#34;theme&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span class=&lt;span style="color:#a5d6ff">&amp;#34;oi oi-list-rich&amp;#34;&lt;/span> aria-hidden=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt; Theme togle
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/NavLink&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="тестирование">Тестирование&lt;/h1>
&lt;img src="https://i.gyazo.com/a4894edb345cfd8c9f0cf2de869dba32.gif" />
&lt;p>Как вы можете видеть, как только мы переключаем изменения цвета темы, фона и шрифта, теперь вы можете видеть, насколько это мощно: если вы действительно разрабатываете всю страницу, используя эти переменные в CSS, вы можете иметь разные темы и просто переключать их!&lt;/p>
&lt;p>Как здорово!!&lt;/p>
&lt;h1 id="код">Код&lt;/h1>
&lt;p>Весь этот проект находится на Github, и вы можете найти его &lt;a href="https://github.com/emimontesdeoca/BlazorDarkmodeToggle">здесь&lt;/a>!&lt;/p>
&lt;p>Если у вас есть какие-либо проблемы или вопросы, не стесняйтесь обращаться ко мне в любой социальной сети по адресу @emimontesdeoca (в Твиттере на самом деле это &lt;code>@emimontesdeocaa&lt;/code> с двумя &lt;code>aa&lt;/code> в конце). Большую часть моих соцсетей вы также можете найти в шапке блога.&lt;/p>
&lt;p>Надеюсь, вам понравился пост!&lt;/p>
&lt;h1 id="ресурсы">Ресурсы&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-6.0">Вызов функций JavaScript из методов .NET в ASP.NET Core Blazor&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>Docker</category></item><item><title>Загрузка файлов в хранилище BLOB-объектов Azure в Blazor</title><link>https://emimontesdeoca.github.io/ru/posts/uploading-files-az-blob-blazor/</link><pubDate>Wed, 23 Mar 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/uploading-files-az-blob-blazor/</guid><description>Отправьте файлы в хранилище BLOB-объектов Azure из приложения Blazor, используя встроенный входной файл HTML5.</description><content:encoded>&lt;p>﻿Служба хранения BLOB-объектов Azure — одна из наиболее часто используемых служб в экосистеме Azure. Она позволяет загружать в облако большое количество файлов и при этом стоит очень дешево. Он также имеет интуитивно понятный веб-сайт, к которому вы можете получить доступ или загрузить его по прямой ссылке.&lt;/p>
&lt;p>В целом довольно хороший сервис, я использую его как для работы, так и для личных проектов, и, честно говоря, самое лучшее — это простота того, как заставить его работать.&lt;/p>
&lt;p>В большинстве случаев хранилище BLOB-объектов Azure используется на серверной стороне, напрямую загружая что-то, что является результатом операции или чего-то еще. Так что я на самом деле не загружал файлы с веб-сайта или что-то в этом роде. Не повезло!&lt;/p>
&lt;p>Поскольку я фанат Blazor, я предлагаю вам способ загрузки файлов в хранилище BLOB-объектов Azure с использованием встроенного ввода файлов из HTML5.&lt;/p>
&lt;h1 id="предварительные-требования">Предварительные требования&lt;/h1>
&lt;ul>
&lt;li>Учетная запись Azure (если у вас ее нет, перейдите &lt;a href="aka.ms/free">здесь&lt;/a> с кучей $$$).&lt;/li>
&lt;li>IDE (VS, Code, подойдет любая)&lt;/li>
&lt;li>.NET Core 3.0 или выше&lt;/li>
&lt;/ul>
&lt;h1 id="создание-хранилища-blob-объектов-azure">Создание хранилища BLOB-объектов Azure&lt;/h1>
&lt;p>Перейдите на свой портал Azure и приступим к созданию учетной записи хранения.&lt;/p>
&lt;p>Сначала найдите &lt;code>Storage accounts&lt;/code> и нажмите на результат.&lt;/p>
&lt;img src="https://i.gyazo.com/dfc7db88129cd5e2a5015a7bfd846685.png" />
&lt;p>После загрузки нажмите &lt;code>Create&lt;/code>.&lt;/p>
&lt;p>Появится форма с множеством шагов, так что продолжайте и заполните их.&lt;/p>
&lt;img src="https://i.gyazo.com/2293db6fea38aa8b9c61980d088c56c4.png" />
&lt;p>Заполнив все нужными настройками, пройдите проверку и создайте ресурс.&lt;/p>
&lt;img src="https://i.gyazo.com/f416db52c958744f8e9ca1dbe904a790.png" />
&lt;p>После того, как он будет создан, нажмите &lt;code>Go to resource&lt;/code>. Мы собираемся получить строку подключения, которая позволит нам поиграть с API.&lt;/p>
&lt;img src="https://i.gyazo.com/713702e57e8c18db16ec214ea999507f.png" />
&lt;h1 id="получите-ключи-ресурсов">Получите ключи ресурсов&lt;/h1>
&lt;p>После того, как мы добрались до нашего вновь созданного ресурса, перейдите к &lt;code>Acccess keys&lt;/code> под &lt;code>Security + networking&lt;/code>. После загрузки вы увидите &lt;code>Connection string&lt;/code> и &lt;code>Key&lt;/code>, скопируйте их, потому что они нам понадобятся позже!&lt;/p>
&lt;img src="https://i.gyazo.com/87af79ff72e5973f3fbcdf22547d2c54.png" />
&lt;h1 id="создание-крутого-проекта-blazor-server">Создание крутого проекта Blazor Server&lt;/h1>
&lt;p>Для этого урока я буду использовать Visual Studio 2022, но, как я уже говорил ранее, вы можете использовать любую другую IDE и просто создать проект с помощью интерфейса командной строки &lt;code>dotnet&lt;/code> .&lt;/p>
&lt;p>Тогда давайте продолжим и очень быстро создадим проект Blazor с помощью Visual Studio всего за несколько шагов.&lt;/p>
&lt;img src="https://i.gyazo.com/fab7bbabc9601132c8554c0b83ff4f58.png" />
&lt;img src="https://i.gyazo.com/dd015b5c8f1a46c2df0fa5af7cfc08e4.png" />
&lt;img src="https://i.gyazo.com/e7225a77fb3a91c2fb5d39877165e9b8.png" />
&lt;p>Если мы запустим то, что только что создали, оно будет выглядеть так: простое обычное приложение Blazor.&lt;/p>
&lt;img src="https://i.gyazo.com/95785a6c6cc68050d9989c489df0f599.png" />
&lt;p>Теперь, когда у нас создан проект, давайте сделаем некоторые элементы пользовательского интерфейса, чтобы у нас была новая страница с набором входных данных, чтобы мы могли заполнить ключи из ресурса, &lt;code>InputFile&lt;/code> для файла(ов), кнопку для выполнения чего-либо и сообщение, которое будет результатом действия.&lt;/p>
&lt;h2 id="создание-модели">Создание модели&lt;/h2>
&lt;p>Чтобы сделать это правильно, нам понадобятся несколько моделей. Сначала у нас будет класс &lt;code>BlobRequest&lt;/code>, который будет обрабатывать строку подключения, имя контейнера и файлы.&lt;/p>
&lt;p>Кроме того, чтобы все было аккуратно, мы создадим класс с именем &lt;code>BlobFile&lt;/code>, в котором будем хранить &lt;code>Name&lt;/code> и &lt;code>Data&lt;/code>.&lt;/p>
&lt;p>Я создал папку под названием &lt;code>Models&lt;/code>, поэтому у нас все разделено.&lt;/p>
&lt;p>Занятия очень простые.```csharp
using System.ComponentModel.DataAnnotations;&lt;/p>
&lt;p>namespace UploadingFilesAzBlobBlazor.Models {
public class BlobRequest
{
[Required(ErrorMessage = &amp;ldquo;Connection string is required&amp;rdquo;)]
public string? ConnectionString { get; set; }
[Required(ErrorMessage = &amp;ldquo;Container name is required&amp;rdquo;)]
public string? Container { get; set; }
public List&lt;BlobFile>? Files { get; set; }
}
}&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>namespace UploadingFilesAzBlobBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public &lt;span style="color:#ff7b72">class&lt;/span> BlobFile
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public string&lt;span style="color:#f85149">?&lt;/span> Name { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public byte[]&lt;span style="color:#f85149">?&lt;/span> Data { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="изменение-пользовательского-интерфейса">Изменение пользовательского интерфейса&lt;/h2>
&lt;p>Теперь, когда наша модель создана, давайте продолжим и поколдуем с ее помощью над пользовательским интерфейсом!&lt;/p>
&lt;p>Прежде всего создайте страницу в папке &lt;code>Pages&lt;/code> под названием &lt;code>Upload.razor&lt;/code>.&lt;/p>
&lt;p>На этой странице мы добавим небольшую форму, которая будет заполнять наш класс &lt;code>BlobRequest&lt;/code>, поэтому нам нужно добавить входные данные для &lt;code>ConnectionString&lt;/code>, &lt;code>Container&lt;/code> и &lt;code>Files&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/upload&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@using UploadingFilesAzBlobBlazor.Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inject IJSRuntime JsRuntime
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;PageTitle&amp;gt; Upload&amp;lt;/PageTitle&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt; Azure blob storage uploader!&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;EditForm Model=&lt;span style="color:#a5d6ff">&amp;#34;@modal&amp;#34;&lt;/span> OnValidSubmit=&lt;span style="color:#a5d6ff">&amp;#34;@HandleValidSubmit&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;DataAnnotationsValidator /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;ValidationSummary /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;connectionString&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt; Connection &lt;span style="color:#ff7b72">string&lt;/span>&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;InputText class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;connectionString&amp;#34;&lt;/span> @bind-Value=&lt;span style="color:#a5d6ff">&amp;#34;modal.ConnectionString&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;container&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt; Container name&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;InputText class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;container&amp;#34;&lt;/span> @bind-Value=&lt;span style="color:#a5d6ff">&amp;#34;modal.Container&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;formFileMultiple&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt; Files&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;InputFile OnChange=&lt;span style="color:#a5d6ff">&amp;#34;OnInputFileChange&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;formFileMultiple&amp;#34;&lt;/span> multiple /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;button type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary&amp;#34;&lt;/span>&amp;gt; Submit&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/EditForm&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> BlobRequest modal = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> EditContext editContext;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IReadOnlyList &amp;lt;IBrowserFile&amp;gt; selectedFiles;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnInputFileChange(InputFileChangeEventArgs e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> selectedFiles = e.GetMultipleFiles();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> .StateHasChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task HandleValidSubmit() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// This is where we are going to upload stuff ! &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> JsRuntime.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Files uploaded!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Это небольшой код, но в основном он отображает форму и выполняет проверку.&lt;/p>
&lt;img src="https://i.gyazo.com/00d0ad6d23fa93bdb0fb354218018c5f.png"/>
&lt;p>Если вы нажмете кнопку загрузки и все в порядке, отобразится предупреждение.&lt;/p>
&lt;img src="https://i.gyazo.com/27a3ff831fa9936bac21d8aa8ff60936.gif"/>
&lt;h2 id="загрузка-в-хранилище-blob-объектов-azure">Загрузка в хранилище BLOB-объектов Azure&lt;/h2>
&lt;p>Теперь, когда у нас готова большая часть пользовательского интерфейса, давайте установим пакет, который будет обрабатывать API хранилища BLOB-объектов Azure и позволит нам загружать файлы. Довольно просто.&lt;/p>
&lt;h3 id="добавьте-пакет-nuget">Добавьте пакет NuGet&lt;/h3>
&lt;p>Давайте управлять пакетами NuGet из проекта и добавим &lt;code>Azure.Storage.Blob&lt;/code>.&lt;/p>
&lt;img src="https://i.gyazo.com/c6ecd7b13c0a63a50f38342407acee49.png"/>
&lt;h3 id="отправка-в-хранилище-blob-объектов-azure">Отправка в хранилище BLOB-объектов Azure&lt;/h3>
&lt;p>Теперь давайте реализуем логику фактической загрузки файла. Обычно мы используем строку подключения и ключ из файла app.config, но в целях этого руководства мы используем входные данные и передаем туда данные.&lt;/p>
&lt;p>Сначала добавим &lt;code>using&lt;/code> для класса хранилища BLOB-объектов Azure.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@using Azure.Storage.Blobs
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@using Azure.Storage.Blobs.Models
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Затем мы будем работать над методом &lt;code>HandleValidSubmit&lt;/code>, который будет подключаться к хранилищу BLOB-объектов, создавать контейнер, если он не существует, а затем загружать файлы.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task HandleValidSubmit()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Instantiate container&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> container = &lt;span style="color:#ff7b72">new&lt;/span> BlobContainerClient(modal.ConnectionString, modal.Container);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create container if not exists&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> createResponse = &lt;span style="color:#ff7b72">await&lt;/span> container.CreateIfNotExistsAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Set access policy&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (createResponse != &lt;span style="color:#79c0ff">null&lt;/span> &amp;amp;&amp;amp; createResponse.GetRawResponse().Status == &lt;span style="color:#a5d6ff">201&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> container.SetAccessPolicyAsync(Azure.Storage.Blobs.Models.PublicAccessType.Blob);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// For each file that we have uploaded&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> file &lt;span style="color:#ff7b72">in&lt;/span> selectedFiles)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// New blob&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> blob = container.GetBlobClient(file.Name);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Delete any blob with the same name&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> blob.DeleteIfExistsAsync(Azure.Storage.Blobs.Models.DeleteSnapshotsOption.IncludeSnapshots);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create a file stream and use the UploadSync method to upload the Blob.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> fileStream = file.OpenReadStream())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> blob.UploadAsync(fileStream, &lt;span style="color:#ff7b72">new&lt;/span> BlobHttpHeaders { ContentType = file.ContentType });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Display success message&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> JsRuntime.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Files uploaded!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Display error message&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> JsRuntime.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;An error ocurred!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="тестирование-кода">Тестирование кода&lt;/h3>
&lt;p>Теперь пришло время протестировать код. Для этого просто заполните форму и нажмите «Отправить».&lt;/p>
&lt;img src="https://i.gyazo.com/4ce26ec6487038f3bc70c8e977b16215.png"/>
&lt;p>Мы отобразим предупреждение, и файл должен быть загружен в хранилище BLOB-объектов Azure, перейдите на портал Azure и посмотрите.&lt;/p>
&lt;img src="https://i.gyazo.com/8c070489d4aa6a9f917d685a2ba670f3.png"/>
&lt;p>Как видите, контейнер тоже был создан, теперь если мы попадем внутрь контейнера, то увидим загруженный нами файл.&lt;/p>
&lt;img src="https://i.gyazo.com/166076b94e065578a3aaa4012d3b5c37.png"/>
&lt;p>Хорошая работа!&lt;/p>
&lt;h3 id="рефакторинг">Рефакторинг&lt;/h3>
&lt;p>Теперь, когда все готово, давайте переместим код со страницы &lt;code>.razor&lt;/code> в класс &lt;code>.razor.cs&lt;/code>, чтобы он выглядел лучше.&lt;/p>
&lt;p>Если вы используете Visual Studio 2022, при наведении курсора мыши прямо на &lt;code>@code&lt;/code> загорится лампочка, она покажет вам опцию &lt;code>Extract block to code behind&lt;/code> и сделает то, что сказано!&lt;/p>
&lt;img src="https://i.gyazo.com/1a4b52c9adf1728860b1965bc9b11bdd.png"/>
&lt;p>Теперь вы все разделите и сделаете!&lt;/p>
&lt;h1 id="код">Код&lt;/h1>
&lt;p>Весь этот проект находится на Github, и вы можете найти его &lt;a href="https://github.com/emimontesdeoca/UploadingFilesAzBlobBlazor">здесь&lt;/a>!&lt;/p>
&lt;p>Если у вас есть какие-либо проблемы или вопросы, не стесняйтесь обращаться ко мне в любой социальной сети по адресу @emimontesdeoca (в Твиттере на самом деле это &lt;code>@emimontesdeocaa&lt;/code> с двумя &lt;code>aa&lt;/code> в конце). Большую часть моих соцсетей вы также можете найти в шапке блога.&lt;/p>
&lt;p>Надеюсь, вам понравился пост! Сайя!&lt;/p>
&lt;h1 id="ресурсы">Ресурсы&lt;/h1>
&lt;ul>
&lt;li>[[[[ТОК_51]]])&lt;/li>
&lt;li>[[[[ТОК_52]]])&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>Azure</category><category>NuGet</category><category>Docker</category></item><item><title>Переопределение основного контроллера Pimcore</title><link>https://emimontesdeoca.github.io/ru/posts/override-method-pimcore-core-bundles/</link><pubDate>Thu, 10 Dec 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/override-method-pimcore-core-bundles/</guid><description>Переопределите основные контроллеры Pimcore, создав собственные пакеты Symfony с конфигурацией сервиса.</description><content:encoded>&lt;p>﻿Я уже некоторое время работаю над проектом Pimcore, никогда в жизни не касался PHP и Symfony, так что это был большой вызов.&lt;/p>
&lt;p>Документация великолепна, я имею в виду, серьезно, она великолепна, но похоже, что она не предназначена для новичков в языке/фреймворке, поэтому все, что я делал, мне приходилось записывать.&lt;/p>
&lt;p>Узнав, что вы можете расширить Pimcore с помощью пакетов, я потратил часы и часы, пытаясь переопределить контроллеры, методы JavaScript и многое другое.&lt;/p>
&lt;p>Итак, в этом посте я объясню, как мне удалось переопределить контроллер. Как переопределить файлы javascript, будет позже 😎.&lt;/p>
&lt;h1 id="создание-пакета">Создание пакета&lt;/h1>
&lt;p>Прежде всего нам нужно создать для него пакет, это довольно просто, как описано в &lt;a href="https://pimcore.com/docs/pimcore/current/Development_Documentation/Extending_Pimcore/Bundle_Developers_Guide/index.html">документации Pimcore&lt;/a>, нам просто нужно запустить &lt;code>bin/console pimcore:generate:bundle --namespace=EmiDemo/EmiDemoBundle&lt;/code> в папке проекта.&lt;/p>
&lt;p>Это вызовет некоторые вопросы, но не о чем беспокоиться.&lt;/p>
&lt;div style="text-align:center">&lt;img src="https://i.gyazo.com/9aa04169e668506d18638388d0061910.png" />&lt;/div>
&lt;p>Он создаст новую папку в папке &lt;code>src&lt;/code> с пространством имен, которое мы объявили ранее, и внутри у нас будут все необходимые файлы для пакета.&lt;/p>
&lt;div style="text-align:center">&lt;img src="https://i.gyazo.com/4d3329c303bb43012faaae43290c613b.png" />&lt;/div>
&lt;p>Кроме того, плагин будет обнаружен административным сайтом Pimcore, но будет отключен, вам необходимо включить его, чтобы начать его использовать.&lt;/p>
&lt;div style="text-align:center">&lt;img src="https://i.gyazo.com/e0170db9111df69a00bdb3f10473c9a7.png" />&lt;/div>
&lt;h1 id="переопределение-метода-контроллера">Переопределение метода контроллера&lt;/h1>
&lt;p>Для переопределения метода в контроллере сначала, очевидно, вам нужно найти действие, которое вы хотите обновить. Это кажется простым, но я расскажу вам так, как обычно (об этом узнал от моего товарища по команде &lt;a href="https://twitter.com/cesabreu">Cesar&lt;/a>).&lt;/p>
&lt;p>Сначала зайдите на страницу, на которой, по вашему мнению, контроллер предпринимает действия. В моем случае я хочу проверить ту страницу, которая загружается, когда мы открываем актив.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/df3833858806b14a39f50a0707a19dcd">&lt;img src="https://i.gyazo.com/df3833858806b14a39f50a0707a19dcd.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Затем просто откройте консоль и зайдите во вкладку «Сеть», повторите те же действия еще раз и попробуйте найти действие.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/5fbd9496d585d145bea3f9a3b950de73">&lt;img src="https://i.gyazo.com/5fbd9496d585d145bea3f9a3b950de73.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Используя эту информацию, вы можете видеть, что действие, загружающее актив, — это &lt;code>http://localhost/admin/asset/get-data-by-id?_dc=1607601778450&amp;amp;id=2&amp;amp;type=image&lt;/code>. Отсюда мы видим, что действие контроллера — &lt;code>get-data-by-id&lt;/code>.&lt;/p>
&lt;h2 id="найдите-действие-в-главном-контроллере">Найдите действие в главном контроллере&lt;/h2>
&lt;p>Для меня самый простой способ сделать это — просто найти во всех файлах кода Visual Studio&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/96cbeea01a1174d45e5a263a997c882a">&lt;img src="https://i.gyazo.com/96cbeea01a1174d45e5a263a997c882a.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Вероятно, он найдет более одного, поэтому вам нужно посмотреть и решить, какой из них является именно тем. В нашем случае мы работаем с активами, поэтому ясно, что мы хотим использовать &lt;code>AssetController.php&lt;/code>.&lt;/p>
&lt;h2 id="измените-основной-контроллер">Измените основной контроллер&lt;/h2>
&lt;p>Это зависит от разработчика, обычно я этого не делаю, потому что быстрее просто переопределить контроллер и продолжить разработку оттуда. Но я бы рекомендовал сначала обновить контроллер в основном пакете, чтобы проверить, работают ли ваши изменения.&lt;/p>
&lt;p>В нашем случае я просто верну сообщение в начале метода, чтобы проверить его работу.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-php" data-lang="php">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">$this&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&amp;gt;&lt;/span>adminJson([&lt;span style="color:#a5d6ff">&amp;#39;success&amp;#39;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#ff7b72">false&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#39;message&amp;#39;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Overriding the getDataByIdAction in the core!!&amp;#34;&lt;/span>]);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://gyazo.com/32139d67b9e5369d5ab38daed1b229ea">&lt;img src="https://i.gyazo.com/32139d67b9e5369d5ab38daed1b229ea.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Затем давайте перезагрузим наш ресурс и проверим вкладку «Сеть».&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/d1653f36e7bece9da6232ede0a431c05">&lt;img src="https://i.gyazo.com/d1653f36e7bece9da6232ede0a431c05.png" alt="Изображение от Гьязо">&lt;/a>Давайте оставим там то, что у нас есть, чтобы позже мы знали, что используем пакетное сообщение вместо основного.&lt;/p>
&lt;p>Теперь давайте просто скопируем это во временный файл, чтобы отслеживать, что мы сделали.&lt;/p>
&lt;h2 id="переместите-эти-изменения-в-пакет">Переместите эти изменения в пакет&lt;/h2>
&lt;p>Теперь, чтобы перенести эти изменения в наш пакет, сначала нам нужно кое-что из нашего основного контроллера:&lt;/p>
&lt;ul>
&lt;li>Пространство имен&lt;/li>
&lt;li>Импорт&lt;/li>
&lt;li>Имя контроллера&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://gyazo.com/416b3a527be397aaf6d9f89073c02428">&lt;img src="https://i.gyazo.com/416b3a527be397aaf6d9f89073c02428.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>И, очевидно, метод &lt;code>getDataByIdAction&lt;/code> который мы хотим переопределить.&lt;/p>
&lt;h2 id="разоблачение-контроллера">Разоблачение контроллера&lt;/h2>
&lt;p>Нам нужно выставить контроллер, для этого нужно зайти в файл &lt;code>/src/EmiDemo/EmiDemoBundle/Resources/config/pimcore/routing.yml&lt;/code> и добавить&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">options&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">expose&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#79c0ff">true&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Также нам нужно изменить &lt;code>prefix&lt;/code> на тот, который мы собираемся переопределить, в нашем случае на контроллер &lt;code>admin&lt;/code>.&lt;/p>
&lt;p>В итоге это будет выглядеть вот так&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">emi_demo_emi_demo&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">resource&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@EmiDemoEmiDemoBundle/Controller/&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">type&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">annotation&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">prefix&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">/admin&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">options&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">expose&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#79c0ff">true&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="defaultcontrollerphp">DefaultController.php&lt;/h2>
&lt;p>В нашем файле мы добавим импорт и расширим контроллер тем, который переопределим.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/b8d798436ed111d4676312f8aeba443d">&lt;img src="https://i.gyazo.com/b8d798436ed111d4676312f8aeba443d.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Затем мы просто скопируем метод из основного контроллера и, в нашем случае, обновим возвращаемое сообщение.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/dcdd9f8166e4ba8820cb8e285c43dec8">&lt;img src="https://i.gyazo.com/dcdd9f8166e4ba8820cb8e285c43dec8.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Если вы помните, у нас уже есть сообщение в основном контроллере: &lt;code>Overriding the getDataByIdAction in the core!!&lt;/code> и теперь мы должны увидеть &lt;code>Overriding the getDataByIdAction in the bundle!!&lt;/code>&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/8e16b1de2a40292c843c51576acf43c4">&lt;img src="https://i.gyazo.com/8e16b1de2a40292c843c51576acf43c4.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Давайте просто ничего не будем возвращать и посмотрим, что теперь мы можем видеть страницу такой, какой она была раньше.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/dda8df5f6d721f5ba25d6a056ac7f9bf">&lt;img src="https://i.gyazo.com/dda8df5f6d721f5ba25d6a056ac7f9bf.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Закомментируйте оператор возврата и перезагрузите компьютер.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/2fdbff7f45c38fb2bc56a3fc73661077">&lt;img src="https://i.gyazo.com/2fdbff7f45c38fb2bc56a3fc73661077.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h2 id="вот-и-все">Вот и все&lt;/h2>
&lt;p>И с помощью этой небольшой демонстрации вы увидите, насколько легко можно переопределить существующий базовый контроллер Pimcore. Есть несколько шагов, которым вы должны следовать, но в этом нет ничего сложного. Просто убедитесь, что вы открываете контроллер и время от времени очищаете кеш, пока вносите изменения, иногда он кэшируется, и вы застреваете, думая, что он не работает и просто кэшируется.&lt;/p>
&lt;p>Если вам интересно, как переопределить файлы JavaScript, это будет описано в другом уроке 😁.&lt;/p></content:encoded></item><item><title>Настройка Apple Magic Keyboard 2 в Windows 10</title><link>https://emimontesdeoca.github.io/ru/posts/configure-apple-keyboard-windows/</link><pubDate>Wed, 11 Nov 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/configure-apple-keyboard-windows/</guid><description>Установите драйверы и настройте Apple Magic Keyboard 2 для правильной работы в Windows 10.</description><content:encoded>&lt;p>&lt;a href="https://gyazo.com/9c8641cdd22bd528b2141bad1322c74a">&lt;img src="https://i.gyazo.com/9c8641cdd22bd528b2141bad1322c74a.jpg" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Буквально вчера я купил Apple Magic Keyboard 2, хотя у меня около 5 механических клавиатур, потому что я хотел попробовать ее, и она была беспроводной.&lt;/p>
&lt;p>Моя основная ОС — Windows 10, она мне нравится, и я не хочу ее менять, поэтому, помня об этом, я знал, что необходимо будет сделать некоторые вещи, чтобы клавиатура работала идеально. Я знаю, как работает Apple и как им нравится сохранять свои устройства в своей экосистеме.&lt;/p>
&lt;h2 id="проблемы">Проблемы&lt;/h2>
&lt;p>Если вы подключите клавиатуру, вы узнаете несколько вещей:&lt;/p>
&lt;ul>
&lt;li>Функциональные клавиши не работают&lt;/li>
&lt;li>Некоторые клавиши неправильно назначены (это случилось со мной в испанской версии)&lt;/li>
&lt;/ul>
&lt;h2 id="документация">Документация&lt;/h2>
&lt;p>Чтобы заставить его работать, мне пришлось много читать в Интернете, но именно эти две ссылки помогли мне заставить его работать:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.bluetoothgoodies.com/info/apple-devices/">В полной мере используйте Apple Magic Keyboard/Mouse/Trackpad в Windows&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://superuser.com/questions/82826/how-do-i-use-my-f-1-f12-keys-without-pressing-fn-on-windows-7-using-bootcamp-o">Как использовать клавиши f-1–f12 без нажатия FN в Windows 7 с помощью Bootcamp на Macbook Pro?&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="установка-драйвера-клавиатуры-apple">Установка драйвера клавиатуры Apple&lt;/h2>
&lt;p>Некоторые из этих шагов взяты из документации, упомянутой ранее:&lt;/p>
&lt;ol>
&lt;li>Установите &lt;a href="https://www.7-zip.org/">7zip&lt;/a> на свой компьютер, если у вас его нет.&lt;/li>
&lt;li>Установите &lt;a href="https://www.python.org/downloads/">Python (версия 2.x)&lt;/a> на свой компьютер, если у вас его нет.
&lt;ul>
&lt;li>ВАЖНО: Последняя версия Python — 3.x. Но вам нужна версия 2.x, поскольку скрипт бригадира несовместим с версией 3.x.&lt;/li>
&lt;li>(опция) Установщик по умолчанию не добавляет python.exe в ваш PATH. Если хотите, вам нужно включить эту опцию. (см. скриншот справа)&lt;/li>
&lt;li>Если у вас уже есть другая версия Python, вы, вероятно, не захотите включать эту опцию.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Загрузите brigadier (скрипт Python, который поможет вам загрузить последнюю версию Boot Camp).&lt;/li>
&lt;li>Щелкните правой кнопкой мыши следующую ссылку и сохраните файл, используя «Сохранить ссылку как&amp;hellip;». [[[ТОК_6]]]&lt;/li>
&lt;li>Откройте окно командной строки (также известное как окно DOS) и перейдите в каталог, в который вы загрузили скрипт бригадира.&lt;/li>
&lt;li>Предполагая, что сценарий бригадира сохранен как «brigadier.txt», выполните следующую команду:
&lt;ul>
&lt;li>Если в вашем PATH находится версия Python 2.x: python brigadier.txt &amp;ndash;model=MacBook13,2&lt;/li>
&lt;li>В противном случае: [Путь к Python версии 2.x]\python.exe brigadier.txt &amp;ndash;model=MacBook13,2&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Он загрузит большой пакет со всеми драйверами из Bootcamp.&lt;/li>
&lt;li>Создайте папку с именем &lt;code>BootCamp&lt;/code> и скопируйте в нее &lt;code>BootCamp-xxx-yyyyyy\BootCamp\Drivers\Apple\BootCamp.msi&lt;/code> и &lt;code>BootCamp-xxx-yyyyyy\BootCamp\Drivers\Apple\AppleKeyboardMagic2&lt;/code>.&lt;/li>
&lt;li>Запустите PowerShell администратора и выполните &lt;code>BootCamp.msi&lt;/code>, он установит кое-что, но нам нужно обновить драйвер, используя содержимое папки &lt;code>AppleKeyboardMagic2&lt;/code>&lt;/li>
&lt;li>Запускаем Диспетчер устройств (&lt;code>devmgmt.msc&lt;/code>)&lt;/li>
&lt;li>Разверните узел &lt;code>Human Interface Devices&lt;/code>&lt;/li>
&lt;li>Найдите &lt;code>Bluetooth HID Device&lt;/code>&lt;/li>
&lt;li>Обновите драйвер, используя содержимое папки &lt;code>AppleKeyboardMagic2&lt;/code>&lt;/li>
&lt;li>Перезагрузите компьютер.&lt;/li>
&lt;/ol>
&lt;p>Вы должны увидеть, что клавиатура Bluetooth теперь определяется как клавиатура Apple.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/278f6bd3e419d6688ccfadf6918ff309">&lt;img src="https://i.gyazo.com/278f6bd3e419d6688ccfadf6918ff309.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h3 id="обновление-поведения-клавиш-fnесли-вы-все-установили-правильно-вы-заметите-что-клавиши-fn-включены-по-умолчанию-это-означает-что-вам-нужно-нажать-fn--f5-для-фактического-нажатия-кнопки-f5">Обновление поведения клавиш FNЕсли вы все установили правильно, вы заметите, что клавиши FN включены по умолчанию. Это означает, что вам нужно нажать &lt;code>fn&lt;/code> + &lt;code>F5&lt;/code> для фактического нажатия кнопки &lt;code>F5&lt;/code>.&lt;/h3>
&lt;p>Чтобы это исправить, я нашел решение, указанное в разделе документации, которое работает путем изменения некоторой записи в реестре.&lt;/p>
&lt;ol>
&lt;li>Откройте реестр&lt;/li>
&lt;li>Перейдите к &lt;code>HKEY_CURRENT_USER\SOFTWARE\Apple Inc.\Apple Keyboard Support&lt;/code>&lt;/li>
&lt;li>Создайте или обновите &lt;code>OSXFnBehavior&lt;/code> и установите для него значение &lt;code>0&lt;/code>&lt;/li>
&lt;li>Перезагрузите компьютер.&lt;/li>
&lt;/ol>
&lt;h3 id="обновить-сопоставление-ключей">Обновить сопоставление ключей&lt;/h3>
&lt;p>Если у вас возникли проблемы с сопоставлениями, вы можете использовать &lt;a href="https://www.randyrants.com/category/sharpkeys/">SharpKeys&lt;/a> и обновить их.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/ee0301205ffeddaae4241db40002864d">&lt;img src="https://i.gyazo.com/ee0301205ffeddaae4241db40002864d.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Его очень просто использовать, но не забудьте выйти из системы или перезагрузить компьютер, чтобы включить обновления, поскольку при этом обновляется реестр.&lt;/p>
&lt;p>В моем случае мне пришлось обновить ключи &lt;code>Windows&lt;/code>, &lt;code>alt&lt;/code>, &lt;code>º&lt;/code> и &lt;code>&amp;lt;&amp;gt;&lt;/code> ключи.&lt;/p></content:encoded></item><item><title>Добавление скомпилированных библиотек в пакет NuGet</title><link>https://emimontesdeoca.github.io/ru/posts/adding-compiled-dll-to-nuget/</link><pubDate>Thu, 01 Oct 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/adding-compiled-dll-to-nuget/</guid><description>Исправьте недостающие библиотеки DLL в пакетах NuGet, включив ссылки на скомпилированные библиотеки в файл nuspec.</description><content:encoded>&lt;p>Буквально на днях я обнаружил ошибку, связанную с &lt;code>NullReferenceException&lt;/code> в отсутствующей библиотеке, которая должна была там быть, поскольку она взята из загруженного нами пакета nuget.&lt;/p>
&lt;p>По сути, я скомпилировал класс библиотеки, который ссылался на несколько проектов. На этапе сборки файлы &lt;code>dll&lt;/code> добавляются в папку &lt;code>bin&lt;/code>, но когда &lt;code>packaging&lt;/code> это пакет nuget и устанавливается в другое решение, файлы &lt;code>dll&lt;/code>, на которые я ссылался и которые следует скопировать в папку bin, отсутствуют.&lt;/p>
&lt;h3 id="как-это-решить">Как это решить&lt;/h3>
&lt;p>Решить эту проблему довольно просто: вам необходимо обновить файл &lt;code>nuspec&lt;/code> и для каждой из библиотек, которые вы хотите скопировать, добавить &lt;code>file&lt;/code> в часть &lt;code>files&lt;/code>.&lt;/p>
&lt;p>[[[ТОК_8]]]&lt;/p></content:encoded><category>NuGet</category></item><item><title>Сделать загрузку терминала bash в домашнем каталоге WSL</title><link>https://emimontesdeoca.github.io/ru/posts/bash-straight-to-wsl-machine-with-terminal/</link><pubDate>Tue, 22 Sep 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/bash-straight-to-wsl-machine-with-terminal/</guid><description>Настройте терминал Windows для открытия сеансов WSL непосредственно в домашнем каталоге Linux через .bashrc.</description><content:encoded>&lt;p>Я люблю WSL для разработки и тестирования с того дня, как он вышел, затем приложение Terminal исчезло, и я очень доволен: он хорошо выглядит, отлично работает и имеет отличную производительность.&lt;/p>
&lt;p>Было бы глупо не использовать его с дистрибутивом WSL вместо того, чтобы использовать консоль, которая вам его предоставляет.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/245fb4ff4fbd5297b2a8d9917dbee236">&lt;img src="https://i.gyazo.com/245fb4ff4fbd5297b2a8d9917dbee236.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Эта консоль, очевидно, хороша, но после того, как вы воспользуетесь Терминалом, пути назад уже не будет. Это намного лучше, еще и &lt;em>цвета&lt;/em>.&lt;/p>
&lt;p>Теперь, если вы запустите терминал и откроете новую вкладку для загрузки дистрибутива WSL, он загрузит смонтированную пользовательскую папку.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/dac1264f7b8dbae1f64e769b64c551da">&lt;img src="https://i.gyazo.com/dac1264f7b8dbae1f64e769b64c551da.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>На самом деле это не проблема, потому что если вы просто выполните &lt;code>cd ~&lt;/code> он просто загрузит пользовательскую папку дистрибутива.&lt;/p>
&lt;p>Дело в том, что я не хочу делать это снова и снова, поэтому давайте обновим файл, чтобы он делал это сам.&lt;/p>
&lt;h2 id="bashrc">.bashrc&lt;/h2>
&lt;p>Запустите терминал или консоль, зайдите в папку своего профиля, а затем отредактируйте файл &lt;code>.bashrc&lt;/code> с помощью вашего любимого текстового редактора.&lt;/p>
&lt;p>Добавьте &lt;code>cd ~&lt;/code> в конец файла перед последней инструкцией.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/b601aba59b9e877bc3926ab9ceb2b98c">&lt;img src="https://i.gyazo.com/b601aba59b9e877bc3926ab9ceb2b98c.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Сохраните, снова откройте консоль WSL с терминалом, и вы должны оказаться в папке профилей.&lt;/p>
&lt;p>Имейте в виду, что вам придется сделать это для всех ваших установок WSL, поскольку мы обновляем файл &lt;code>bashrc&lt;/code> для одной установки.&lt;/p></content:encoded></item><item><title>Генерация динамических объектов с использованием ExpandoObject</title><link>https://emimontesdeoca.github.io/ru/posts/dynamic-object-generation-expando-object/</link><pubDate>Fri, 06 Mar 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/dynamic-object-generation-expando-object/</guid><description>Используйте ExpandoObject для динамического создания объектов со свойствами, определяемыми во время выполнения, для гибкого экспорта данных.</description><content:encoded>&lt;p>Мне нужно было преобразовать файлы Excel с собственными определенными столбцами в новый файл с динамическими столбцами, и я был немного озадачен тем, как это сделать правильно, без каких-либо серьезных проблем с производительностью.&lt;/p>
&lt;p>Я подошел к руководству, которое вы можете найти &lt;a href="https://www.oreilly.com/content/building-c-objects-dynamically/">здесь&lt;/a>, в котором используется .NET &lt;a href="https://docs.microsoft.com/en-us/dotnet/api/system.dynamic.expandoobject?view=netframework-4.8">ExpandoObject&lt;/a>, который в значительной степени позволяет вам создавать объект и добавлять динамических участников.&lt;/p>
&lt;h2 id="расширяемыйобъект">РасширяемыйОбъект&lt;/h2>
&lt;p>Определение Microsoft следующее:&lt;/p>
&lt;blockquote>
&lt;p>Представляет объект, члены которого можно динамически добавлять и удалять во время выполнения.&lt;/p>&lt;/blockquote>
&lt;p>И есть несколько замечаний:&lt;/p>
&lt;blockquote>
&lt;p>Класс ExpandoObject позволяет добавлять и удалять члены его экземпляров во время выполнения, а также устанавливать и получать значения этих членов. Этот класс поддерживает динамическую привязку, что позволяет использовать стандартный синтаксис, например sampleObject.sampleMember, вместо более сложного синтаксиса, такого как sampleObject.GetAttribute(&amp;ldquo;sampleMember&amp;rdquo;).&lt;/p>&lt;/blockquote>
&lt;h2 id="текущее-поведение">Текущее поведение&lt;/h2>
&lt;p>У нас есть &lt;code>ExcelExportService&lt;/code>, который, передав &lt;code>List&amp;lt;T&amp;gt;&lt;/code>, в данном случае &lt;code>ExcelItem&lt;/code>, будет использовать &lt;code>Reflection&lt;/code> для создания файла &lt;code>xlsx&lt;/code>.&lt;/p>
&lt;p>Пока что наш код выглядит так:&lt;/p>
&lt;p>[[[ТОК_7]]]&lt;/p>
&lt;p>&lt;code>ExcelItem&lt;/code> — это объект со всеми свойствами, которые используются для создания файла Excel.&lt;/p>
&lt;p>[[[ТОК_9]]]&lt;/p>
&lt;p>Этот подход работает отлично, файл &lt;code>xlsx&lt;/code> генерируется без проблем, причем каждый столбец является каждым свойством, вы можете использовать этот метод &lt;code>GetExcelBytes&lt;/code> следующим образом, например, чтобы сохранить его в файл:&lt;/p>
&lt;p>[[[ТОК_12]]]&lt;/p>
&lt;h2 id="проблема">Проблема&lt;/h2>
&lt;p>Поскольку мы правильно знаем, что используем все свойства объекта &lt;code>ExcelItem&lt;/code> , все, что я хочу, это на самом деле не использовать их все, а просто использовать, может быть, 2 или 3 из них.&lt;/p>
&lt;p>Также обязательным условием является то, что я не хочу менять код, все должен делать &lt;code>administrator&lt;/code>, который будет решать, какие столбцы следует отображать, и он может знать или не знать свойства в коде.&lt;/p>
&lt;p>TLDR: все должно быть динамическим, у нас есть объект со свойствами, и мы должны убедиться, что сгенерированный файл имеет свойства &lt;code>N&lt;/code> этого объекта, но явно не жестко закодирован.&lt;/p>
&lt;h2 id="динамические-столбцы">Динамические столбцы&lt;/h2>
&lt;p>Изменение будет заключаться в использовании объекта &lt;code>dynamic&lt;/code>, поскольку мы хотели бы установить, какие свойства объекта будут использоваться для создания списка столбцов.&lt;/p>
&lt;p>Допустим, у нас есть объект с множеством свойств, например, &lt;em>тонной&lt;/em>, и мы на самом деле не хотим изменять объект &lt;code>ExcelItem&lt;/code> каждый раз, когда мы вносим изменения, мы создаем таблицу &lt;code>ColumnExcelItem&lt;/code>, которая будет использоваться для создания этого &lt;code>ExcelItem&lt;/code>.&lt;/p>
&lt;h3 id="структура-базы-данных">Структура базы данных&lt;/h3>
&lt;p>Мы сохраняем это определение в нашей базе данных примерно так:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>Id PropertyName
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---------------------
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1 Name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2 Surname
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>3 Age
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Обратите внимание, что у нас меньше значений, чем у существующих свойств объекта &lt;code>ExcelItem&lt;/code>: он имеет свойства &lt;code>6&lt;/code> и в базе данных указан только &lt;code>3&lt;/code>.&lt;/p>
&lt;p>Также обратите внимание, что &lt;code>PropertyName&lt;/code> должно соответствовать имени свойства объекта, который я буду использовать для динамического назначения.&lt;/p>
&lt;h2 id="построение-объекта">Построение объекта&lt;/h2>
&lt;p>Теперь, когда у нас есть таблицы базы данных, нам нужно создать репозиторий, чтобы получить их и иметь возможность использовать их в коде.Я не буду делать руководство по этой части, я пропущу начало новой функции &lt;code>GetExcelBytes()&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">byte&lt;/span>[] GetExcelBytes(List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; columns) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;ExcelItem&amp;gt; excelItems = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;ExcelItem&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;&lt;span style="color:#ff7b72">dynamic&lt;/span>&amp;gt; objectsToExcel = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">dynamic&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ExcelExportService exportService = &lt;span style="color:#ff7b72">new&lt;/span> ExcelExportService();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> item &lt;span style="color:#ff7b72">in&lt;/span> itemsToExcel)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">dynamic&lt;/span> newObject = &lt;span style="color:#ff7b72">new&lt;/span> ExpandoObject();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> col &lt;span style="color:#ff7b72">in&lt;/span> columns)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span>.AddProperty(newObject, col, product.GetType().GetProperty(col).GetValue(item, &lt;span style="color:#79c0ff">null&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> objectsToExcel.Add(newObject);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> exportService.ExportToExcel(objectsToExcel);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Теперь нам предстоит создать объект с некоторыми свойствами &lt;code>ExcelItem&lt;/code>, но полностью динамический. Вместо использования всех шести свойств мы используем только три из них — то, которое хотим отправить.&lt;/p>
&lt;p>Теперь давайте представим, что этот объект &lt;code>ExcelItem&lt;/code> имеет 200 свойств, &lt;em>сумасшедший&lt;/em>, но такое может случиться.&lt;/p>
&lt;p>Единственное, что мне нужно сделать, это вставить те свойства, которые я хочу отобразить, в файл Excel в эту таблицу &lt;code>ColumnExcelItem&lt;/code> и все.&lt;/p>
&lt;h2 id="случаев">Случаев&lt;/h2>
&lt;p>В данном случае мы использовали только один случай, но предположим, что вы хотите иметь разные отчеты для некоторых пользователей. Допустим, роль &lt;code>admin&lt;/code> должна получить все свойства, тогда вы создадите в базе данных что-то вроде этой структуры.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>ReportingTemplates
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Id PropertyName
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---------------------
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1 Admin
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2 Users
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>ReportingTemplateItems
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Id TemplateId PropertyName
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---------------------
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1 1 Id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2 1 Name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>3 1 Surname
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>4 1 Age
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>5 1 CreatedAt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>6 1 UpdatedAt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>7 2 Name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>8 2 Surname
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>9 2 Age
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Затем, получив шаблон, вы сможете иметь разные столбцы, все динамически и без привязки к коду.&lt;/p>
&lt;p>Если у нас есть пользователь с ролью &lt;code>Admins&lt;/code>, файл будет содержать столбцы &lt;code>Id&lt;/code>, &lt;code>Name&lt;/code>, &lt;code>Surname&lt;/code>, &lt;code>Age&lt;/code>, &lt;code>CreatedAt&lt;/code>, &lt;code>UpdatedAt&lt;/code>.&lt;/p>
&lt;p>В случае, если вы генерируете его с использованием роли &lt;code>Users&lt;/code>, он будет иметь &lt;code>Name&lt;/code>, &lt;code>Age&lt;/code>. &lt;code>Surname&lt;/code>.&lt;/p>
&lt;h2 id="заключение">Заключение&lt;/h2>
&lt;p>Это отличный способ узнать, как &lt;code>dynamic&lt;/code> работает в .NET, и это действительно хорошее решение, когда вам нужно иметь разные роли или шаблоны для разных пользователей и вы не особо заинтересованы в том, чтобы тратить время на жесткое программирование.&lt;/p></content:encoded><category>.NET</category></item><item><title>Непрерывная доставка пакета NuGet с использованием TravisCI</title><link>https://emimontesdeoca.github.io/ru/posts/automated-nuget-deployment-travis-ci/</link><pubDate>Tue, 21 Jan 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/automated-nuget-deployment-travis-ci/</guid><description>Автоматизируйте компиляцию, тестирование и публикацию пакетов NuGet с помощью конвейеров непрерывной доставки Travis CI.</description><content:encoded>&lt;p>﻿Недавно я сделал &lt;a href="https://emimontesdeoca.github.io/2020/ci-dotnet-core-and-travis-ci/">учебник&lt;/a> о том, как использовать Travis CI в качестве инструмента для автоматического тестирования вашего кода, который вы только что отправили или пытаетесь отправить в ветку &lt;code>master&lt;/code>. Заставить его скомпилировать и протестировать и, наконец, сообщить вам о статусе этого изменения.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/e4c3f9019fdb8a8d80b2649f4c4bbbde">&lt;img src="https://i.gyazo.com/e4c3f9019fdb8a8d80b2649f4c4bbbde.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Но мы можем продолжать делать ее &lt;strong>лучше&lt;/strong>, например, основную библиотеку, созданную в последнем уроке. Я хочу сделать ее общедоступной с помощью канала &lt;a href="https://www.nuget.org/">NuGet&lt;/a>, чтобы каждый мог упростить работу со своими библиотеками!&lt;/p>
&lt;p>&lt;strong>Но как?&lt;/strong> Как мы собираемся скомпилировать, протестировать, а затем опубликовать этот пакет в ленте, чтобы каждый мог загрузить его в свои проекты?&lt;/p>
&lt;p>Ну вот этот урок для 😁!&lt;/p>
&lt;h1 id="что-нового">Что нового?&lt;/h1>
&lt;p>В коде почти нет изменений, нам нужно изменить упаковку, публикацию, а затем то, как мы можем это автоматизировать.&lt;/p>
&lt;h1 id="net-core-cli">.NET Core CLI&lt;/h1>
&lt;p>Мы собираемся использовать &lt;a href="https://docs.microsoft.com/en-gb/dotnet/core/tools/?tabs=netcore2x">.NET Core CLI&lt;/a>, мы использовали его в предыдущем руководстве с такими командами, как &lt;code>dotnet restore&lt;/code>, &lt;code>dotnet build&lt;/code> и &lt;code>dotnet test&lt;/code>.&lt;/p>
&lt;p>Теперь мы собираемся использовать новый набор команд!&lt;/p>
&lt;h1 id="ток_9">[[[ТОК_9]]]&lt;/h1>
&lt;p>Вы можете посмотреть документацию по этому набору инструментов прямо &lt;a href="https://docs.microsoft.com/en-gb/nuget/reference/dotnet-commands">здесь&lt;/a>, но есть краткое объяснение:&lt;/p>
&lt;blockquote>
&lt;p>&lt;code>dotnet nuget&lt;/code> — это набор необходимых инструментов, включающий установку, восстановление, удаление и публикацию пакетов.&lt;/p>&lt;/blockquote>
&lt;h1 id="nuget-фид">NuGet-фид&lt;/h1>
&lt;p>&lt;a href="https://www.nuget.org/">NuGet — менеджер пакетов для .NET&lt;/a>. Клиентские инструменты NuGet предоставляют возможность создавать и использовать пакеты. Галерея NuGet — это центральный репозиторий пакетов, используемый всеми авторами и потребителями пакетов.&lt;/p>
&lt;p>NuGet бесплатен, поэтому вы можете зарегистрироваться, получить ключ API и начать загрузку пакетов. Они будут загружены, проверены и затем перечислены для &lt;strong>всех&lt;/strong>.&lt;/p>
&lt;h1 id="получите-ключ-api">Получите ключ API&lt;/h1>
&lt;p>Чтобы загрузить пакет, вам потребуется получить ключ API после регистрации, поэтому перейдите на &lt;a href="https://www.nuget.org/account/apikeys">страницу ключей API&lt;/a> и создайте новый.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/7509632471a35fb6b5b909699dec2520">&lt;img src="https://i.gyazo.com/7509632471a35fb6b5b909699dec2520.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>&lt;strong>Имейте в виду, что срок действия ключа API составляет 365 дней.&lt;/strong>&lt;/p>
&lt;p>Затем скопируйте свой ключ и сохраните его где-нибудь, как сказано на странице, вы не сможете увидеть его снова, и если вы не сохранили его, вам придется его восстановить.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/2feb401c84034706d12a59f03bd30136">&lt;img src="https://i.gyazo.com/2feb401c84034706d12a59f03bd30136.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h1 id="создать-пакет-nuget">Создать пакет NuGet&lt;/h1>
&lt;p>Теперь, когда у нас есть ключ &lt;code>API&lt;/code>, наш следующий шаг — сгенерировать пакет NuGet из решения, поэтому откройте решение, в котором у вас есть библиотека классов.&lt;/p>
&lt;p>Я буду использовать решение из моего &lt;a href="https://emimontesdeoca.github.io/2020/ci-dotnet-core-and-travis-ci/">последнего урока&lt;/a>, в котором есть класс библиотеки .NET Core под названием &lt;code>CalculatorCLI.Core&lt;/code>.&lt;/p>
&lt;p>Теперь перейдите в &lt;strong>свойства проекта&lt;/strong>, а затем в &lt;strong>Пакет&lt;/strong>. Вы увидите много информации о пакете, например версию пакета, авторов, описания и многое другое.&lt;/p>
&lt;p>Далее следует заполнение, вам не обязательно заполнять все, но важными и обязательными являются &lt;code>Package id&lt;/code>, &lt;code>Package version&lt;/code>, &lt;code>Authors&lt;/code> и &lt;code>Description&lt;/code>. Кроме того, если вы попытаетесь загрузить файл самостоятельно, он будет запрашивать &lt;code>License&lt;/code>, нам тоже нужно будет добавить его.&lt;a href="https://gyazo.com/6becdace2915c40fa2091ed9916fe517">&lt;img src="https://i.gyazo.com/6becdace2915c40fa2091ed9916fe517.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Затем вы можете сохранить проект, щелкнув правой кнопкой мыши по проекту и выбрав &lt;code>Pack&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">1&lt;/span>&amp;gt;&lt;span style="color:#ff7b72;font-weight:bold">------&lt;/span> Build started: Project: CalculatorCLI.Core, Configuration: Debug Any CPU &lt;span style="color:#ff7b72;font-weight:bold">------&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">1&lt;/span>&amp;gt;CalculatorCLI.Core &lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&amp;gt; D:&lt;span style="color:#f85149">\&lt;/span>Development&lt;span style="color:#f85149">\&lt;/span>Personal&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>demo&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI.Core&lt;span style="color:#f85149">\&lt;/span>bin&lt;span style="color:#f85149">\&lt;/span>Debug&lt;span style="color:#f85149">\&lt;/span>netstandard2&lt;span style="color:#a5d6ff">.1&lt;/span>&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI.Core.dll
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">1&lt;/span>&amp;gt;Successfully created &lt;span style="color:#ff7b72">package&lt;/span> &lt;span style="color:#f85149">&amp;#39;&lt;/span>D:&lt;span style="color:#f85149">\&lt;/span>Development&lt;span style="color:#f85149">\&lt;/span>Personal&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>demo&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI.Core&lt;span style="color:#f85149">\&lt;/span>bin&lt;span style="color:#f85149">\&lt;/span>Debug&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI.Core&lt;span style="color:#a5d6ff">.1.0.0.1&lt;/span>.nupkg&lt;span style="color:#f85149">&amp;#39;&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">==========&lt;/span> Build: &lt;span style="color:#a5d6ff">1&lt;/span> succeeded, &lt;span style="color:#a5d6ff">0&lt;/span> failed, &lt;span style="color:#a5d6ff">0&lt;/span> up&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>to&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>date, &lt;span style="color:#a5d6ff">0&lt;/span> skipped &lt;span style="color:#ff7b72;font-weight:bold">==========&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Вывод покажет, что если все в порядке, будет напечатано &lt;code>Successfully created package&lt;/code>.&lt;/p>
&lt;h1 id="использование-dotnet">Использование &lt;code>dotnet&lt;/code>&lt;/h1>
&lt;p>Как и в предыдущем уроке, мы использовали разные команды для создания и тестирования решения. Теперь мы собираемся добавить больше команд для упаковки и публикации пакетов.&lt;/p>
&lt;p>Эти команды:&lt;/p>
&lt;ol>
&lt;li>[[[ТОК_32]]]&lt;/li>
&lt;li>[[[ТОК_33]]]&lt;/li>
&lt;/ol>
&lt;h1 id="скрипты-развертывания">Скрипты развертывания&lt;/h1>
&lt;p>Поскольку мы собираемся изменить способ сборки, я считаю, что лучшей идеей было бы иметь разные файлы, каждый из которых содержит определение сборки в зависимости от среды.&lt;/p>
&lt;p>Итак, давайте создадим папку с именем &lt;code>scripts&lt;/code> с тремя скриптами &lt;code>bash&lt;/code> в корне репозитория.&lt;/p>
&lt;p>&lt;code>scripts\compile.sh&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Restoring...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet restore &lt;span style="color:#a5d6ff">&amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Building...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet build &lt;span style="color:#a5d6ff">&amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34;&lt;/span> -c Release
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>scripts\test.sh&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Testing...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet test &lt;span style="color:#a5d6ff">&amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34;&lt;/span> -c Release -v n
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>scripts\push.sh&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Packing...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet pack ./CalculatorCLI/CalculatorCLI.Core/CalculatorCLI.Core.csproj -c Release
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Pushing...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet nuget push ./CalculatorCLI/CalculatorCLI.Core/bin/Release/*.nupkg -s &lt;span style="color:#a5d6ff">&amp;#34;https://nuget.org&amp;#34;&lt;/span> -k &lt;span style="color:#79c0ff">$NUGET_API_KEY&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="улучшение-файла-travisyml">Улучшение файла &lt;code>.travis.yml&lt;/code>&lt;/h1>
&lt;p>Теперь, когда у нас есть обновленный исходный код, мы знаем команды, которые нам нужно использовать, нам нужно интегрировать нашу непрерывную доставку с непрерывной интеграцией. При этом нам на самом деле не нужно ничего делать, кроме кода, тестирования, проверки и продвижения.&lt;/p>
&lt;p>Теперь мы собираемся сделать с файлом &lt;code>.travis.yml&lt;/code> следующее:&lt;/p>
&lt;ol>
&lt;li>Добавьте разные &lt;code>stages&lt;/code>&lt;/li>
&lt;li>Каждый &lt;code>stage&lt;/code> зависит от ветки&lt;/li>
&lt;li>Если ваша сборка находится на master, это означает, что пакет необходимо обновить, поэтому мы собираемся отправить наш пакет в каналы NuGet.&lt;/li>
&lt;li>Если ваша сборка представляет собой запрос на включение, мы все равно проверим, компилируется ли сборка и протестируем ее, но не будем ее публиковать.&lt;/li>
&lt;/ol>
&lt;h2 id="этапы">Этапы&lt;/h2>
&lt;p>Из их документации:&lt;/p>
&lt;blockquote>
&lt;p>Вы можете отфильтровывать и отклонять сборки, этапы и задания, указав условия в конфигурации сборки (ваш файл .travis.yml).&lt;/p>&lt;/blockquote>
&lt;p>Дополнительную информацию о TravisCI &lt;code>stages&lt;/code> можно найти на &lt;a href="https://docs.travis-ci.com/user/conditional-builds-stages-jobs">странице документации&lt;/a>.&lt;/p>
&lt;p>Итак, мы собираемся изменить файл и добавить этапы, что в итоге получится следующим образом:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">language&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">csharp&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">sudo&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">required&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">mono&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">none &lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">dotnet&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">3.0&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">os&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">linux&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">jobs&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">include&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">stage&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">compile&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">script&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">bash scripts/compile.sh&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">stage&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">test&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">script&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">bash scripts/test.sh&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">stage&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">deploy-prod&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">if&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">branch = master AND type = push&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;Deploy to NuGet&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">script&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">bash scripts/push.sh&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Как вы можете видеть, у нас есть три разных этапа &lt;code>compile&lt;/code>, &lt;code>test&lt;/code> и &lt;code>push&lt;/code>, последний из которых будет запускаться только тогда, когда &lt;code>branch&lt;/code> равен &lt;code>master&lt;/code> и это не &lt;code>pull request&lt;/code> для него, а только &lt;code>push&lt;/code>.&lt;/p>
&lt;p>Если вы этого не понимаете, просто загляните в документацию, и все объясняется довольно просто.&lt;/p>
&lt;p>Имея это в виду, все &lt;code>pull request&lt;/code> не будут ничего отправлять в канал NuGet.&lt;/p>
&lt;h1 id="установка-ключа-api-в-качестве-переменной-среды">Установка ключа API в качестве переменной среды&lt;/h1>
&lt;p>Перейдите к настройкам сборки вашего репозитория и добавьте новую переменную среды с именем &lt;code>NUGET_API_KEY&lt;/code> со значением, являющимся скопированным ключом API со страницы NuGet.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/68a4bcc4ce57eccd6bb25b7d62901c84">&lt;img src="https://i.gyazo.com/68a4bcc4ce57eccd6bb25b7d62901c84.png" alt="Изображение от Гьязо">&lt;/a>&lt;/p>
&lt;h2 id="создайте-pull-request">Создайте &lt;code>pull request&lt;/code>&lt;/h2>
&lt;p>Теперь, когда у нас все настроено, пришло время создать &lt;code>pull request&lt;/code> и проверить, игнорирует ли компиляция для этого запроса на включение последний &lt;code>stage&lt;/code>.&lt;/p>
&lt;h2 id="проверьте-сборки">Проверьте сборки&lt;/h2>
&lt;p>Как только вы сделаете запрос на включение, сборка будет поставлена в очередь на панели управления TravisCI, и, как вы можете видеть, у нас там две разные сборки, а не три.&lt;a href="https://gyazo.com/11a209e72a121aef82a5b5a80d77a2f0">&lt;img src="https://i.gyazo.com/11a209e72a121aef82a5b5a80d77a2f0.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Теперь давайте примем &lt;code>pull request&lt;/code> и снова проверим сборку, чтобы увидеть, что теперь у нас есть три задания вместо двух.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/46dad20d00defb8c14279332a946e73e">&lt;img src="https://i.gyazo.com/46dad20d00defb8c14279332a946e73e.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>И как только вы увидите, что сборка поставлена в очередь, вы увидите три задания, включая &lt;code>Deploy-prod&lt;/code> в конце.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/d24344e42f2b45225acb618ac030fd8f">&lt;img src="https://i.gyazo.com/d24344e42f2b45225acb618ac030fd8f.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Как только вся сборка завершится, вы увидите в журналах, что пакет был отправлен в исходные коды.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/77b1e44b4f9059cb7266fb18fbace8a7">&lt;img src="https://i.gyazo.com/77b1e44b4f9059cb7266fb18fbace8a7.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>И, очевидно, вы можете увидеть это на странице NuGet.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/09421094730ea2e087cc5da639069ec4">&lt;img src="https://i.gyazo.com/09421094730ea2e087cc5da639069ec4.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/a7905ddfd6665bb6e8e62e160f21630d">&lt;img src="https://i.gyazo.com/a7905ddfd6665bb6e8e62e160f21630d.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h1 id="вот-и-все">Вот и все&lt;/h1>
&lt;p>В этом руководстве мы узнали, как автоматизировать доставку пакетов NuGet. Мы использовали некоторые инструменты, такие как &lt;code>TravisCI&lt;/code>, &lt;code>stages&lt;/code> и &lt;code>dotnet nuget&lt;/code>.&lt;/p>
&lt;p>На мой взгляд, даже если это потребует времени для правильной настройки и в некоторых случаях может оказаться слишком сложным. Инвестирование времени в такого рода методы, чтобы все было идеально и автоматизировано, того стоит.&lt;/p>
&lt;p>Вы можете найти исходный код вместе с файлом &lt;code>.travis.yml&lt;/code> прямо &lt;a href="https://github.com/emimontesdeoca/CalculatorCLI-demo">здесь&lt;/a>, если у вас есть какие-либо вопросы, не стесняйтесь обращаться ко мне в &lt;a href="https://twitter.com/emimontesdeocaa">twitter&lt;/a>!&lt;/p></content:encoded><category>.NET</category><category>NuGet</category><category>CI/CD</category></item><item><title>Непрерывная интеграция проекта .NET Core 3.0 с использованием TravisCI</title><link>https://emimontesdeoca.github.io/ru/posts/ci-dotnet-core-and-travis-ci/</link><pubDate>Tue, 14 Jan 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/ci-dotnet-core-and-travis-ci/</guid><description>Настройте непрерывную интеграцию для проектов .NET Core с помощью Travis CI с автоматическими сборками и тестами.</description><content:encoded>&lt;p>В прошлые выходные я решил, что хочу правильно запустить свой проект скрапер-проверки-загрузчика, который я делал в разных репозиториях.&lt;/p>
&lt;p>После запуска еще одного проекта это должно было быть круто, &lt;strong>на самом деле круто&lt;/strong>, использование CI/CD, пул-реквест, документация, значки в файле readme, все, что я видел, это круто и действительно является лучшими практиками.&lt;/p>
&lt;p>И после хороших выходных я создал &lt;a href="https://github.com/Dramarr">Dramarr&lt;/a>, набор инструментов для удаления и загрузки шоу из разных источников.&lt;/p>
&lt;p>У него есть разные репозитории в организации, и большинство из них представляют собой библиотеки, которые компилируются, тестируются и развертываются самостоятельно в запросе на включение и при слиянии в основной ветке.&lt;/p>
&lt;p>Это то, что называется CI/CD или непрерывной интеграцией/непрерывной доставкой.&lt;/p>
&lt;p>Но в этом уроке мы просто поговорим о CI.&lt;/p>
&lt;h1 id="непрерывная-интеграция">Непрерывная интеграция&lt;/h1>
&lt;h2 id="что-это">Что это?&lt;/h2>
&lt;p>Взято из [блога] Мартина Фаулера (&lt;a href="https://martinfowler.com/articles/continuousIntegration.html%29">https://martinfowler.com/articles/continuousIntegration.html)&lt;/a>, это лучшее объяснение, которое я читал:&lt;/p>
&lt;blockquote>
&lt;p>Непрерывная интеграция — это практика разработки программного обеспечения, при которой члены команды часто интегрируют свою работу, обычно каждый человек интегрируется как минимум ежедневно, что приводит к нескольким интеграциям в день.
Каждая интеграция проверяется автоматической сборкой (включая тестирование) для максимально быстрого обнаружения ошибок интеграции.&lt;/p>&lt;/blockquote>
&lt;h2 id="инструменты">Инструменты&lt;/h2>
&lt;p>Существует множество инструментов для интеграции вашего рабочего процесса с CI/CD, но в этом руководстве мы будем использовать &lt;a href="https://github.com/">Github&lt;/a> для хранения нашего кода и инструменты &lt;a href="https://travis-ci.org/">TravisCI&lt;/a> для настройки CI. Что касается языка и фреймворков, мы будем использовать C# и новый .NET Core 3.0.&lt;/p>
&lt;h1 id="требования">Требования&lt;/h1>
&lt;p>Для того, чтобы сделать эту работу, вам нужны три простые вещи:&lt;/p>
&lt;ol>
&lt;li>Последняя версия Visual Studio 2019.&lt;/li>
&lt;li>Аккаунт на Гитхабе&lt;/li>
&lt;li>Учетная запись Travis-CI, связанная с вашей учетной записью Github.&lt;/li>
&lt;/ol>
&lt;h1 id="проект">Проект&lt;/h1>
&lt;p>В рамках этого урока мы будем создавать простой калькулятор. Мы будем создавать библиотеку, инструмент командной строки и проект тестирования, чтобы протестировать все.&lt;/p>
&lt;p>Этот проект тестирования также будет запущен, когда мы настроим CI, а это означает, что если в будущем мы внесем изменения в код и тесты, которые мы изначально создали, не пройдут, мы получим уведомление или можем просто отклонить запрос на включение.&lt;/p>
&lt;h2 id="создание-репозитория-github">Создание репозитория Github&lt;/h2>
&lt;p>Сначала мы создадим репозиторий Github, поэтому зайдите на Github, создайте репозиторий и клонируйте его в локальную среду. Я решил назвать этот новый репозиторий &lt;code>CalculatorCLI-demo&lt;/code>.&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/0657eb2bdeb3c331b9e4585d7deed5ef.png" >
&lt;h2 id="создание-решения">Создание решения&lt;/h2>
&lt;p>Теперь создадим пустое решение с именем &lt;code>CalculatorCLI&lt;/code> в корневой папке клонированного репозитория.&lt;/p>
&lt;h2 id="основная-библиотека">Основная библиотека&lt;/h2>
&lt;p>Как и в реальном проекте, мы будем хранить нашу логику в отдельном проекте, который генерирует библиотеку, поэтому давайте создадим ее.&lt;/p>
&lt;p>Идите и создайте &lt;code>Class Library (.NET Standard)&lt;/code> и назовите его &lt;code>CalculatorCLI.Core&lt;/code>&lt;/p>
&lt;h3 id="версия-net-coreкак-только-вы-создадите-проект-перейдите-к-свойствам-проекта-и-измените-target-framework-на-net-standard-21-чтобы-сделать-его-совместимым-с-проектами-встроенными-в-net-core-30">Версия NET CoreКак только вы создадите проект, перейдите к свойствам проекта и измените &lt;code>Target framework&lt;/code> на &lt;code>.NET Standard 2.1&lt;/code>, чтобы сделать его совместимым с проектами, встроенными в &lt;code>.NET Core 3.0&lt;/code>.&lt;/h3>
&lt;h3 id="код">Код&lt;/h3>
&lt;p>В целях обучения давайте создадим простой класс, обрабатывающий операции.&lt;/p>
&lt;p>[[[ТОК_12]]]&lt;/p>
&lt;h2 id="интерфейс-командной-строки">интерфейс командной строки&lt;/h2>
&lt;p>Теперь, когда у нас есть основной проект, давайте создадим приложение. В данном случае это будет простое консольное приложение, которое принимает аргументы и отображает результат.&lt;/p>
&lt;p>Итак, давайте продолжим и создадим новый &lt;code>Console App (.NET Core)&lt;/code>, я назвал его &lt;code>CalculatorCLI.CLI&lt;/code>.&lt;/p>
&lt;h3 id="версия-net-core">Версия NET Core&lt;/h3>
&lt;p>Как мы делали раньше, как только вы создадите проект, перейдите в свойства проекта и измените &lt;code>Target framework&lt;/code> на &lt;code>.NET Core 3.0&lt;/code>, если это еще не так.&lt;/p>
&lt;p>Затем добавьте ссылку на &lt;code>ConsoleCLI.Core&lt;/code> в наш вновь созданный проект.&lt;/p>
&lt;h3 id="код-1">Код&lt;/h3>
&lt;p>Что касается кода, это проще, чем раньше.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">ConsoleCalculator.Core&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Text.RegularExpressions&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">ConsoleCalculator.CLI&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Program&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> Main(&lt;span style="color:#ff7b72">string&lt;/span>[] args)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (args.Length == &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PrintUsage();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> joinedArgs = &lt;span style="color:#ff7b72">string&lt;/span>.Join(&lt;span style="color:#a5d6ff">&amp;#34; &amp;#34;&lt;/span>, args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> regex = &lt;span style="color:#a5d6ff">@&amp;#34;-op [\+\-\*\/] -l [-0-9]+ -r [-0-9]+&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (Regex.IsMatch(joinedArgs, regex))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span> _left = Int32.Parse(args[&lt;span style="color:#a5d6ff">3&lt;/span>]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span> _right = Int32.Parse(args[&lt;span style="color:#a5d6ff">5&lt;/span>]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> _operator = args[&lt;span style="color:#a5d6ff">1&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> _operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(_operator, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> _result = _operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Result is: {_result}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PrintUsage();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> PrintUsage()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Welcome to ConsoleCalculator!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;-op Operator, it must be +,-,*,/&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;-l Left number&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;-r Left number&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Example usage: -op + -l 5 -r 6&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Мы будем использовать это приложение из команды вроде, поэтому, чтобы оно работало, нам нужно вызвать его, передав некоторые параметры. Например:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ConsoleCalculator.CLI.exe -op + -l &lt;span style="color:#a5d6ff">10&lt;/span> -r &lt;span style="color:#a5d6ff">20&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Что переводится как:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ConsoleCalculator.CLI.exe -operator + -leftValue &lt;span style="color:#a5d6ff">10&lt;/span> -rightValue &lt;span style="color:#a5d6ff">20&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Код для этого довольно прост: если он не соответствует определенному шаблону регулярного выражения, это неправильный вызов и он вызывает &lt;code>PrintUsage()&lt;/code>. Это означает, что если мы введем нечто отличное от числа, поскольку оно установлено в регулярном выражении, оно даже не попытается выполнить расчет.&lt;/p>
&lt;p>Это означает, что если мы назовем это так:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ConsoleCalculator.CLI.exe -operator + -leftValue asdfg -rightValue ghjk
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Он никогда не войдет в логику операций, и мы сохраняем будущие проверки, такие как &lt;code>TryParse&lt;/code> при проверке значений.&lt;/p>
&lt;h2 id="тест">Тест&lt;/h2>
&lt;p>У нас есть основная библиотека и командная строка, но теперь нам нужно протестировать, потому что именно это мы хотим сделать в CI.&lt;/p>
&lt;p>Итак, давайте продолжим и создадим новый &lt;code>MSTest Test Project (.NET Core)&lt;/code> и назовем его &lt;code>CalculatorCLI.Tests&lt;/code>.&lt;/p>
&lt;h3 id="версия-net-core-1">Версия NET Core&lt;/h3>
&lt;p>Как мы делали раньше, как только вы создадите проект, перейдите в свойства проекта и измените &lt;code>Target framework&lt;/code> на &lt;code>.NET Core 3.0&lt;/code>, если это еще не так.&lt;/p>
&lt;p>Затем добавьте ссылку на &lt;code>ConsoleCLI.Core&lt;/code> и &lt;code>ConsoleCLI.Core&lt;/code> в наш недавно созданный тестовый проект.&lt;/p>
&lt;h3 id="код-2">Код&lt;/h3>
&lt;p>Мы собираемся разделить тест на два разных файла: &lt;code>CoreTests.cs&lt;/code> и &lt;code>CLITests.cs&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">CalculatorCLI.Core&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.VisualStudio.TestTools.UnitTesting&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Collections.Generic&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Text&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CalculatorCLI.Tests&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestClass]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CoreTests&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> _left = &lt;span style="color:#a5d6ff">2&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> _right = &lt;span style="color:#a5d6ff">2&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldAdd()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> expectedResult = &lt;span style="color:#a5d6ff">4&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;+&amp;#34;&lt;/span>, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> functionResult = operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.AreEqual(functionResult, expectedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldSubstract()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> expectedResult = &lt;span style="color:#a5d6ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> functionResult = operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.AreEqual(functionResult, expectedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldMultiply()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> expectedResult = &lt;span style="color:#a5d6ff">4&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;*&amp;#34;&lt;/span>, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> functionResult = operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.AreEqual(functionResult, expectedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldDivide()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> expectedResult = &lt;span style="color:#a5d6ff">1&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> functionResult = operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.AreEqual(functionResult, expectedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ExpectedException(typeof(System.DivideByZeroException))]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldThrowExceptionForDivideByZero()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, _left, &lt;span style="color:#a5d6ff">0&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ExpectedException(typeof(System.Exception), &amp;#34;Operator invalid&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldThrowExceptionForWrongOperator()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;text&amp;#34;&lt;/span>, _left, &lt;span style="color:#a5d6ff">0&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.VisualStudio.TestTools.UnitTesting&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Collections.Generic&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Text&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CalculatorCLI.Tests&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestClass]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CLITests&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _left = &lt;span style="color:#a5d6ff">&amp;#34;2&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _right = &lt;span style="color:#a5d6ff">&amp;#34;2&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldAdd()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> args = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;-op&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;+&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-l&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;45&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-r&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;96&amp;#34;&lt;/span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CalculatorCLI.CLI.Program.Main(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldSubstract()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> args = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;-op&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-l&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;45&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-r&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;96&amp;#34;&lt;/span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CalculatorCLI.CLI.Program.Main(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldMultiply()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> args = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;-op&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;*&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-l&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;45&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-r&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;96&amp;#34;&lt;/span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CalculatorCLI.CLI.Program.Main(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldDivide()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> args = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;-op&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-l&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;45&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-r&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;96&amp;#34;&lt;/span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CalculatorCLI.CLI.Program.Main(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>После всего созданного мы получим такое решение:&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/ffecc23a14d796af9a46dbb390c0d072.png" />
&lt;p>Теперь мы можем запускать тесты, поэтому перейдите в &lt;code>Test Explorer&lt;/code> в Visual Studio и запустите их!&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/17d7ac9b2f1e7fb2666a68fc87882eed.png" />
&lt;h1 id="трэвис-ci">Трэвис CI&lt;/h1>
&lt;p>Если вы еще этого не сделали, TravisCI — это размещенная система непрерывной интеграции и развертывания.&lt;/p>
&lt;p>Здесь нам нужно сделать несколько шагов, но сначала мы собираемся связать наш репозиторий Github, чтобы его прослушивали агенты TravisCI, чтобы построить и протестировать наш проект.&lt;/p>
&lt;h2 id="включить-репозиторий">Включить репозиторий&lt;/h2>
&lt;p>Для этого войдите на страницу Travis CI и перейдите к своим репозиториям, затем отфильтруйте созданный вами проект и включите его, щелкнув ползунок рядом с именем репозитория.&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/28b366dddd3f5caa9100ca6b6d200764.png" >
&lt;h2 id="создать-travisymlнам-нужно-создать-файл-с-именем-travisyml-в-корне-вашего-проекта-это-потому-что-как-указано-в-документации">Создать .travis.ymlНам нужно создать файл с именем &lt;code>.travis.yml&lt;/code> в корне вашего проекта, это потому, что &lt;a href="https://docs.travis-ci.com/user/tutorial/">как указано в документации&lt;/a>:&lt;/h2>
&lt;blockquote>
&lt;p>Travis запускает сборки только на основе коммитов, которые вы отправляете после добавления файла .travis.yml.&lt;/p>&lt;/blockquote>
&lt;p>Итак, создайте файл &lt;code>.travis.yml&lt;/code> в корне репозитория со следующими строками:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">language&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">csharp&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">sudo&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">required&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">mono&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">none &lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">dotnet&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">3.0&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">os&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">linux&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">before_script&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">dotnet restore &amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">script&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">dotnet build &amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34; -c Release&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">dotnet test &amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34; -c Release -v n&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Я не буду вдаваться в синтаксис работы файла &lt;code>.travis.yml&lt;/code>, но давайте рассмотрим, что он делает:&lt;/p>
&lt;ol>
&lt;li>Настраиваем, что язык будет &lt;code>csharp&lt;/code>.&lt;/li>
&lt;li>Мы не будем использовать &lt;code>mono&lt;/code>, потому что &lt;code>.NET Core 3.0&lt;/code> будет работать в Linux.&lt;/li>
&lt;li>Устанавливаем версию &lt;code>dotnet&lt;/code> на &lt;code>3.0&lt;/code>.&lt;/li>
&lt;li>Устанавливаем &lt;code>os&lt;/code>, по умолчанию это &lt;code>linux&lt;/code>, но я все равно его добавил.&lt;/li>
&lt;li>Теперь у нас есть &lt;code>before_script&lt;/code> который будет работать перед основной логикой .here, поэтому я поставил задачу запустить &lt;code>dotnet restore&lt;/code> для решения, чтобы потом все загружалось идеально.&lt;/li>
&lt;li>Теперь в &lt;code>script&lt;/code> мы выполним &lt;code>dotnet builld&lt;/code> и &lt;code>dotnet test&lt;/code> для нашего решения, это проверит его компиляцию, а затем запустит тесты.&lt;/li>
&lt;/ol>
&lt;p>Иа, мы закончили!&lt;/p>
&lt;h1 id="загрузить-в-мастер">Загрузить в мастер&lt;/h1>
&lt;p>Теперь нам просто нужно все подтолкнуть к освоению.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-cmd" data-lang="cmd">&lt;span style="display:flex;">&lt;span>git add --all
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>git commit -m &lt;span style="color:#a5d6ff">&amp;#34;Initial files&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>git push
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="проверьте-непрерывную-интеграцию">Проверьте непрерывную интеграцию&lt;/h1>
&lt;p>Мы можем проверить статус CI отправки в &lt;code>master&lt;/code>, которую мы сделали, как на странице репозитория, так и на панели управления TravisCI.&lt;/p>
&lt;h2 id="в-процессе">В процессе&lt;/h2>
&lt;img align="center" src="https://i.gyazo.com/977ea42a90adccf0736464b6603867a5.png" >
&lt;br/>
&lt;br/>
&lt;img align="center" src="https://i.gyazo.com/52a5f9356df5436c862b7df6fe66a9f4.png" >
&lt;h2 id="готово">Готово&lt;/h2>
&lt;img align="center" src="https://i.gyazo.com/c3d3521925a3e20bcf55bf5f6a2a711d.png" >
&lt;br/>
&lt;br/>
&lt;img align="center" src="https://i.gyazo.com/8c749c2ce44837a39fc5cd3e8838a798.png" >
&lt;h1 id="давайте-сломаем-это">Давайте сломаем это&lt;/h1>
&lt;p>Теперь, чтобы увидеть, насколько это мощно, давайте сломаем код и изменим основную библиотеку, чтобы она не работала.&lt;/p>
&lt;h2 id="изменения-кода">Изменения кода&lt;/h2>
&lt;p>Итак, перейдите в &lt;code>Operation.cs&lt;/code> и измените что-нибудь, что нарушит некоторые тесты.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CalculatorCLI.Core&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">enum&lt;/span> OperatorsEnum
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ADD,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> SUBSTRACT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MULTIPLY,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DIVIDE
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Operation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> OperatorsEnum OperatorEnum { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> LeftValue { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> RightValue { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Operation(&lt;span style="color:#ff7b72">string&lt;/span> operatorString, &lt;span style="color:#ff7b72">int&lt;/span> leftValue, &lt;span style="color:#ff7b72">int&lt;/span> rightValue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (operatorString)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;+&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.SUBSTRACT;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.SUBSTRACT;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;*&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.MULTIPLY;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.DIVIDE;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;Operator invalid&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> LeftValue = leftValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RightValue = rightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> DoOperation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (OperatorEnum)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.ADD:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue + RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.SUBSTRACT:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue - RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.MULTIPLY:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue * RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.DIVIDE:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue / RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;Operator is not valid&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>И если мы запустим тест еще раз, поскольку мы изменили регистр на сложение, он завершится неудачей:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;+&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.SUBSTRACT;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Как и ожидалось, в случае &lt;code>ShouldAdd&lt;/code> это не удалось:&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/a57d0a0f8c07cc1ab6e4f55a8466cbbd.png" >
&lt;p>Теперь зафиксируйте это изменение, отправьте его мастеру и дождитесь результатов от агента TravisCI.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>git add --all
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>git commit -m &lt;span style="color:#a5d6ff">&amp;#34;Breaking changes&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>git push
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="сборка">Сборка&lt;/h2>
&lt;p>Теперь давайте зайдем в логи TravisCI и увидим, что мы успешно сломали проект, потому что интеграционный тест не пройден, а статус сборки — ошибка.&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/397befe9b7f5a32b6e97511733296b00.png" >
&lt;p>В самом конце лога мы видим саму ошибку:&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/e5cbf32c5dd7a08bf6628d81edff3130.png" >
&lt;h2 id="давайте-исправим-это-еще-раз">Давайте исправим это еще раз!&lt;/h2>
&lt;p>Теперь верните то, что мы сделали, отправьте код мастеру и проверьте статус новой сборки.&lt;/p>
&lt;p>Тесты проходят успешно:&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/02deb8fcb4fca618ff2d79f1c27c6df5.png" >
&lt;p>И сборка тоже удалась:&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/38a227f0634c6040c6608f8c51f36cd3.png" >
&lt;h1 id="заключение">Заключение&lt;/h1>
&lt;p>&lt;strong>Он действительно очень мощный&lt;/strong>, CI и CD существуют уже давно, но теперь его довольно просто запустить в каждом отдельном проекте, неважно, насколько он мал или прост.С моей точки зрения, каждый должен хотя бы настроить CI для каждого из своих проектов, потому что это хорошая практика, и в конечном итоге это сэкономит вам время на отладку и поиск ошибок, которые не должны возникать, если вы установили правильные &lt;code>tests&lt;/code> и CI.&lt;/p>
&lt;h1 id="вот-и-все">Вот и все&lt;/h1>
&lt;p>Вот и все, как создать решение .NET Core 3.0 с непрерывной интеграцией в каждой сборке с использованием TravisCI и сохранением кода в Github.&lt;/p>
&lt;p>Вы можете найти исходный код этого проекта &lt;a href="https://github.com/emimontesdeoca/CalculatorCLI-demo">здесь&lt;/a>.&lt;/p></content:encoded><category>.NET</category><category>CI/CD</category></item><item><title>Мой взгляд на кэш в памяти</title><link>https://emimontesdeoca.github.io/ru/posts/my-take-on-in-memory-cache/</link><pubDate>Tue, 03 Sep 2019 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/my-take-on-in-memory-cache/</guid><description>Создайте собственную реализацию кэша в памяти с поддержкой срока действия, используя C# и универсальные шаблоны.</description><content:encoded>&lt;p>Я работал над некоторыми вещами, которые обрабатывали большой объем данных. Делая это, я понял, что большая часть этого никогда не меняется, или, по крайней мере, в течение определенного времени это не так.&lt;/p>
&lt;p>Поэтому я подумал, что было бы полезно создать личный репозиторий кэша, конечно, это не ново, несколько недель назад я прочитал об этом в [посте] StackOverflow (&lt;a href="https://nickcraver.com/blog/2019/08/06/stack-overflow-how-we-do-app-caching/#in-memory--redis-cache%29">https://nickcraver.com/blog/2019/08/06/stack-overflow-how-we-do-app-caching/#in-memory--redis-cache)&lt;/a>, написанном &lt;a href="https://nickcraver.com/">Ником Крейвером&lt;/a> о том, как они управляют кешем приложений.&lt;/p>
&lt;p>Кроме того, я всегда хотел разобраться с кешем, узнать, как он работает, его логику и как заставить его работать, &lt;em>так&amp;hellip; почему бы и нет!&lt;/em>&lt;/p>
&lt;h2 id="поток">Поток&lt;/h2>
&lt;p>Вот краткий обзор того, как будет вести себя класс, который управляет кешем, когда пользователь запрашивает значение.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/830d5a91089c3344c8b406c66ea547b8">&lt;img src="https://i.gyazo.com/830d5a91089c3344c8b406c66ea547b8.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h2 id="реализация">Реализация&lt;/h2>
&lt;h3 id="прежде-чем-мы-начнем">Прежде чем мы начнем&lt;/h3>
&lt;p>Я знаю, я знаю. Есть &lt;a href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.caching.memorycache?view=netframework-4.8">System.Runtime.Caching&lt;/a>, который обрабатывает кеш памяти. Но я решил создать его сам. Если вы хотите использовать этот класс, проверьте &lt;a href="https://stackoverflow.com/search?q=System.Runtime.Caching">здесь&lt;/a> инструкции.&lt;/p>
&lt;p>###Элемент кэша&lt;/p>
&lt;p>Первый шаг — создать класс, в котором будет храниться значение объекта и дата истечения срока действия. Вероятно, есть лучший способ сделать это, но я так подумал, так что вот:&lt;/p>
&lt;p>[[[ТОК_6]]]&lt;/p>
&lt;p>###Репозиторий кэша&lt;/p>
&lt;p>Затем нам нужен класс, который обрабатывает объекты и где-то их сохраняет (как &lt;code>CacheItem&lt;/code>). Мне нравится обрабатывать все данные/модели в классах, имеющих суфикс &lt;code>Repository&lt;/code>, &lt;em>но это не обязательно&lt;/em>, поэтому давайте создадим один для кэширования.&lt;/p>
&lt;p>[[[ТОК_9]]]&lt;/p>
&lt;p>На данный момент у нас есть статический словарь под названием &lt;code>Cache&lt;/code>, в котором будут храниться все элементы. Помните, что это будет продолжаться только во время работы приложения, поэтому в этом &lt;em>учебнике&lt;/em> есть кэширование в памяти.&lt;/p>
&lt;p>&lt;em>Имейте в виду, что элемент &lt;code>Cache&lt;/code> будет инициализирован после загрузки класса &lt;code>CacheRepository&lt;/code>.&lt;/em>&lt;/p>
&lt;p>Единственный метод, доступный при вызове класса CacheRepository, — это &lt;code>GetOrSet(string key, Func&amp;lt;T&amp;gt; lookup, TimeSpan durationMinutes)&lt;/code> для которого требуется три параметра:&lt;/p>
&lt;ol>
&lt;li>&lt;code>key&lt;/code>: идентификатор объекта для сохранения.&lt;/li>
&lt;li>&lt;code>lookup&lt;/code>: функция обратного вызова в случае, если срок действия кеша истек или он равен нулю.&lt;/li>
&lt;li>&lt;code>durationMinutes&lt;/code>: продолжительность в минутах, которая будет добавлена ​​к текущему времени (в формате UTC).&lt;/li>
&lt;/ol>
&lt;h2 id="время-кэширования">Время кэширования&lt;/h2>
&lt;p>Теперь используйте наш репозиторий кэширования, чтобы получить откуда-нибудь данные.&lt;/p>
&lt;p>Чтобы все это имело смысл, давайте создадим пример объекта с некоторыми свойствами, а затем репозиторий для извлечения и заполнения списка этого объекта.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">User&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Guid Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Email { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">UserRepository&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;User&amp;gt; Get()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Code to get users&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Поскольку у нас есть метод заполнения списка &lt;code>User&lt;/code>, давайте воспользуемся классом &lt;code>CacheRepository&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> UserRepository _userRepository = &lt;span style="color:#ff7b72">new&lt;/span> UserRepository();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;User&amp;gt; _user;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;User&amp;gt; User
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">get&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _user = CacheRepository.GetOrSet(&lt;span style="color:#a5d6ff">$&amp;#34;users&amp;#34;&lt;/span>, usersRepo.Get, TimeSpan.FromMinutes(&lt;span style="color:#a5d6ff">10&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> _user;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>И точно так же, каждый раз, когда вы обращаетесь к переменной &lt;code>User&lt;/code>, она запрашивает у &lt;code>CacheRepository&lt;/code> значение объекта, имеющего ключ &lt;code>users&lt;/code>.&lt;/p>
&lt;p>Если этот ключ существует, он проверит дату истечения срока действия. Если какое-либо из этих условий ложно, он будет использовать обратный вызов для установки значения (с помощью &lt;code>usersRepo.Get&lt;/code>) объекта, сохранения его в кэше с датой истечения срока действия, установленной в &lt;code>DateTime.UtcNow + TimeSpan.FromMinutes(10)&lt;/code> и возврата его.&lt;/p></content:encoded><category>.NET</category></item><item><title>Делайте скриншоты страниц с помощью Devtools</title><link>https://emimontesdeoca.github.io/ru/posts/google-chrome-screenshot-tool/</link><pubDate>Thu, 09 May 2019 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/google-chrome-screenshot-tool/</guid><description>Делайте скриншоты всей страницы и области с помощью встроенных команд Chrome DevTools для создания снимков экрана.</description><content:encoded>&lt;p>Бывали случаи, когда мне нужно было показать клиенту или товарищу по команде полный снимок экрана текущего состояния веб-сайта или даже его части, поэтому я использовал &lt;a href="https://chrome.google.com/webstore/detail/full-page-screen-capture/fdpohaocaechififmbbbbbknoalclacl?hl=en">Снимок экрана всей страницы&lt;/a>, а для любых других видов снимков экрана — &lt;a href="https://support.microsoft.com/en-us/help/13776/windows-use-snipping-tool-to-capture-screenshots">Инструмент Windows Snippet&lt;/a> или &lt;a href="https://www.techsmith.com/store/snagit">Snaggit&lt;/a>. Если я хочу разместить его в Интернете, как в этом посте &lt;a href="https://gyazo.com">Gyazo&lt;/a>, в котором также есть создатель GIF.&lt;/p>
&lt;p>Недавно я обнаружил, что в &lt;strong>Devtools&lt;/strong> есть инструмент, который делает скриншот всей страницы без расширения!&lt;/p>
&lt;p>Это не новая мысль, она была включена в &lt;a href="https://developers.google.com/web/updates/2017/04/devtools-release-notes">обновление Devtools в апреле 2017 года&lt;/a>, &lt;em>но похоже, что я живу под камнем и не узнал об этом до сих пор&amp;hellip;&lt;/em>&lt;/p>
&lt;h2 id="инструменты-разработчика">Инструменты разработчика&lt;/h2>
&lt;p>Как указано на &lt;a href="https://developers.google.com/web/tools/chrome-devtools/?hl=en">официальной странице Devtools&lt;/a>:&lt;/p>
&lt;blockquote>
&lt;p>Chrome DevTools — это набор инструментов веб-разработчика, встроенных непосредственно в браузер Google Chrome. DevTools может помочь вам оперативно редактировать страницы и быстро диагностировать проблемы, что в конечном итоге поможет вам быстрее создавать более качественные веб-сайты.&lt;/p>&lt;/blockquote>
&lt;h2 id="запуск-команд-в-devtools">Запуск команд в Devtools&lt;/h2>
&lt;p>Вы уже знаете, как открыть Devtools, но если вы просто забыли и &lt;em>ради этого поста&lt;/em>, откройте его с помощью ключа &lt;code>F12&lt;/code> или ярлыка &lt;code>Ctrl + Shift + I&lt;/code>. Затем вам нужно открыть &lt;code>Run command&lt;/code> в меню Devtools или использовать &lt;code>Ctrl + Shift + P&lt;/code>.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/8209d7d3132efba1843a3d51e4ad2183">&lt;img src="https://i.gyazo.com/8209d7d3132efba1843a3d51e4ad2183.gif" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h2 id="инструмент-создания-снимков-экрана">Инструмент создания снимков экрана&lt;/h2>
&lt;p>&lt;a href="https://gyazo.com/25ea6c9d258a1117efca5a2d92f715e2">&lt;img src="https://i.gyazo.com/25ea6c9d258a1117efca5a2d92f715e2.gif" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Если вы наберете &lt;code>screenshot&lt;/code> команды будут фильтроваться, вы можете использовать 4 типа методов создания снимков экрана:&lt;/p>
&lt;ol>
&lt;li>Скриншот области&lt;/li>
&lt;li>Скриншот в полный размер.&lt;/li>
&lt;li>Скриншот узла&lt;/li>
&lt;li>Сделать снимок экрана&lt;/li>
&lt;/ol>
&lt;p>Прежде чем проверять все методы создания снимков экрана, после того, как devtools завершит обработку изображения, &lt;strong>он автоматически загрузит его с именем веб-страницы&lt;/strong>.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/89a4935eb0ddb1a06ae997551fd19677">&lt;img src="https://i.gyazo.com/89a4935eb0ddb1a06ae997551fd19677.gif" alt="Изображение от Гьязо">&lt;/a>&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/e2e29c7fd7e90bb8bfd36ef130793394">&lt;img src="https://i.gyazo.com/e2e29c7fd7e90bb8bfd36ef130793394.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h3 id="скриншот-области">Скриншот области&lt;/h3>
&lt;p>&lt;code>area screenshot&lt;/code> позволит вам выбрать часть сайта и сделать снимок экрана.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/f508a479893b6e8af9234d6a77404b26">&lt;img src="https://i.gyazo.com/f508a479893b6e8af9234d6a77404b26.gif" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Результат:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/2b096e1ca2974157e478587a4902c1d2">&lt;img src="https://i.gyazo.com/2b096e1ca2974157e478587a4902c1d2.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h3 id="скриншот-в-полном-размере">Скриншот в полном размере&lt;/h3>
&lt;p>&lt;code>full size screenshot&lt;/code> сделает снимок экрана всей веб-страницы, от начала до конца.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/e6c30b8eafbef04091bf66c16429017c">&lt;img src="https://i.gyazo.com/e6c30b8eafbef04091bf66c16429017c.gif" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Результат:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/9519ca32c0029a36432b700268741280">&lt;img src="https://i.gyazo.com/9519ca32c0029a36432b700268741280.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h3 id="скриншот-узла">Скриншот узла&lt;/h3>
&lt;p>Команда &lt;code>node screenshot&lt;/code> позволит вам сделать снимок экрана выбранного узла в элементе проверки.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/5b936026735964bb789c4bcae02bb792">&lt;img src="https://i.gyazo.com/5b936026735964bb789c4bcae02bb792.gif" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Результат:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/56089c3d89f9b059dfb018d18572276d">&lt;img src="https://i.gyazo.com/56089c3d89f9b059dfb018d18572276d.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h3 id="сделать-скриншот">Сделать скриншот&lt;/h3>
&lt;p>&lt;code>capture screenshot&lt;/code> сделает снимок экрана текущей страницы без прокрутки и размера браузера.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/6dc4f654e8f218e44fcc1fb51ce4c9f5">&lt;img src="https://i.gyazo.com/6dc4f654e8f218e44fcc1fb51ce4c9f5.gif" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Результат:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/3f15d4a004cba6152d1f9606c20540cc">&lt;img src="https://i.gyazo.com/3f15d4a004cba6152d1f9606c20540cc.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p></content:encoded></item><item><title>Пользовательские языки с использованием встроенных и внешних ресурсов в .NET Framework.</title><link>https://emimontesdeoca.github.io/ru/posts/embedded-resources-and-external-resources/</link><pubDate>Mon, 06 May 2019 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/embedded-resources-and-external-resources/</guid><description>Реализуйте поддержку нескольких языков, используя встроенные и внешние файлы ресурсов .resx в .NET Framework.</description><content:encoded>&lt;h1 id="введение">Введение&lt;/h1>
&lt;p>На работе мы столкнулись с обычной проблемой, с которой, я почти уверен, приходится сталкиваться многим разработчикам, а именно с языками и специальными языками клиентов, которые предоставляют наши услуги своим клиентам.&lt;/p>
&lt;p>Например, предположим, что базовый язык использует неформальный язык, а клиент хочет использовать формальный язык, потому что его клиенты — пожилые люди.&lt;/p>
&lt;p>Также обратите внимание, что это решение предназначено для нескольких языков, мы будем использовать английский и испанский.&lt;/p>
&lt;p>&lt;em>Мой родной язык не английский. Приношу извинения за ошибки в уроке. Если вы обнаружите ошибки и захотите их исправить, вы можете открыть запрос на включение в &lt;a href="https://github.com/emimontesdeoca/emimontesdeoca.github.io">этот репозиторий&lt;/a>, и я с радостью его одобрю!&lt;/em>&lt;/p>
&lt;h1 id="ресурсы">Ресурсы&lt;/h1>
&lt;p>Ресурсы — это XML-файлы с расширением &lt;code>.resx&lt;/code> со структурой ключ/значение, которые выглядят следующим образом:&lt;/p>
&lt;p>[[[ТОК_2]]]&lt;/p>
&lt;p>Мы будем создавать ресурсы для двух разных языков: &lt;strong>английского&lt;/strong> и &lt;strong>испанского&lt;/strong>, поэтому номенклатура файлов будет такой: &lt;code>resources.ISOLANGUAGECODE.resx&lt;/code>, например: &lt;code>resource.es.resx&lt;/code> для испанского и &lt;code>resource.resx&lt;/code> для английского. Если позже мы захотим добавить немецкий, файл будет называться &lt;code>resource.de.resx&lt;/code>.&lt;/p>
&lt;h2 id="встроенный-ресурс">Встроенный ресурс&lt;/h2>
&lt;p>Встроенные ресурсы, при компиляции проекта, они будут добавлены внутрь &lt;code>dll&lt;/code>.&lt;/p>
&lt;p>Вот изображение со встроенным ресурсом, как видите, файл &lt;code>Resource.resx&lt;/code> не виден при компиляции.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/b696d4ff1129634477c0fe3d570a05e8">&lt;img src="https://i.gyazo.com/b696d4ff1129634477c0fe3d570a05e8.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h2 id="внешний-ресурс">Внешний ресурс&lt;/h2>
&lt;p>С другой стороны, внешние или невстроенные ресурсы — это ресурсы, которые будут добавлены в папку после компиляции.&lt;/p>
&lt;p>Файлы ресурсов (&lt;code>.resx&lt;/code>) будут находиться в папке &lt;code>Properties&lt;/code>&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/db83dc7bb9e3d3fb521cb321eaa8e74a">&lt;img src="https://i.gyazo.com/db83dc7bb9e3d3fb521cb321eaa8e74a.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h1 id="сборка-проекта">Сборка проекта&lt;/h1>
&lt;p>Начнем с консольного проекта в .NET Framework.&lt;/p>
&lt;h2 id="создание-встроенного-файла-ресурсов">Создание встроенного файла ресурсов&lt;/h2>
&lt;p>Добавьте файл ресурсов с несколькими ключами и убедитесь, что это встроенный ресурс, который позже мы будем использовать для обновления с помощью внешних ресурсов.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/79af1981a3ee3214089791a3511b94ba">&lt;img src="https://i.gyazo.com/79af1981a3ee3214089791a3511b94ba.png" alt="Изображение от Гьязо">&lt;/a>&lt;/p>
&lt;h2 id="создание-внешнего-файла-ресурсов">Создание внешнего файла ресурсов&lt;/h2>
&lt;p>Чтобы отделить внешние ресурсы от встроенных, мы добавим внешние ресурсы в папку с именем, чтобы мы могли легко получить к ним доступ позже.&lt;/p>
&lt;p>&lt;em>Совет: чтобы добавить папку в папку «Свойства», создать ее снаружи и переместить внутрь, Visual Studio не позволяет вам ее создать&lt;/em>&lt;/p>
&lt;p>Сделайте то же самое, что мы сделали для встроенного ресурса, затем перейдите к &lt;code>properties&lt;/code> файла и внутри &lt;code>Advanced&lt;/code>, &lt;code>Build action&lt;/code> и измените его на: &lt;code>Content&lt;/code> и измените &lt;code>Copy to Output Dictionary&lt;/code> на &lt;code>Copy if newer&lt;/code>.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/6f7d53c82760b6989d74dc1864bbc2d4">&lt;img src="https://i.gyazo.com/6f7d53c82760b6989d74dc1864bbc2d4.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Вот как это должно выглядеть:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/75b2f06b11fa99b01bca7b5ac64b4e35">&lt;img src="https://i.gyazo.com/75b2f06b11fa99b01bca7b5ac64b4e35.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h2 id="добавляем-ключ-в-appconfig">Добавляем ключ в app.config&lt;/h2>
&lt;p>Поскольку мы классные разработчики и любим, чтобы все было сделано хорошо и &lt;strong>НЕ&lt;/strong> менялось код для каждого клиента, давайте добавим ключ в &lt;code>appconfig&lt;/code>, который будет содержать имя папки, в которой мы будем искать файлы ресурсов.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;utf-8&amp;#34; ?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;configuration&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;startup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;supportedRuntime&lt;/span> version=&lt;span style="color:#a5d6ff">&amp;#34;v4.0&amp;#34;&lt;/span> sku=&lt;span style="color:#a5d6ff">&amp;#34;.NETFramework,Version=v4.7.2&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/startup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;appSettings&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;add&lt;/span> key=&lt;span style="color:#a5d6ff">&amp;#34;CustomResources.Folder&amp;#34;&lt;/span> value=&lt;span style="color:#a5d6ff">&amp;#34;John&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/appSettings&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/configuration&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>```Поэтому позже, когда мы будем искать ресурсы, они будут искать в `Properties.John` вместо `Properties.Doe`.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Кроме того, это упрощает внесение изменений, когда приложение уже развернуто, поскольку вы можете легко изменить файл app.config.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>## Доступ к встроенным файлам ресурсов
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Использовать .NET Framework довольно просто, поэтому для доступа к встроенным свойствам нам просто нужно использовать объект `Properties` , который будет иметь файлы ресурсов в качестве свойства, а внутри этого объекта будут все ключи/значения, которые есть в файле `resx`.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>```csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>static void Main(string[] args)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> var hello = Properties.Resource.Action_greeting;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> var bye = Properties.Resource.Action_cancel;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine($&amp;#34;Action_greeting value: {hello}, Action_cancel value: {bye}&amp;#34;);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Read();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Запустив это, мы должны показать что-то вроде этого:&lt;/p>
&lt;p>&lt;code>Action_greeting value: Hello, Action_cancel value: Cancel&lt;/code>&lt;/p>
&lt;h2 id="доступ-к-файлам-внешних-ресурсов">Доступ к файлам внешних ресурсов&lt;/h2>
&lt;p>Эта часть практически такая же: вы можете получить доступ к файлам ресурсов с помощью объекта &lt;code>Properties&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> hello = Properties.Resource.Action_greeting;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> bye = Properties.Resource.Action_cancel;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> johnhello = Properties.John.Resource.Action_greeting;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> johnbye = Properties.John.Resource.Action_cancel;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> doehello = Properties.Doe.Resource.Action_greeting;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> doebye = Properties.Doe.Resource.Action_cancel;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="проблема">Проблема&lt;/h1>
&lt;p>Основная проблема при использовании этого метода заключается в том, что каждый ключ является свойством внутри объекта, поэтому нам приходится вызывать его так, как мы видели раньше. Если вы хотите вызвать ключ &lt;code>Action_greeting&lt;/code> файла ресурсов &lt;code>John&lt;/code> мы должны использовать следующий &lt;code>Properties.John&lt;/code> и затем &lt;code>Resource.Action_greeting&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Вот в чём проблема.&lt;/strong>&lt;/p>
&lt;p>Это потому, что если мы разрабатываем приложение для большого количества клиентов, менять способ вызова файлов ресурсов для каждого из них — плохая идея.&lt;/p>
&lt;p>&lt;em>Можете ли вы себе такое представить?&lt;/em> Компилируем приложение для каждого клиента и меняем &lt;code>John&lt;/code> на &lt;code>Doe&lt;/code>, а затем на что-то еще. &lt;strong>Это безумие!&lt;/strong>&lt;/p>
&lt;h1 id="решение">Решение&lt;/h1>
&lt;p>Наш руководитель группы придумал довольно хороший метод, что-то вроде резервной системы: у нас должна быть базовая модель ресурсов, а затем для каждого из клиентов есть файл ресурсов, который будет обновлять файл своими ресурсами, и в итоге мы получаем единый список ресурсов.&lt;/p>
&lt;p>Если клиенту не нужны специальные ресурсы, мы используем базовые ресурсы, а если они им нужны, мы используем их.&lt;/p>
&lt;p>Чтобы внести это в контрольный список, нам нужно сделать:&lt;/p>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> Найдите способ сопоставить все ключи/значения со словарем для встроенных ресурсов.&lt;/li>
&lt;li>[] Найдите способ сопоставить все ключи/значения со словарем для внешних ресурсов.&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> Смешайте оба файла и получите один словарь для каждого языка.&lt;/li>
&lt;li>[] Создать метод, который обращается к словарю и возвращает значение&lt;/li>
&lt;/ul>
&lt;h2 id="диаграмма">Диаграмма&lt;/h2>
&lt;p>&lt;a href="https://gyazo.com/c7e9ae6c7792c3bace10ff9b3b2b08ee">&lt;img src="https://i.gyazo.com/c7e9ae6c7792c3bace10ff9b3b2b08ee.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h2 id="давайте-напишем-код">Давайте напишем код&lt;/h2>
&lt;p>Прежде всего, давайте создадим отдельный класс, в котором будет вся наша логика: от получения файлов ресурсов до их смешивания и возврата значения. Этот класс будет называться &lt;code>CustomResources&lt;/code>.&lt;/p>
&lt;p>Вот как это выглядит:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CustomResources&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; _ResourcesEnglish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ResourcesEnglish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; _ResourcesSpanish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ResourcesSpanish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; OverwriteDictionary(Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; currentDictionary, Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; newDictionary, &lt;span style="color:#ff7b72">bool&lt;/span> addIfDoesntExist = &lt;span style="color:#79c0ff">false&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetDictionaryFromEmbedded(&lt;span style="color:#ff7b72">string&lt;/span> embedded, &lt;span style="color:#ff7b72">string&lt;/span> cultureInfoCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetDictionaryFromFile(&lt;span style="color:#ff7b72">string&lt;/span> file)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetText(&lt;span style="color:#ff7b72">string&lt;/span> key)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetText(&lt;span style="color:#ff7b72">string&lt;/span> key, &lt;span style="color:#ff7b72">string&lt;/span> language)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Обратите внимание, что мы реализуем отложенную загрузку свойств, что помогает повысить производительность и позволяет однократно загружать словарь.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>GetDictionaryFromEmbedded&lt;/code>: возвращает словарь из встроенных ресурсов.&lt;/li>
&lt;li>&lt;code>GetDictionaryFromFile&lt;/code>: возвращает словарь из внешних ресурсов.&lt;/li>
&lt;li>&lt;code>OverwriteDictionary&lt;/code>: смешивает два словаря и возвращает один.&lt;/li>
&lt;li>&lt;code>GetText&lt;/code>: возвращает значение по ключу.&lt;/li>
&lt;/ul>
&lt;h2 id="от-встроенного-ресурса-к-словарю">От встроенного ресурса к словарю&lt;/h2>
&lt;p>Нам нужно получить все свойства из XML-файла и вернуть словарь:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetDictionaryFromEmbedded(&lt;span style="color:#ff7b72">string&lt;/span> embedded, &lt;span style="color:#ff7b72">string&lt;/span> cultureInfoCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; res = &lt;span style="color:#ff7b72">new&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ResourceManager rm = &lt;span style="color:#ff7b72">new&lt;/span> ResourceManager(embedded, Assembly.GetExecutingAssembly());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> resourceSet = rm.GetResourceSet(&lt;span style="color:#ff7b72">new&lt;/span> CultureInfo(cultureInfoCode), &lt;span style="color:#79c0ff">true&lt;/span>, &lt;span style="color:#79c0ff">true&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> resourceDictionary = resourceSet.Cast&amp;lt;DictionaryEntry&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToDictionary(r =&amp;gt; r.Key.ToString(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> r =&amp;gt; r.Value.ToString());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res = resourceDictionary;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> a = e.Message;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Error getting resource file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> res;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Две вещи:- Обратите внимание, что ему нужен параметр с именем &lt;code>embedded&lt;/code>, который parementers — это имя файла, который вы можете увидеть в дизайнере, в нашем случае это: &lt;code>resources-demo.Properties.Resource&lt;/code>.&lt;/p>
&lt;ul>
&lt;li>Также у нас есть параметрcultreInfoCode, который является кодом выбираемого языка. К счастью для нас, .NET Framework делает всю работу за нас, и нам не нужно ничего делать, просто установите английский или испанский язык, и он выберет между &lt;code>resource.es.resx&lt;/code> или &lt;code>resource.resx&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="из-внешнего-ресурса-в-словарь">Из внешнего ресурса в словарь&lt;/h2>
&lt;p>Получение файла из файла немного сложное, но несложное: нам нужно получить текущее местоположение исполняемого файла, соединить местоположение файла ресурсов, а затем проанализировать его в словаре.&lt;/p>
&lt;p>Но сначала вам нужно добавить ссылку на &lt;code>System.Windows.Forms&lt;/code>, чтобы получить доступ к &lt;code>ResXResourceReader&lt;/code>.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/5bdf8353acc57b1d95b72acc14af41cd">&lt;img src="https://i.gyazo.com/5bdf8353acc57b1d95b72acc14af41cd.png" alt="Изображение от Гьязо">&lt;/a>&lt;/p>
&lt;p>Теперь перейдем к нашему методу &lt;code>GetDictionaryFromFile&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetDictionaryFromFile(&lt;span style="color:#ff7b72">string&lt;/span> file)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; res = &lt;span style="color:#ff7b72">new&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> currentPath = (System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase) + file).Replace(&lt;span style="color:#a5d6ff">&amp;#34;file:\\&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> (ResXResourceReader resxReader = &lt;span style="color:#ff7b72">new&lt;/span> ResXResourceReader(currentPath))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (DictionaryEntry entry &lt;span style="color:#ff7b72">in&lt;/span> resxReader)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res.Add((&lt;span style="color:#ff7b72">string&lt;/span>)entry.Key, (&lt;span style="color:#ff7b72">string&lt;/span>)entry.Value);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> a = e.Message;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> res;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>В параметрах файла необходимо указать расположение от исполняемого файла до файла ресурсов, в нашем случае это: &lt;code>&amp;quot;\\Properties\\John\\Resource.resx&amp;quot;&lt;/code>.&lt;/p>
&lt;h2 id="смешивание-словарей">Смешивание словарей&lt;/h2>
&lt;p>Мы почти закончили, во-первых, не забудьте добавить &lt;code>System.Configuration&lt;/code> к ссылкам, чтобы вы могли получить доступ к &lt;code>app.settings&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; OverwriteDictionary(Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; currentDictionary, Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; newDictionary, &lt;span style="color:#ff7b72">bool&lt;/span> addIfDoesntExist = &lt;span style="color:#79c0ff">false&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> identifier = ConfigurationManager.AppSettings[&lt;span style="color:#a5d6ff">&amp;#34;CustomResources.Folder&amp;#34;&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (String.IsNullOrEmpty(identifier))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> currentDictionary;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> item &lt;span style="color:#ff7b72">in&lt;/span> newDictionary)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentDictionary[item.Key] = item.Value;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span>(addIfDoesntExist)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentDictionary.Add(item.Key, item.Value);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> currentDictionary;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="получение-текста-из-словаря">Получение текста из словаря&lt;/h2>
&lt;p>Создайте общедоступный метод, который вызывает закрытый метод, который выбирает язык:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetText(&lt;span style="color:#ff7b72">string&lt;/span> key, &lt;span style="color:#ff7b72">string&lt;/span> language)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (language)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> ResourceSpanish[key];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;en&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> ResourceEnglish[key];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;No value with key: {key} and language: {language}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetText(&lt;span style="color:#ff7b72">string&lt;/span> key)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> GetText(key, Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;No value with key: {key}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Обратите внимание, что вам придется изменить переключатель, если вы добавляете больше языков.&lt;/strong>&lt;/p>
&lt;h2 id="добавление-кода-в-метод-получения-свойств">Добавление кода в метод получения свойств&lt;/h2>
&lt;p>Поскольку сейчас у нас есть все методы, мы можем изменить метод получения публичного свойства, чтобы получать значения.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; _ResourcesEnglish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ResourcesEnglish
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">get&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (_ResourcesEnglish == &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> folderIndentifier = ConfigurationManager.AppSettings[&lt;span style="color:#a5d6ff">&amp;#34;CustomResources.Folder&amp;#34;&lt;/span>]; ;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> baseResources = GetDictionaryFromEmbedded(&lt;span style="color:#a5d6ff">&amp;#34;resources-demo.Properties.Resource&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> customResources = GetDictionaryFromFile(&lt;span style="color:#a5d6ff">$&amp;#34;\\Properties\\{folderIndentifier}\\Resource.resx&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;en&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ResourcesEnglish = OverwriteDictionary(baseResources, customResources);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> _ResourcesEnglish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; _ResourcesSpanish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ResourcesSpanish
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">get&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (_ResourcesSpanish == &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> folderIndentifier = ConfigurationManager.AppSettings[&lt;span style="color:#a5d6ff">&amp;#34;CustomResources.Folder&amp;#34;&lt;/span>]; ;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> baseResources = GetDictionaryFromEmbedded(&lt;span style="color:#a5d6ff">&amp;#34;resources-demo.Properties.Resources&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> customResources = GetDictionaryFromFile(&lt;span style="color:#a5d6ff">$&amp;#34;\\Properties\\{folderIndentifier}\\Resources.es.resx&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ResourcesSpanish = OverwriteDictionary(baseResources, customResources);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> _ResourcesSpanish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol>
&lt;li>Сначала мы получаем идентификатор из файла app.settings.&lt;/li>
&lt;li>Затем получаем базовые ресурсы, встроенные.&lt;/li>
&lt;li>После этого мы получаем пользовательские ресурсы, для этого нам нужно имя папки (которое является идентификатором).&lt;/li>
&lt;li>Затем смешиваем их и возвращаем значение.&lt;/li>
&lt;/ol>
&lt;p>Все это будет сделано один раз, из-за ленивой загрузки.&lt;/p>
&lt;h1 id="тестирование">Тестирование&lt;/h1>
&lt;p>Все, что связано с кодом, закончено, теперь давайте его протестируем, чтобы получить значение из словаря нам нужно вызвать метод &lt;code>CustomResources.GetText(string key)&lt;/code> который возвращает значение.&lt;/p>
&lt;h2 id="обновление-целых-файлов-ресурсов">Обновление целых файлов ресурсов&lt;/h2>
&lt;p>Это тестирование представляет собой тот случай, когда мы хотим обновить весь ключ/значение файлов ресурсов, как вы можете видеть на изображениях, у нас одинаковые ключи, но разные значения.&lt;/p>
&lt;p>Мы будем тестировать &lt;code>John&lt;/code>, и для того, чтобы это установить, нам нужно установить в app.config значение &lt;code>&amp;lt;add key=&amp;quot;CustomResources.Folder&amp;quot; value=&amp;quot;John&amp;quot; /&amp;gt;&lt;/code>.&lt;/p>
&lt;p>Теперь давайте проверим наш базовый файл ресурсов (&lt;code>Properties/Resource.es.resx&lt;/code>):&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/26d6cef147ca3cdd93a1d6cc4c60c212">&lt;img src="https://i.gyazo.com/26d6cef147ca3cdd93a1d6cc4c60c212.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>И затем наш внешний файл ресурсов (&lt;code>Properties/John/Resource.es.resx&lt;/code>):&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/fd6582da0d6a427c2b21cc8b1999f9e0">&lt;img src="https://i.gyazo.com/fd6582da0d6a427c2b21cc8b1999f9e0.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Окей, все настроено, запускаем консольное приложение, остановимся на части свойств &lt;code>get&lt;/code> и все проверим&lt;/p>
&lt;p>&lt;code>folderIdentifier&lt;/code> имеет значение настройки приложения:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/ac30d3deb7abe29a877b368d9300b990">&lt;img src="https://i.gyazo.com/ac30d3deb7abe29a877b368d9300b990.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>&lt;code>baseResources&lt;/code> имеет значение базовых ресурсов, встроенных:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/a61410b365b55d4735f2b2622a18b966">&lt;img src="https://i.gyazo.com/a61410b365b55d4735f2b2622a18b966.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>&lt;code>customResources&lt;/code> содержит значения внешних ресурсов внутри папки &lt;code>John&lt;/code>:&lt;a href="https://gyazo.com/0e93733fbd616b274a69cdffbc16afb1">&lt;img src="https://i.gyazo.com/0e93733fbd616b274a69cdffbc16afb1.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>И, наконец, &lt;code>_ResourcesSpanish&lt;/code> имеет значение, смешанное с базовыми ресурсами и внешними ресурсами.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/0e25a79023364ea1d5be2109dbf6492c">&lt;img src="https://i.gyazo.com/0e25a79023364ea1d5be2109dbf6492c.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h2 id="обновление-только-одного">Обновление только одного&lt;/h2>
&lt;p>Теперь давайте протестируем то же самое, но по другому сценарию: просто обновим один ключ, а другой оставим таким же.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/9c2a05fb976f1f671b13c9cf77220336">&lt;img src="https://i.gyazo.com/9c2a05fb976f1f671b13c9cf77220336.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Как вы можете видеть, файлы имеют одинаковое значение значения для &lt;code>Action_greeting&lt;/code>, но другое значение для &lt;code>Action_cancel&lt;/code>, поэтому следует обновлять только &lt;code>Action_cancel&lt;/code>.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/16938859b05bb1f0fe08751a08b1fcad">&lt;img src="https://i.gyazo.com/16938859b05bb1f0fe08751a08b1fcad.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;h2 id="отсутствует-файл-ресурсов">Отсутствует файл ресурсов&lt;/h2>
&lt;p>Если вы не предоставите внешний файл ресурсов, это вообще не имеет значения, потому что, поскольку мы ожидаем наличие файла, в случае сбоя он вернет пустой словарь, а при смешивании обоих словарей в конечном итоге получится базовый.&lt;/p>
&lt;h2 id="отсутствует-пара-во-встроенном-ресурсе">Отсутствует пара во встроенном ресурсе&lt;/h2>
&lt;p>Если у вас есть пара во внешнем файле ресурсов, по умолчанию она не будет добавлена в окончательный словарь. Вы можете изменить это, вызвав метод &lt;code>OverwriteDictionary()&lt;/code> при смешивании обоих словарей и установив для параметра &lt;code>addIfDoesntExist&lt;/code> значение &lt;code>true&lt;/code>.&lt;/p>
&lt;h2 id="разные-языки">Разные языки&lt;/h2>
&lt;p>Как вы можете видеть, мы не указали какой-либо язык, потому что все это делается функцией &lt;code>GetText(string key)&lt;/code>, которая вызывает &lt;code>GetText(string key, string language)&lt;/code>, а параметр &lt;code>language&lt;/code> заполняется &lt;code>TwoLetterISOLanguageName&lt;/code> который возвращает текущий язык нашего потока.&lt;/p>
&lt;p>В моем случае испанский язык является языком по умолчанию, поэтому он всегда отображается на испанском, но мы можем попробовать использовать и английский.&lt;/p>
&lt;p>Давайте немного напишем код и создадим что-нибудь для проверки испанского и английского языков.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> Main(&lt;span style="color:#ff7b72">string&lt;/span>[] args)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(&amp;#34;en-US&amp;#34;);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> System.Threading.Thread.CurrentThread.CurrentCulture = &lt;span style="color:#ff7b72">new&lt;/span> CultureInfo(&lt;span style="color:#a5d6ff">&amp;#34;en-US&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WriteText(&lt;span style="color:#a5d6ff">&amp;#34;Action_greeting&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WriteText(&lt;span style="color:#a5d6ff">&amp;#34;Action_cancel&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(&amp;#34;es-ES&amp;#34;);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> System.Threading.Thread.CurrentThread.CurrentCulture = &lt;span style="color:#ff7b72">new&lt;/span> CultureInfo(&lt;span style="color:#a5d6ff">&amp;#34;es-ES&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WriteText(&lt;span style="color:#a5d6ff">&amp;#34;Action_greeting&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WriteText(&lt;span style="color:#a5d6ff">&amp;#34;Action_cancel&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Read();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> WriteText(&lt;span style="color:#ff7b72">string&lt;/span> key) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;[Culture: {System.Threading.Thread.CurrentThread.CurrentCulture}] Key: {key} -&amp;gt; value: {CustomResources.GetText(key)}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Запустив это, результат должен быть примерно таким:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>[Culture: en-US] Key: Action_greeting -&amp;gt; value: Hello
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Culture: en-US] Key: Action_cancel -&amp;gt; value: Finish
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Culture: es-ES] Key: Action_greeting -&amp;gt; value: Hola
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Culture: es-ES] Key: Action_cancel -&amp;gt; value: Terminar
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://gyazo.com/de7e5efcc2ff5e82529749b1188706ff">&lt;img src="https://i.gyazo.com/de7e5efcc2ff5e82529749b1188706ff.png" alt="Изображение от Gyazo">&lt;/a>&lt;/p>
&lt;p>Первые два значения взяты из &lt;code>resources.resx&lt;/code> на английском языке, а последние оба на испанском, и значения извлекаются из &lt;code>resources.es.resx&lt;/code>.&lt;/p>
&lt;h1 id="вот-и-все">Вот и все&lt;/h1>
&lt;p>В этом уроке мы нашли способ объединить встроенные и внешние ресурсы для языков. Это было решением проблемы, которая возникла у нас в команде, и с тех пор все работает без каких-либо проблем.&lt;/p>
&lt;p>Вы можете проверить исходный код &lt;a href="https://github.com/emimontesdeoca/resources-demo">здесь&lt;/a>.&lt;/p>
&lt;p>Если у вас есть какие-либо вопросы, напишите мне в Твиттере по адресу &lt;a href="https://twitter.com/emimontesdeocaa">@emimontesdeocaa&lt;/a>, и я свяжусь с вами, когда у меня будет время.&lt;/p></content:encoded><category>.NET</category></item><item><title>Интеграционный тест с использованием Bot Framework и DirectLine для потоковых случаев.</title><link>https://emimontesdeoca.github.io/ru/posts/integration-test-bot-framework-with-flow-cases/</link><pubDate>Wed, 25 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/integration-test-bot-framework-with-flow-cases/</guid><description>Расширьте интеграционные тесты Bot Framework для поддержки сценариев многоэтапного диалога.</description><content:encoded>&lt;h2 id="введение">Введение&lt;/h2>
&lt;p>В предыдущих сообщениях блога мы провели интеграционный тест для отдельных случаев. «Единичные случаи» — это те случаи, что задав что-то боту, он ответит только один раз, а затем мы сравниваем результаты.&lt;/p>
&lt;p>&lt;strong>В этом руководстве не объясняется, как выполняются аутентификация и вызовы API. Если вы хотите это проверить, ознакомьтесь с руководством для отдельных случаев.&lt;/strong>&lt;/p>
&lt;h3 id="а-как-насчет-плавных-разговоров">А как насчет плавных разговоров?&lt;/h3>
&lt;p>Используя текущий способ, у нас нет какого-либо потока в разговоре, например, если вы хотите попросить о помощи, а затем появляется меню с различными опциями, а затем, после выбора одного из них, другое меню. Это составит примерно 2 или более &lt;code>responses&lt;/code>. &lt;strong>Это означает, что использование решения для интеграционного тестирования, которое мы использовали ранее, не будет работать.&lt;/strong>&lt;/p>
&lt;p>Вот схема того, как работает интеграционный тест для отдельных случаев.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/8915f2653033c1143947ef59196403f4">&lt;img src="https://i.gyazo.com/8915f2653033c1143947ef59196403f4.png" alt="https://gyazo.com/8915f2653033c1143947ef59196403f4">&lt;/a>&lt;/p>
&lt;p>А вот схема того, как мы собираемся адаптировать текущее решение для интеграционного тестирования к потоковым сценариям.&lt;/p>
&lt;p>[&lt;img src="%5B%5B%5B%D0%A2%D0%9E%D0%9A_4%5D%5D%5D" alt="[[[ТОК_3]]])">&lt;/p>
&lt;p>&lt;strong>Следующее объяснение не охватывает всю основную информацию о том, как работает Bot Framework. Если вы не понимаете, пожалуйста, просмотрите официальную документацию.&lt;/strong>&lt;/p>
&lt;h2 id="пример-случая">Пример случая&lt;/h2>
&lt;p>В следующем руководстве я буду использовать бота с созданным мной потоковым диалогом, который просит о помощи, а затем выбирает различные варианты.&lt;/p>
&lt;p>[&lt;img src="%5B%5B%5B%D0%A2%D0%9E%D0%9A_6%5D%5D%5D" alt="[[[ТОК_5]]])">&lt;/p>
&lt;h2 id="новая-структура-json">Новая структура JSON&lt;/h2>
&lt;p>Теперь, когда у нас есть более одного &lt;code>request&lt;/code> при разговоре с ботом, нам нужно изменить нашу структуру json, чтобы добавить все &lt;code>request&lt;/code>, которые мы будем делать.&lt;/p>
&lt;p>[[[ТОК_9]]]&lt;/p>
&lt;p>Как видите, произошло важное изменение: &lt;code>request&lt;/code> теперь называется &lt;code>requests&lt;/code>. Это означает, что теперь у нас есть &lt;code>List&amp;lt;Activity&amp;gt;&lt;/code> вместо одного &lt;code>activity&lt;/code>.&lt;/p>
&lt;h2 id="новые-объекты">Новые объекты&lt;/h2>
&lt;p>Раньше у нас были объекты, настроенные для отдельных случаев: &lt;code>TestEntry&lt;/code> и &lt;code>TestEntryCollection&lt;/code>. Для случаев потока мы будем создавать новые объекты: &lt;code>TestEntryFlow&lt;/code> и &lt;code>TestEntryFlowCollection&lt;/code>.&lt;/p>
&lt;h3 id="testentryflow">&lt;code>TestEntryFlow&lt;/code>&lt;/h3>
&lt;p>Этот объект предназначен для каждой записи, которая есть в коллекции. Обратите внимание, что объект &lt;code>Requests&lt;/code> теперь является &lt;code>List&amp;lt;Activity&amp;gt;&lt;/code> вместо одного &lt;code>Activity&lt;/code>, как я упоминал ранее.&lt;/p>
&lt;p>Поскольку мы будем спрашивать бота несколько раз, нам нужно иметь несколько &lt;code>activities&lt;/code>, которые будут отправлены в беседу.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TestEntryFlow&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Entry name&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;name&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Activity requested by the entry&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;requests&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;Activity&amp;gt; Requests { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Activity response expected by the entry&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;response&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Activity Response { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert value in string&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;assert&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Assert { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="testentirescollection">&lt;code>TestEntiresCollection&lt;/code>&lt;/h3>
&lt;p>Этот объект будет содержать соответствующую информацию для &lt;code>DirectLine&lt;/code>, такую как &lt;code>secret&lt;/code> и конечные точки, а также список &lt;code>Entries&lt;/code>, который мы будем тестировать.&lt;/p>
&lt;p>Обратите внимание, что &lt;code>Entries&lt;/code> теперь представляет собой список &lt;code>TestEntryFlow&lt;/code>, а не &lt;code>TestEntry&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TestEntryFlowCollection&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// DirectLine Secret&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;secret&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Secret { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Endpoint to get the token using the secret for DirectLine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;directlineGenerateTokenEndpoint&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> DirectLineGenerateTokenEndpoint { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Endpoint for a conversation in DirectLine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;directlineConversationEndpoint&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> DirectLineConversationEndpoint { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Entries list&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;entries&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;TestEntryFlow&amp;gt; Entries { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="создание-testmethod-для-случаев-потока">Создание &lt;code>TestMethod&lt;/code> для случаев потока&lt;/h2>
&lt;h3 id="новый-поток">Новый поток&lt;/h3>
&lt;p>Прежде всего, еще раз взгляните на схему (она та же, что я выложил выше).&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83">&lt;img src="https://i.gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83.png" alt="https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83">&lt;/a>&lt;/p>
&lt;p>Как видите, структура потока выполнения теста практически такая же:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Получить информацию&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Аутентификация&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Создать разговор&lt;strong>И вот что изменилось: теперь нам нужно несколько раз отправлять весь запрос в беседу. Для этого нам нужно выполнить цикл для каждого запроса, отправить его боту, а затем сравнить последний ответ с ожидаемым ответом.&lt;/strong>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Отправьте все запросы&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Получить все сообщения&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Получите последний ответ&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Сравните с ожидаемым ответом&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Подтвердить результат&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h3 id="код">Код&lt;/h3>
&lt;p>Прежде всего, нам нужно получить информацию из файла, это то же самое, что мы делали раньше с отдельными случаями.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Load entries from file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> path = System.IO.File.ReadAllText(&lt;span style="color:#a5d6ff">@&amp;#34;C:\dataFlow.json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Deserialize to object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> data = JsonConvert.DeserializeObject&amp;lt;TestEntryFlowCollection&amp;gt;(path);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Теперь нам нужно выполнить цикл для каждого &lt;code>TestEntryFlow&lt;/code> из &lt;code>data.entries&lt;/code>, при этом мы можем следовать тому же потоку, который мы делали в отдельных случаях, до новой части, где мы зацикливаемся на &lt;code>requests&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with current requested values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> token, newToken, conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Activity latestResponse = &lt;span style="color:#ff7b72">new&lt;/span> Activity();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Act for step&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 1 - Get token using secret from DirectLine in BotFramework panel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>token = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(data.Secret, data.DirectLineGenerateTokenEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>).token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 2 - Create a new conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> createdConversation = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(token, data.DirectLineConversationEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// This returns a new token and a conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>newToken = createdConversation.token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>conversationId = createdConversation.conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 3 - Send an activity to the conversation with new token and conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> directlineConversationActivitiesEndpoint = data.DirectLineConversationEndpoint + conversationId + &lt;span style="color:#a5d6ff">&amp;#34;/activities&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Следующий шаг довольно прост: нам нужно зациклить &lt;code>entry.requests&lt;/code> и отправить каждый &lt;code>activity&lt;/code> в диалог.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">foreach&lt;/span> (Activity step &lt;span style="color:#ff7b72">in&lt;/span> entry.Requests)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (step.Type == ActivityTypes.Message)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Step&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(newToken, directlineConversationActivitiesEndpoint, JsonConvert.SerializeObject(step));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 4 - Get all activities, we get a List&amp;lt;activity&amp;gt; and a watermark&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> getLastActivity = Utils.downloadString&amp;lt;ActivityResponse&amp;gt;(newToken, directlineConversationActivitiesEndpoint);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 5 - Get the latest activity which is the response we should be expecting&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> latestResponse = getLastActivity.activities[Int32.Parse(getLastActivity.watermark)];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Мы используем &lt;code>watermark&lt;/code> для получения последнего сообщения. &lt;code>watermark&lt;/code> — это значение, которое API DirectLine возвращает при запросе информации о разговоре.&lt;/p>
&lt;p>После этого нам останется только заполнить &lt;code>globals&lt;/code> нашими &lt;code>latestReponse&lt;/code> и &lt;code>expectedResponse&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with new values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> globals = &lt;span style="color:#ff7b72">new&lt;/span> Objects.Globals { Request = entry.Response, Response = latestResponse };
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>И чтобы закончить дело, мы оцениваем строку &lt;code>assert&lt;/code> в &lt;code>entry&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Assert.IsTrue(&lt;span style="color:#ff7b72">await&lt;/span> CSharpScript.EvaluateAsync&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt;(entry.Assert, globals: globals));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="окончательный-код">Окончательный код&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task ShouldTestFlowCases()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Load entries from file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> path = System.IO.File.ReadAllText(&lt;span style="color:#a5d6ff">@&amp;#34;C:\dataFlow.json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Deserialize to object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> data = JsonConvert.DeserializeObject&amp;lt;TestEntryFlowCollection&amp;gt;(path);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Flow: Arrange -&amp;gt; Act -&amp;gt; arrange -&amp;gt; assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (TestEntryFlow entry &lt;span style="color:#ff7b72">in&lt;/span> data.Entries)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with current requested values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> token, newToken, conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Activity latestResponse = &lt;span style="color:#ff7b72">new&lt;/span> Activity();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Act for step&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 1 - Get token using secret from DirectLine in BotFramework panel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> token = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(data.Secret, data.DirectLineGenerateTokenEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>).token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 2 -Create a new conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> createdConversation = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(token, data.DirectLineConversationEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// This returns a new token and a conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> newToken = createdConversation.token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> conversationId = createdConversation.conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 3 - Send an activity to the conversation with new token and conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> directlineConversationActivitiesEndpoint = data.DirectLineConversationEndpoint + conversationId + &lt;span style="color:#a5d6ff">&amp;#34;/activities&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (Activity step &lt;span style="color:#ff7b72">in&lt;/span> entry.Requests)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (step.Type == ActivityTypes.Message)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Step&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(newToken, directlineConversationActivitiesEndpoint, JsonConvert.SerializeObject(step));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 4 - Get all activities, we get a List&amp;lt;activity&amp;gt; and a watermark&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> getLastActivity = Utils.downloadString&amp;lt;ActivityResponse&amp;gt;(newToken, directlineConversationActivitiesEndpoint);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 5 - Get the latest activity which is the response we should be expecting&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> latestResponse = getLastActivity.activities[Int32.Parse(getLastActivity.watermark)];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with new values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> globals = &lt;span style="color:#ff7b72">new&lt;/span> Objects.Globals { Request = entry.Response, Response = latestResponse };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.IsTrue(&lt;span style="color:#ff7b72">await&lt;/span> CSharpScript.EvaluateAsync&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt;(entry.Assert, globals: globals));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="улучшения">Улучшения&lt;/h2>
&lt;p>Я верю, что лучший поток возможен, но это улучшение будет означать, что структуру JSON также следует изменить. Кроме того, чтобы это произошло, json должен быть более заполнен.&lt;/p>
&lt;p>Чтобы сделать это тестирование лучше, у нас должен быть &lt;code>response&lt;/code> для каждого &lt;code>request&lt;/code>, и мы должны утверждать каждый раз, когда отправляем сообщение. Сейчас мы делаем это, сохраняя все &lt;code>requests&lt;/code> и &lt;strong>финальный &lt;code>response&lt;/code>&lt;/strong> .&lt;/p>
&lt;p>Я сделал схему, чтобы показать, как это будет выглядеть.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc">&lt;img src="https://i.gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc.png" alt="https://gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc">&lt;/a>&lt;/p>
&lt;p>Я твердо убежден, что этот способ в целом намного лучше с точки зрения целостности теста, поскольку вы тестируете практически каждое поведение в потоке, а не просто тестируете окончательный ответ.&lt;/p>
&lt;hr>
&lt;p>&lt;strong>Ну, это все, что касается этого руководства. Помните, что это руководство является продолжением руководства для отдельных случаев. Если вы чувствуете себя потерянным, обратитесь к тому руководству, которое длиннее и содержит больше объяснений для всего.&lt;/strong>&lt;/p>
&lt;p>Помните, что весь код хранится у меня на github в репозитории &lt;a href="https://github.com/emimontesdeoca/integration-test-directline-bot-framework">this&lt;/a>.&lt;/p>
&lt;p>Хорошего дня!&lt;/p></content:encoded><category>.NET</category><category>Bot Framework</category></item><item><title>Интеграционный тест с использованием Bot Framework и DirectLine (1)</title><link>https://emimontesdeoca.github.io/ru/posts/integration-test-bot-framework-1/</link><pubDate>Tue, 24 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/integration-test-bot-framework-1/</guid><description>Настройте интеграционные тесты для чат-ботов Bot Framework, используя API DirectLine и тестовые примеры JSON.</description><content:encoded>&lt;p>Поскольку я начал работать в компании в течение короткого периода времени, мне поручили работать над интеграционным тестом для Bot Framework.&lt;/p>
&lt;p>Моя работа в группе — сделать бота максимально стабильным, но прежде всего, что такое Bot Framework?&lt;/p>
&lt;blockquote>
&lt;p>Создавайте, подключайте, развертывайте интеллектуальных ботов и управляйте ими, чтобы естественным образом взаимодействовать с вашими пользователями на веб-сайте, в приложении, в Cortana, Microsoft Teams, Skype, Slack, Facebook Messenger и т. д. Быстро начните работу с полноценной средой для создания ботов, платя только за то, что вы используете.&lt;/p>&lt;/blockquote>
&lt;p>Вы можете найти дополнительную информацию о Bot Framework прямо &lt;a href="https://dev.botframework.com/">здесь&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Следующее объяснение не охватывает всю основную информацию о том, как работает Bot Framework. Если вы не понимаете, пожалуйста, просмотрите официальную документацию.&lt;/strong>&lt;/p>
&lt;h2 id="зачем-мне-нужен-интеграционный-тест">Зачем мне нужен интеграционный тест?&lt;/h2>
&lt;p>Интеграционный тест необходим, потому что каждый раз, когда один из моих коллег выдвигает исправление, новую функцию или даже новую ошибку, эти тесты будут запускаться перед отправкой кода в производство, и если какой-либо из тестов завершится неудачно, код не пойдет в производство, а это означает, что у конечного пользователя не будет ошибки.&lt;/p>
&lt;h2 id="тест-интеграции-с-ботами">Тест интеграции с ботами?&lt;/h2>
&lt;p>Я верю, что для ботов интеграционный тест очень важен. У вас не может быть бота, у которого некоторые меню не работают или некоторые функции ничего не возвращают.&lt;/p>
&lt;p>Компании используют ботов для своих клиентов, потому что не хотят, чтобы люди были заняты их проблемами. Если бот может помочь пользователю, другие сотрудники смогут использовать свое время для чего-то более важного.&lt;/p>
&lt;h2 id="обзор-решения">Обзор решения.&lt;/h2>
&lt;p>Чтобы это работало, я использовал тестовый проект в Visual Studio, который будет использовать WebClient для API Rest и файл Json, где мы будем хранить наши случаи.&lt;/p>
&lt;h2 id="json-файл">JSON-файл&lt;/h2>
&lt;p>[[[ТОК_1]]]&lt;/p>
&lt;p>Как видите, у нас есть:&lt;/p>
&lt;ul>
&lt;li>Секрет Директа -&amp;gt; секрет от опубликованного бота&lt;/li>
&lt;li>Конечная точка генерации токена Directline -&amp;gt; конечная точка для получения токена с использованием секрета.&lt;/li>
&lt;li>Конечная точка разговора по прямой линии -&amp;gt; конечная точка, чтобы поиграть с разговором.&lt;/li>
&lt;li>Вступление -&amp;gt; тестовый пример
&lt;ul>
&lt;li>Запрос -&amp;gt; что отправляем в беседу&lt;/li>
&lt;li>Ответ -&amp;gt; то, что мы ожидаем получить&lt;/li>
&lt;li>Assert -&amp;gt; что мы сравниваем&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="десериализация">Десериализация&lt;/h2>
&lt;p>У нас есть идеально отформатированный файл json, теперь нам нужно загрузить его в решение, поэтому мы будем использовать JSON.NET и некоторые классы. Сначала у нас есть коллекция записей, в которой есть все, а затем для каждой коллекции есть список записей.&lt;/p>
&lt;p>[[[ТОК_2]]]&lt;/p>
&lt;p>И это объект, который имеет имя, запрос, ответ и утверждение для тестового примера.&lt;/p>
&lt;p>[[[ТОК_3]]]&lt;/p>
&lt;h2 id="разбор-json-в-объект-в-тестовом-примере">Разбор json в объект в тестовом примере&lt;/h2>
&lt;p>Имея классы для объекта синтаксического анализа, это довольно просто, поскольку нам нужно читать как объект.&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;p>Теперь, имея эту коллекцию, мы сможем просмотреть ее и получить информацию, используя, например, foreach.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (TestEntry entry &lt;span style="color:#ff7b72">in&lt;/span> data.Entries)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ....
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>И на этом всё, следующая часть будет включать авторизацию для DirectLine, помните, что весь код хранится у меня на github в репозитории &lt;a href="https://github.com/emimontesdeoca/integration-test-directline-bot-framework">this&lt;/a>.&lt;/p></content:encoded><category>.NET</category><category>Bot Framework</category></item><item><title>Интеграционный тест с использованием Bot Framework и DirectLine (2)</title><link>https://emimontesdeoca.github.io/ru/posts/integration-test-bot-framework-2/</link><pubDate>Tue, 24 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/integration-test-bot-framework-2/</guid><description>Внедрите авторизацию DirectLine и вызовы API для тестирования интеграции Bot Framework (часть 2).</description><content:encoded>&lt;h4 id="в-этой-части-мы-проведем-авторизацию-directline-и-получим-значения-из-ответа-бота">В этой части мы проведем авторизацию DirectLine и получим значения из ответа бота.&lt;/h4>
&lt;p>Теперь, когда мы выполнили десериализацию, пришло время получить информацию из коллекции и всего, что мы будем использовать для получения авторизации, выполнения вызовов API и подтверждения результата.&lt;/p>
&lt;p>&lt;strong>Следующее объяснение не охватывает всю основную информацию о том, как работает Bot Framework. Если вы не понимаете, пожалуйста, просмотрите официальную документацию.&lt;/strong>&lt;/p>
&lt;h2 id="выполнение-вызовов-api-с-помощью-webclient">Выполнение вызовов API с помощью WebClient&lt;/h2>
&lt;p>Чтобы нам было проще вызывать API, я создал класс &lt;code>utils&lt;/code>, в котором мы сохраняем функции, которые будем использовать несколько раз. Этот класс включает в себя &lt;code>uploadString&lt;/code> для POST и &lt;code>downloadString&lt;/code> для GET.&lt;/p>
&lt;p>[[[ТОК_3]]]&lt;/p>
&lt;h2 id="авторизация-directline">Авторизация DirectLine&lt;/h2>
&lt;p>Если вы прочитаете официальную документацию, то сможете узнать, как это сделать, и это довольно легко, а с нашими функциями еще проще. Прежде всего помните, что мы находимся внутри оператора foreach, поскольку мы выполняем аутентификацию для каждого случая на случай, если у нас закончится время, что означает, что тест завершится неудачей.&lt;/p>
&lt;p>[[[ТОК_4]]]&lt;/p>
&lt;p>Теперь у нас есть токен, который будет использоваться для всех последующих вызовов конечной точки диалога.&lt;/p>
&lt;h2 id="создание-разговора">Создание разговора.&lt;/h2>
&lt;p>Чтобы поговорить с ботом, нам сначала нужно создать разговор. Этот разговор вернет новый токен, который включает идентификатор разговора.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 2 -Create a new conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> createdConversation = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(token, data.DirectLineConversationEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// This returns a new token and a conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>newToken = createdConversation.token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>conversationId = createdConversation.conversationId;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Кроме того, мы храним &lt;code>newToken&lt;/code> и &lt;code>conversationId&lt;/code>, оба они понадобятся пользователю для отправки сообщений боту.&lt;/p>
&lt;h2 id="отправить-активность-в-беседу">Отправить активность в беседу&lt;/h2>
&lt;p>Теперь с помощью &lt;code>conversationId&lt;/code> и &lt;code>conversationEndpoint&lt;/code> мы можем создать конечную конечную точку для отправки &lt;code>Activity&lt;/code>, который является &lt;code>request&lt;/code> из файла json.&lt;/p>
&lt;p>[[[ТОК_12]]]&lt;/p>
&lt;h2 id="получить-последнее-сообщение">Получить последнее сообщение&lt;/h2>
&lt;p>В истории сообщений после того, как мы отправили действие, бот уже должен был ответить, поэтому нам нужно получить все сообщения с водяным знаком, а затем, используя этот водяной знак, отфильтровать последнее сообщение/действие.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 4 - Get all activities, we get a List&amp;lt;activity&amp;gt; and a watermark&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> getLastActivity = Utils.downloadString&amp;lt;ActivityResponse&amp;gt;(newToken, directlineConversationActivitiesEndpoint);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 5 - Get the latest activity which is the response we should be expecting&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> latestResponse = getLastActivity.activities[Int32.Parse(getLastActivity.watermark)];
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>И это все, что касается этой части, следующая часть будет включать в себя часть, в которой мы получаем текст из &lt;code>assert&lt;/code> в json, преобразуем его в код, например, используя &lt;code>eval()&lt;/code> в Javascript, но в C#, а затем используя &lt;code>Assert.isTrue()&lt;/code> для получения окончательного результата теста.&lt;/p>
&lt;p>Помните, что весь код хранится у меня на github в репозитории &lt;a href="https://github.com/emimontesdeoca/integration-test-directline-bot-framework">this&lt;/a>.&lt;/p></content:encoded><category>.NET</category><category>Bot Framework</category></item><item><title>Интеграционный тест с использованием Bot Framework и DirectLine (3)</title><link>https://emimontesdeoca.github.io/ru/posts/integration-test-bot-framework-3/</link><pubDate>Tue, 24 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/posts/integration-test-bot-framework-3/</guid><description>Оценивайте ответы ботов с помощью Roslyn CodeAnalysis в интеграционных тестах Bot Framework (часть 3).</description><content:encoded>&lt;h4 id="это-последняя-часть-руководства-в-этой-части-мы-будем-оценивать-текст-из-утверждения-в-json">Это последняя часть руководства, в этой части мы будем оценивать текст из утверждения в json.&lt;/h4>
&lt;p>Теперь, когда мы использовали &lt;code>request&lt;/code> и отправили боту как &lt;code>activity&lt;/code>, мы получили &lt;code>response&lt;/code> и нам нужно сравнить то, что &lt;code>assert&lt;/code> в json (ответ, который мы сохранили в json, это ожидаемый ответ) с ответом, который мы получили от бота.&lt;/p>
&lt;p>&lt;strong>Следующее объяснение не охватывает всю основную информацию о том, как работает Bot Framework. Если вы не понимаете, пожалуйста, просмотрите официальную документацию.&lt;/strong>&lt;/p>
&lt;h2 id="добавление-microsoftcodeanaанализ-в-решение">Добавление Microsoft.CodeAnaанализ в решение&lt;/h2>
&lt;p>Прежде всего, нам нужно включить CodeAnalysis как пакет NuGet.&lt;/p>
&lt;p>[&lt;img src="%5B%5B%5B%D0%A2%D0%9E%D0%9A_5%5D%5D%5D" alt="[[[ТОК_4]]])">&lt;/p>
&lt;p>После установки не забудьте добавить пакет в файл &lt;code>.cs&lt;/code>.&lt;/p>
&lt;p>[[[ТОК_7]]]&lt;/p>
&lt;h2 id="создание-объекта-globals">Создание объекта &lt;code>Globals&lt;/code>&lt;/h2>
&lt;p>Чтобы оценить, мы должны передать параметры оценщику. Этому оценщику нужен файл конфигурации.&lt;/p>
&lt;p>[[[ТОК_9]]]&lt;/p>
&lt;p>Эта часть очень важна: в этот файл конфигурации мы будем включать объекты, которые будем сравнивать, поэтому нам нужно передать ему &lt;strong>ожидаемый ответ&lt;/strong> и &lt;strong>полученный ответ&lt;/strong>.&lt;/p>
&lt;p>Имея эту информацию, он будет использовать &lt;code>assert&lt;/code> в файле json и оценивать то, что он говорит.&lt;/p>
&lt;p>[[[ТОК_11]]]&lt;/p>
&lt;p>&lt;strong>&lt;code>EvaluateAsync&amp;lt;T&amp;gt;&lt;/code> оценивает и возвращает T, в нашем случае мы передаем &lt;code>string&lt;/code> для оценки и &lt;code>globals&lt;/code>, который содержит данные, в которых он будет оценивать.&lt;/strong>&lt;/p>
&lt;p>Я попытаюсь объяснить это на примере, используя запись (которая имеет имя, запрос, ответ и утверждение).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;DecirHola&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;request&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Hola&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;User&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;textFormat&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;plain&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.195Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelData&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;clientActivityId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;1523261059363.6264723268323733.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;ClientCapabilities&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requiresBotState&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsTts&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsListening&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;response&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.901Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;localTimestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T09:04:37+01:00&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;serviceUrl&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;http://localhost:50629&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;emulator&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;j98bbdf097a&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Bot&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;conversation&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;eabcie4be8ak&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;recipient&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;No tengo respuesta para eso.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;attachments&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;replyToId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;47me557ikbf7&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;assert&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Request.Text == Response.Text&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Давайте возьмем самое важное из этой записи: сначала утверждение &lt;strong>&lt;code>&amp;quot;assert&amp;quot;: &amp;quot;Request.Text == Response.Text&amp;quot;&lt;/code>&lt;/strong>, это означает, что оно сравнит &lt;code>Request.Text&lt;/code> с &lt;code>Response.Text&lt;/code> и вернет значение в виде логического значения.&lt;/p>
&lt;p>Но когда мы вызываем функцию &lt;code>await CSharpScript.EvaluateAsync&amp;lt;bool&amp;gt;(entry.Assert, globals: globals)&lt;/code> мы передаем 2 параметра:&lt;/p>
&lt;ul>
&lt;li>&lt;code>string&lt;/code> строка для оценки -&amp;gt; &lt;code>&amp;quot;Request.Text == Response.Text&amp;quot;&lt;/code>&lt;/li>
&lt;li>Данные &lt;code>globals&lt;/code> для оценщика -&amp;gt; в этом случае мы должны предоставить &lt;code>Request&lt;/code> и &lt;code>Response&lt;/code>, запрос — это наш &lt;strong>ожидаемый ответ&lt;/strong>, а ответ — это &lt;strong>полученный ответ&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>Поскольку мы заполняем данные в вычислителе, теперь мы можем использовать строку и оценивать, поэтому она вернет &lt;code>true&lt;/code> или &lt;code>false&lt;/code>.&lt;/p>
&lt;h2 id="готово">Готово&lt;/h2>
&lt;p>Мы закончили, здесь вы можете увидеть готовый &lt;code>TestMethod&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task ShouldTestSingleCases()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Load entries from file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> path = System.IO.File.ReadAllText(&lt;span style="color:#a5d6ff">@&amp;#34;C:\data.json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Deserialize to object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> data = JsonConvert.DeserializeObject&amp;lt;TestEntriesCollection&amp;gt;(path);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Flow: Arrange -&amp;gt; Act -&amp;gt; arrange -&amp;gt; assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (TestEntry entry &lt;span style="color:#ff7b72">in&lt;/span> data.Entries)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with current requested values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> token, newToken, conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (entry.Request.Type == ActivityTypes.Message)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Act&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 1 - Get token using secret from DirectLine in BotFramework panel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> token = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(data.Secret, data.DirectLineGenerateTokenEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>).token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 2 -Create a new conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> createdConversation = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(token, data.DirectLineConversationEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// This returns a new token and a conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> newToken = createdConversation.token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> conversationId = createdConversation.conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 3 - Send an activity to the conversation with new token and conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> directlineConversationActivitiesEndpoint = data.DirectLineConversationEndpoint + conversationId + &lt;span style="color:#a5d6ff">&amp;#34;/activities&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(newToken, directlineConversationActivitiesEndpoint, JsonConvert.SerializeObject(entry.Request));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 4 - Get all activities, we get a List&amp;lt;activity&amp;gt; and a watermark&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> getLastActivity = Utils.downloadString&amp;lt;ActivityResponse&amp;gt;(newToken, directlineConversationActivitiesEndpoint);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 5 - Get the latest activity which is the response we should be expecting&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> latestResponse = getLastActivity.activities[Int32.Parse(getLastActivity.watermark)];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with new values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> globals = &lt;span style="color:#ff7b72">new&lt;/span> Objects.Globals { Request = entry.Response, Response = latestResponse };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.IsTrue(&lt;span style="color:#ff7b72">await&lt;/span> CSharpScript.EvaluateAsync&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt;(entry.Assert, globals: globals));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="диаграмма">Диаграмма&lt;/h2>
&lt;p>Вот диаграмма всего процесса, которому мы следовали, чтобы понять этот момент. Надеюсь, если вы чего-то не поняли, это прояснит ситуацию.&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/1e3b7c9c2286844062878b4b8ca02d2d">&lt;img src="https://i.gyazo.com/1e3b7c9c2286844062878b4b8ca02d2d.png" alt="https://gyazo.com/1e3b7c9c2286844062878b4b8ca02d2d">&lt;/a>&lt;/p>
&lt;p>И это все, это делается для единичных случаев, когда случай 1 к 1, пользователь отправляет &lt;code>activity&lt;/code> и бот возвращает еще один одиночный &lt;code>activity&lt;/code>.&lt;/p>
&lt;p>Надеюсь, вам понравилось. &lt;strong>следующим будут сценарии тестирования с более чем одним ответом.&lt;/strong>&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/d964cfac395ed438a5282e60614863e7">&lt;img src="https://i.gyazo.com/d964cfac395ed438a5282e60614863e7.png" alt="https://gyazo.com/d964cfac395ed438a5282e60614863e7">&lt;/a>&lt;/p>
&lt;p>Помните, что весь код хранится у меня на github в репозитории &lt;a href="https://github.com/emimontesdeoca/integration-test-directline-bot-framework">this&lt;/a>.&lt;/p></content:encoded><category>.NET</category><category>Bot Framework</category><category>NuGet</category></item><item><title>Выступления</title><link>https://emimontesdeoca.github.io/ru/speaking/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/speaking/</guid><description>Конференционные выступления и сессии Эмилиано Монтесдеока — Microsoft MVP и международный докладчик.</description><content:encoded>&lt;p>Я регулярно выступаю на международных технологических конференциях, фокусируясь на &lt;strong>.NET&lt;/strong>, &lt;strong>Azure&lt;/strong>, &lt;strong>ИИ&lt;/strong> и &lt;strong>облачно-нативной разработке&lt;/strong>. Я был отмечен как &lt;strong>Самый активный докладчик Sessionize&lt;/strong> в 2023, 2024 и 2025 годах, а также имею сертификат &lt;strong>Microsoft MVP&lt;/strong> в области технологий для разработчиков.&lt;/p>
&lt;p>Если вы хотите, чтобы я выступил на вашем мероприятии, &lt;a href="mailto:emimontesdeoca@outlook.es">давайте поговорим&lt;/a>!&lt;/p>
&lt;hr>
&lt;h2 id="2026">2026&lt;/h2>
&lt;h3 id="blazor-в-2026-году">Blazor в 2026 году&lt;/h3>
&lt;p>Взгляд на текущее состояние Blazor в 2026 году и то, как он эволюционировал вместе с .NET 10. Рассматриваем последние изменения и улучшения, их влияние на разработку реальных приложений, и какая модель хостинга Blazor лучше всего подходит для каждого сценария.&lt;/p>
&lt;h3 id="усиление-поддержки-уровня-3-с-помощью-ии-azure-functions--openai-в-действии">Усиление поддержки уровня 3 с помощью ИИ: Azure Functions + OpenAI в действии&lt;/h3>
&lt;p>Как Azure Functions, оповещения Azure Monitor и Azure OpenAI работают вместе для ускорения поддержки уровня 3. От автоматической сортировки алертов до интеллектуального анализа логов — практический взгляд на добавление ИИ в реальные рабочие процессы поддержки.&lt;/p>
&lt;h3 id="от-дашборда-к-агенту-следующий-шаг-наблюдаемости">От дашборда к агенту: следующий шаг наблюдаемости&lt;/h3>
&lt;p>Практическая сессия, соединяющая мир ИИ-агентов и наблюдаемости в продакшене. От Semantic Kernel до автономных агентов, запрашивающих данные OpenTelemetry, взаимодействующих с Grafana через MCP и помогающих эксплуатировать и оптимизировать реальные системы.&lt;/p>
&lt;p>&lt;strong>Ожидается&lt;/strong>: &lt;a href="https://globalazure.es">Global Azure 2026&lt;/a> — Апрель 2026, Мадрид, Испания&lt;/p>
&lt;hr>
&lt;h2 id="2025">2025&lt;/h2>
&lt;h3 id="фреймворк-агентов-microsoft-спасает-рождество">Фреймворк агентов Microsoft спасает Рождество&lt;/h3>
&lt;p>Построение и координация нескольких ИИ-агентов с помощью Microsoft Agent Framework для поиска идеальных рождественских подарков. Каждый агент специализируется — от генерации идей подарков до сравнения цен — работая вместе для более умных праздничных покупок.&lt;/p>
&lt;h3 id="усиление-поддержки-уровня-3-с-ии-azure-functions--semantic-kernel-в-действии">Усиление поддержки уровня 3 с ИИ: Azure Functions + Semantic Kernel в действии&lt;/h3>
&lt;p>Azure Functions, Azure Monitor и Semantic Kernel работают вместе, чтобы сделать поддержку уровня 3 более быстрой и эффективной — от автоматического обнаружения алертов до интеллектуального анализа логов.&lt;/p>
&lt;h3 id="что-нового-в-blazor-с-net-10">Что нового в Blazor с .NET 10?&lt;/h3>
&lt;p>Изучение ключевых новых возможностей Blazor для .NET 10, включая улучшения производительности, формы, постоянное состояние и более мощную JavaScript-интероперабельность.&lt;/p>
&lt;h3 id="гибридный-ии-с-c-semantic-kernel-и-gemini-создание-умных-корпоративных-приложений">Гибридный ИИ с C#, Semantic Kernel и Gemini: Создание умных корпоративных приложений&lt;/h3>
&lt;p>Оркестрация гибридных ИИ-рабочих процессов на C# — сочетание локальных моделей (Phi-3) с Gemini Pro/Ultra через Vertex AI, построение когнитивных агентов с плагинами Semantic Kernel и проектирование адаптивных архитектур.&lt;/p>
&lt;h3 id="net-aspire-создание-cloud-native-приложений-без-головной-боли">.NET Aspire: Создание Cloud-Native приложений без головной боли&lt;/h3>
&lt;p>Практическая сессия по созданию облачных микросервисов, изначально масштабируемых, отказоустойчивых и наблюдаемых. Изучение того, как .NET Aspire устраняет 70% облачного бойлерплейта.&lt;/p>
&lt;h3 id="net-aspire-cloud-native-без-хаоса">.NET Aspire: Cloud-Native без хаоса&lt;/h3>
&lt;p>Реальные примеры проектирования приложений, изначально масштабируемых — с наблюдаемостью и авто-устойчивостью с самого первого коммита.&lt;/p>
&lt;hr>
&lt;h2 id="2024">2024&lt;/h2>
&lt;h3 id="ии-встречает-sql-создание-умной-пиццерии-с-net-и-semantic-kernel">ИИ встречает SQL: Создание умной пиццерии с .NET и Semantic Kernel&lt;/h3>
&lt;p>Взаимодействие ИИ на естественном языке, понимающего ваш код и данные — создание интеллектуальной пиццерии на .NET и Semantic Kernel.&lt;/p>
&lt;h3 id="net-aspire-cloud-native-приложения-без-сложностей">.NET Aspire: Cloud-Native приложения без сложностей&lt;/h3>
&lt;p>Создание облачных микросервисов без головной боли — практическая демо-сессия, показывающая, как .NET Aspire упрощает Cloud-native разработку.&lt;/p>
&lt;h3 id="power-platform-путешествие-от-low-code-к-pro-code-технологиям">Power Platform: Путешествие от Low-code к Pro-code технологиям&lt;/h3>
&lt;p>Переход от Low-code Power Apps к продвинутым Pro-code компонентам с использованием TypeScript в рамках той же платформы.&lt;/p>
&lt;h3 id="интеллектуальная-автоматизация-трансформируйте-свои-корпоративные-приложения-с-интегрированным-ии">Интеллектуальная автоматизация: Трансформируйте свои корпоративные приложения с интегрированным ИИ&lt;/h3>
&lt;p>Вывод .NET-приложений на следующий уровень — сочетание традиционной логики с ИИ-рассуждением с помощью Semantic Kernel и .NET Aspire.&lt;/p>
&lt;h3 id="от-low-code-к-pro-code-подходы-к-разработке-ии-в-корпоративных-решениях">От Low-Code к Pro-Code: Подходы к разработке ИИ в корпоративных решениях&lt;/h3>
&lt;p>Исследование различных подходов для корпоративных решений — от Power Platform с ИИ до традиционной разработки с Semantic Kernel.&lt;/p>
&lt;h3 id="управление-командировочными-расходами-с-azure">Управление командировочными расходами с Azure&lt;/h3>
&lt;p>Использование Azure AI Vision, Document Intelligence и OpenAI с Semantic Kernel для автоматизации управления командировочными расходами.&lt;/p>
&lt;hr>
&lt;h2 id="значки-и-признание">Значки и Признание&lt;/h2>
&lt;ul>
&lt;li>🏆 &lt;strong>Sessionize Most Active Speaker 2025&lt;/strong>&lt;/li>
&lt;li>🏆 &lt;strong>Sessionize Most Active Speaker 2024&lt;/strong>&lt;/li>
&lt;li>🏆 &lt;strong>Sessionize Most Active Speaker 2023&lt;/strong>&lt;/li>
&lt;li>🏅 &lt;strong>Microsoft MVP в области технологий для разработчиков&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://sessionize.com/emimontesdeoca/">Посмотреть все сессии на Sessionize →&lt;/a>&lt;/p></content:encoded></item><item><title>Обо мне</title><link>https://emimontesdeoca.github.io/ru/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ru/about/</guid><description>Об Эмилиано Монтесдеока — Microsoft MVP, тимлиде по облачным решениям и пропагандисте сообщества.</description><content:encoded>&lt;h2 id="обо-мне">Обо мне&lt;/h2>
&lt;p>Я &lt;strong>Эмилиано Монтесдеока (Emiliano Montesdeoca)&lt;/strong> — уругвайско-испанский разработчик программного обеспечения, &lt;strong>Microsoft MVP в области технологий для разработчиков&lt;/strong>, и счастливый отец, живущий в &lt;strong>Тенерифе, Канарские острова&lt;/strong>.&lt;/p>
&lt;p>Я люблю решать сложные технические задачи и создавать масштабируемые облачные решения с технологиями Microsoft. Мой ежедневный набор инструментов вращается вокруг &lt;strong>.NET&lt;/strong>, &lt;strong>Azure&lt;/strong>, &lt;strong>ИИ с Semantic Kernel&lt;/strong> и современных архитектур, таких как &lt;strong>.NET Aspire&lt;/strong>.&lt;/p>
&lt;h2 id="чем-я-занимаюсь">Чем я занимаюсь&lt;/h2>
&lt;p>В качестве &lt;strong>тимлида по облачным решениям&lt;/strong> в &lt;a href="https://intelequia.com">Intelequia Technologies&lt;/a> я руковожу проектированием и разработкой облачно-нативных приложений — сочетая стратегическое видение с практическим исполнением. Я процветаю на стыке архитектуры и кода, обеспечивая создание командой решений, которые одновременно элегантны и готовы к производственной среде.&lt;/p>
&lt;h2 id="сообщество">Сообщество&lt;/h2>
&lt;p>Я регулярно выступаю на международных конференциях и был признан &lt;strong>Самым активным докладчиком Sessionize&lt;/strong> в &lt;a href="https://sessionize.com/emimontesdeoca/">2023&lt;/a>, &lt;a href="https://sessionize.com/emimontesdeoca/">2024&lt;/a> и &lt;a href="https://sessionize.com/emimontesdeoca/">2025&lt;/a> годах. Делюсь практическими знаниями из реального мира через выступления на конференциях по всему миру и через свои блоги.&lt;/p>
&lt;p>Я также создатель &lt;a href="https://thedotnetblog.com">&lt;strong>The .NET Blog&lt;/strong>&lt;/a> — ресурса для сообщества .NET — и регулярно пишу в этом блоге о том, что учу и создаю.&lt;/p>
&lt;p>Наставничество и построение сообщества — это неотъемлемая часть того, кто я есть. Отдача экосистеме, которая сформировала мою карьеру, — это то, к чему я отношусь серьёзно.&lt;/p>
&lt;h2 id="давайте-свяжемся">Давайте свяжемся&lt;/h2>
&lt;p>Я всегда открыт для выступлений на мероприятиях, сотрудничества в проектах или разговоров об облачной архитектуре и ИИ. Не стесняйтесь обращаться!&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Email&lt;/strong>: &lt;a href="mailto:emimontesdeoca@outlook.es">emimontesdeoca@outlook.es&lt;/a>&lt;/li>
&lt;li>&lt;strong>GitHub&lt;/strong>: &lt;a href="https://github.com/emimontesdeoca">@emimontesdeoca&lt;/a>&lt;/li>
&lt;li>&lt;strong>X / Twitter&lt;/strong>: &lt;a href="https://twitter.com/emimontesdeocaa">@emimontesdeocaa&lt;/a>&lt;/li>
&lt;li>&lt;strong>LinkedIn&lt;/strong>: &lt;a href="https://www.linkedin.com/in/emimontesdeoca/">emimontesdeoca&lt;/a>&lt;/li>
&lt;li>&lt;strong>Instagram&lt;/strong>: &lt;a href="https://www.instagram.com/emimontesdeoca/">@emimontesdeoca&lt;/a>&lt;/li>
&lt;li>&lt;strong>Sessionize&lt;/strong>: &lt;a href="https://sessionize.com/emimontesdeoca/">emimontesdeoca&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item></channel></rss>