<?xml version="1.0" encoding="utf-8"?> 
<rss version="2.0"
  xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
  xmlns:atom="http://www.w3.org/2005/Atom">

<channel>

<title>Богдан Стефанюк: дописи з тегом asp.net core</title>
<link>https://www.stefaniuk.website/tags/asp-net-core/</link>
<description>Всім привіт! Я — Богдан, фулстек розробник з Києва. Пишу в основному про програмування та штуки які вивчаю. Час від часу публікую підбірки фоток, зроблених на плівку, розказую про подорожі та цікаві речі, що оточують мене.</description>
<author></author>
<language>uk</language>
<generator>Aegea 11.2 (v4116)</generator>

<itunes:subtitle>Всім привіт! Я — Богдан, фулстек розробник з Києва. Пишу в основному про програмування та штуки які вивчаю. Час від часу публікую підбірки фоток, зроблених на плівку, розказую про подорожі та цікаві речі, що оточують мене.</itunes:subtitle>
<itunes:image href="" />
<itunes:explicit></itunes:explicit>

<item>
<title>Push Notification via ASP.NET Core</title>
<guid isPermaLink="false">277</guid>
<link>https://www.stefaniuk.website/all/push-notification-via-asp-net-core/</link>
<pubDate>Thu, 08 Jun 2023 20:43:17 -0400</pubDate>
<author></author>
<comments>https://www.stefaniuk.website/all/push-notification-via-asp-net-core/</comments>
<description>
&lt;p&gt;Недавно запустили на роботі новий функціонал інбоксів. Якщо коротко, то це універсальний месенджер з підтримкою різних платформ та каналів що дозволяє вести комунікацію з кінцевими клієнтами в одному місці.&lt;/p&gt;
&lt;p&gt;Для кращого UX треба було добавити пуш нотифікації. На цей момент у нас уже були підключені веб сокети по яким ганяли різні дані, в тому числі і про нові повідомлення. Але такі сповіщення можна відобразити тільки коли відкрита сторінка в браузері.&lt;/p&gt;
&lt;p&gt;Тому вирішили добавити підтримку браузерних пушів. Саме про те як їх підключити та використовувати в звʼязці з ASP.NET Core я хочу розказати.&lt;/p&gt;
&lt;h2&gt;&lt;b&gt;Як це працює?&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;Процес роботи технології пуш нотифікацій у браузері досить простий. Спочатку ми запрошуємо права на отримання сповіщень у користувача. Браузер покаже йому модалку із запитанням «Do you want to receive push notifications?».&lt;/p&gt;
&lt;p&gt;Після того як він підтвердить вибір нам потрібно підписати користувача на отримання нотифікацій використовуючи Push API браузера. Під капотом браузер робить запит на спеціальний push service та повертає нам обʼєкт PushSubscription в якому є вся необхідна інформація для відправки пушів:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;{
  &amp;quot;endpoint&amp;quot;: &amp;quot;https://fcm.googleapis.com/fcm/send/c1KrmpTuRm…&amp;quot;,
  &amp;quot;expirationTime&amp;quot;: null,
  &amp;quot;keys&amp;quot;: {
    &amp;quot;p256dh&amp;quot;: &amp;quot;BGyyVt9FFV…&amp;quot;,
    &amp;quot;auth&amp;quot;: &amp;quot;R9sidzkcdf…&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Далі ми берем цей обʼєкт і відправляємо на наш сервер, де кладемо його у базу даних або зберігаємо іншим способом щоб використовувати його в подальшому.&lt;/p&gt;
&lt;p&gt;Якщо придивитися до структури PushSubscription, можна помітити адрес ендпоінта. Суть в тому, що наш сервер не відправляє сповіщення напряму в браузер. Для цього використовується спеціальний push service, який у кожного розробника браузера свій.&lt;/p&gt;
&lt;div style="max-width: 740px;"&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://www.stefaniuk.website/pictures/push-notifications-structure.png" width="1761" height="646" alt="" /&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Нам повезло, що браузери змогли домовитись і стандартизувати протокол push сервіса, тому нам можна не паритись за формат даних для кожного вендора. Усі вони приймають один формат даних, який містить:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Контент повідомлення&lt;/li&gt;
&lt;li&gt;Кому його відправити&lt;/li&gt;
&lt;li&gt;Як саме його відправити, з яким пріорітетом, в який топік та з яким таймаутом.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Кожне повідомлення шифрується за допомогою ключів, що були створені при генерації підписки. Крім цього, потрібно також підписувати повідомлення за допомогою приватного ключа, щоб підтвердити, що це саме наш сервер відправляє повідомлення. Виглядає це ось так:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Генеруємо пару публічного та приватного ключа, які ще називають VAPID ключами. Цей крок треба зробити всього один раз. В інтернеті купа онлайн сервісів де можна згенерувати ключі.&lt;/li&gt;
&lt;li&gt;Публічний ключ використовуємо при створенні підписки на стороні браузера. Дальше цей ключ асоціюється з ендпоінтом для конкретної підписки.&lt;/li&gt;
&lt;li&gt;Приватний ключ використовуємо для підписки повідомлення на стороні нашого сервера.&lt;/li&gt;
&lt;li&gt;Дальше за допомогою публічного ключа ми можемо перевірити що повідомлення було відправлено саме нашим сервером а не кимось іншим.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Структура VAPID ключа:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;{
    &amp;quot;sub&amp;quot;: &amp;quot;mailto:bohdan@stefaniuk.io&amp;quot;,
    &amp;quot;public_key&amp;quot;: &amp;quot;sdgdklsnvi2f0m-12dgsg...&amp;quot;,
    &amp;quot;private_key&amp;quot;: &amp;quot;1fn0ASFfnaksf...&amp;quot;
 }&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Але, як в тому жарті, є нюанс. Subject в VAPID ключі є опціональним і його не обовʼязково відправляти разом з запитом на push сервіс. Проте Сафарі так не вважає, ще й вимагає специфічний формат. Коли я генерував ключі, то мав ось такий sub &lt;i&gt;mailto: &amp;lt;bohdan@stefaniuk.io&amp;gt;&lt;/i&gt;.&lt;/p&gt;
&lt;p&gt;В хромі все працювало чудово, а сафарі кидав помилку, що запит невалідний. Виявилось, що вони не підтримують пробіли та кутові лапки, замінив sub на &lt;i&gt;mailto:bohdan@stefaniuk.io&lt;/i&gt; і все запрацювало.&lt;/p&gt;
&lt;h2&gt;&lt;b&gt;Реалізація бекенду&lt;/b&gt;&lt;/h2&gt;
&lt;p class="note-md"&gt;Якщо у вас macOS, то ви не зможете локально протестувати сповіщення. Проблема в тому що .NET не підтримує шифрування AesGcm на маках, тому при відправці вилетить ексепшен. Поки я не знайшов як це обійти.&lt;/p&gt;
&lt;p&gt;На стороні сервера будемо використовувати бібліотеку &lt;i&gt;Lib.Net.Http.WebPush&lt;/i&gt; яка під капотом буде шифрувати, підписувати та відправляти наші сповіщення. Для цього вона дає декілька базових модельок та один клас для відправки пушів.&lt;/p&gt;
&lt;p&gt;Почнемо з реалізації створення та видалення підписок та зберігання їх в БД. Для цього добавимо клас ApplicationPushSubscription, який описує структуру таблиці та підключимо його в Entity Framework контекст:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;public class ApplicationPushSubscription
{
    [Key]
    public string P256Dh { get; set; }
    public string Endpoint { get; set; }
    public string Auth { get; set; }
    public Guid UserId { get; set } // Id користувача в нашій системі
}

public class DatabaseContext : DbContext
{
    public DbSet&amp;lt;ApplicationPushSubscription&amp;gt; PushSubscriptions { get; set; }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Тепер створимо клас PushNotificationsService в який добавимо методи для створення та видалення підписок. Метод створення буде приймати екземпляр класу PushSubscription з бібліотеки Lib.Net.Http.WebPush.&lt;/p&gt;
&lt;p&gt;Його можна було б змінити на свій клас, щоб логіка бібліотеки не просочувалась в інші модулі але задля простоти залишимо так як є.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;public class PushNotificationsService
{
    private readonly DatabaseContext _context;
    private readonly PushServiceClient _pushClient;
    
    public PushNotificationsService (DatabaseContext context, PushServiceClient pushClient)
    {
        _context = context;
        _pushClient = pushClient;
    }
    
    public async Task CreateSubscription(Guid userId, PushSubscription pushSubscription)
    {
        var subscription = new ApplicationPushSubscription()
        {
            UserId = userId,
            AccountId = currentUser.AccountId,
            Endpoint = subscription.Endpoint,
            P256Dh = subscription.GetKey(PushEncryptionKeyName.P256DH),
            Auth = subscription.GetKey(PushEncryptionKeyName.Auth)
        };
        
        if (await _context.PushSubscriptions.AnyAsync(x =&amp;gt; x.P256Dh == subscription.P256Dh))
        {
            return;
        }
        
        _context.PushSubscriptions.Add(subscription);
    
        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateException)
        {
            var alreadyExists = await _context.PushSubscriptions.AnyAsync(x =&amp;gt; x.P256Dh == subscription.P256Dh);
            if (!alreadyExists)
            {
                throw;
            }
        }
    }
    
    public async Task DeleteSubscription(Guid userId, string endpoint)
    {
        var subscription = await _context.PushSubscriptions
            .FirstOrDefaultAsync(x =&amp;gt; x.UserId == userId &amp;amp;&amp;amp; x.Endpoint == endpoint);
        
        if (subscription == null)
        {
            return;
        }
        
        _context.PushSubscriptions.Remove(subscription);
        await _context.SaveChangesAsync();
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Реалізовуємо метод для відправки сповіщень нашим користувачам. Він буде приймати Id користувача якому ми відправляємо повідомлення і модель з даними.&lt;/p&gt;
&lt;p&gt;Нам залишається отримати підписку з БД, серіалізувати повідомлення в JSON і скормити це все бібліотеці.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;public async Task SendNotificationAsync(Guid userId, PushNotificationModel model)
{
    try
    {
        var applicationPushSubscriptions = await _context.PushNotifications
            .Where(x =&amp;gt; x.UserId == userId)
            .ToList();
            
        foreach(var applicationPushSubscription in applicationPushSubscriptions)
        {
            var pushSubscription = new PushSubscription();
            pushSubscription.Endpoint = applicationPushSubscription.Endpoint;
            pushSubscription.SetKey(PushEncryptionKeyName.P256DH, applicationPushSubscription.P256Dh);
            pushSubscription.SetKey(PushEncryptionKeyName.Auth, applicationPushSubscription.Auth);
            
            var settings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
            var payload = JsonConvert.SerializeObject(model, settings);
    
            var pushMessage = new PushMessage(payload);
            await _pushClient.RequestPushMessageDeliveryAsync(
                pushSubscription,
                pushMessage,
                _pushClient.DefaultAuthentication,
                VapidAuthenticationScheme.WebPush);
        }
    }
    catch (PushServiceClientException exception)
    {
        if (exception.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Gone)
        {
            _context.PushSubscriptions.Remove(subscription);
            await _context.SaveChangesAsync();
        }
        else
        {
            // Re-Throw exception or log
        }
    }
    catch (Exception exception)
    {
        // Re-Throw exception or log
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Структура класу &lt;i&gt;PushNotificationModel&lt;/i&gt; досить проста та універсальна. Я передаю заголовок сповіщення, текст та додаткові дані:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;public class PushNotificationModel
{
    public string Title { get; set; }
    public string Body { get; set; }
    public object AdditionalData { get; set; }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Підключаємо наш сервіс в ендопінт:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;[Route(&amp;quot;api/push-notifications&amp;quot;)]
[Authorize]
[ApiController]
public class PushNotificationsController : ControllerBase
{
    private readonly PushNotificationService _pushNotificationService;

    public PushNotificationsController(PushNotificationService pushNotificationService)
    {
        _pushNotificationService = pushNotificationService;
    }
    
    [HttpPost(&amp;quot;subscriptions&amp;quot;)]
    public async Task&amp;lt;IActionResult&amp;gt; CreateSubscription([FromBody] PushSubscription subscription)
    {
        var user = HttpContext.GetCurrentUser(); // Our custom method to get current logged-in user
        await _pushNotificationService.CreateSubscription(user.Id, subscription);
        return Ok();
    }

    [HttpDelete(&amp;quot;subscriptions&amp;quot;)]
    public async Task&amp;lt;IActionResult&amp;gt; DeleteSubscription([FromQuery] string endpoint)
    {
        var user = HttpContext.GetCurrentUser();
        await _pushNotificationService.DeleteSubscription(user.Id, endpoint);
        return Ok();
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Все що залишається — підключити наші класи в DI і все готово. Для простоти підключення можемо добавити ще один пакет &lt;i&gt;Lib.AspNetCore.WebPush&lt;/i&gt;.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;services.AddTransient&amp;lt;PushNotificationService&amp;gt;();
services.AddMemoryVapidTokenCache();
services.AddPushServiceClient(options =&amp;gt;
{
    var clientOptions = configuration.GetSection(nameof(PushServiceClient));
    options.Subject = clientOptions.GetValue&amp;lt;string&amp;gt;(nameof(options.Subject));
    options.PublicKey = clientOptions.GetValue&amp;lt;string&amp;gt;(nameof(options.PublicKey));
    options.PrivateKey = clientOptions.GetValue&amp;lt;string&amp;gt;(nameof(options.PrivateKey));
});&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;&lt;b&gt;Реалізація фронтенда&lt;/b&gt;&lt;/h2&gt;
&lt;p class="note-md"&gt;Підтримка технології Web Push була додана в Сафарі тільки в версії MacOS 13 та вище.&lt;/p&gt;
&lt;p&gt;Перше, що потрібно реалізувати на стороні клієнта — це запит прав на отримання сповіщень. Для цього використовуємо обʼєкт &lt;i&gt;Notification&lt;/i&gt;:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;// Запит у браузера прав на підключення пушей
async function askPermission() {
    const permissionPromiseResult = await new Promise(function (resolve, reject) {
        const permissionResult = Notification.requestPermission(function (result) {
            resolve(result);
        });

        if (permissionResult) {
            permissionResult.then(resolve, reject);
        }
    });
    
    if (permissionPromiseResult !== 'granted') {
        alert(&amp;quot;We weren't granted permission.&amp;quot;);
    } else {
        await subscribeUserToPush();
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Після того, як отримали права, нам потрібно зареєструвати service worker в якому буде логіка відображення сповіщень, створити підписку та відправити її на наш сервер.&lt;/p&gt;
&lt;p&gt;При створенні підписки необхідно передати наш публічний ключ. Але він повинен бути в форматі масиву байтів, тому треба спочатку його декодувати і вже тоді передавати.&lt;/p&gt;
&lt;p&gt;В результаті отримуємо обʼєкт &lt;i&gt;pushSubscription&lt;/i&gt;, який ми відправляємо на створений раніше ендпоінт. Крім цього, ще відправляємо &lt;i&gt;Bearer&lt;/i&gt; токен, щоб розуміти до якого користувача привʼязати підписку.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;const publicKey = &amp;quot;vapid-public-key&amp;quot;;

async function subscribeUserToPush() {
    const registration = await navigator.serviceWorker.register('/service-worker.js');
    const subscribeOptions = {
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(publicKey),
    };

    var pushSubscription = await registration.pushManager.subscribe(subscribeOptions);
   
    fetch('http://localhost:5000/api/push-notifications/subscriptions', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer 1245'
        },
        body: JSON.stringify(pushSubscription)
    }).then(function (response) {
        if (response.ok) {
            alert('Successfully subscribed for Push Notifications');
        } else {
            alert('Failed to store the Push Notifications subscription on server');
        }
    }).catch(function (error) {
        console.log('Failed to store the Push Notifications subscription on server: ' + error);
    });
    
    return pushSubscription;
}

function urlBase64ToUint8Array(base64String) {
    var padding = '='.repeat((4 - (base64String.length % 4)) % 4);
    var base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');

    var rawData = window.atob(base64);
    var outputArray = new Uint8Array(rawData.length);

    for (var i = 0; i &amp;lt; rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
};&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Наш сервіс воркер буде максимально простим. Для початку додамо логіку відображення пушей та передачі в них додаткових даних з сервера.&lt;/p&gt;
&lt;p&gt;Також необхідно додати логіку обробки кліку по сповіщенню. Ми будемо перевіряти чи відкритий сайт в браузері. Якщо ні, то відкривати його в новій вкладці. А якщо у нас вже є вкладка з сайтом, то переключаємось на неї і робимо редірект на потрібну сторінку.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;self.addEventListener('push', function (event) {
    var data = event.data.json();
    // {
    //     &amp;quot;title&amp;quot;: &amp;quot;New Notification&amp;quot;,
    //     &amp;quot;body&amp;quot;: &amp;quot;This is the body of the notification&amp;quot;
    //     &amp;quot;additionalData&amp;quot;: {
    //         &amp;quot;pageId&amp;quot;: &amp;quot;12512515&amp;quot;
    //     }
    // }

    event.waitUntil(self.registration.showNotification(data.title, {
        body: data.body,
        icon: '/logo.png',
        data: {
            pageId: data.additionalData?.pageId,
        },
    }));
});

self.addEventListener('notificationclick', function (event) {
    var notification = event.notification;

    event.waitUntil(
        self.clients.matchAll().then(function (allClients) {
            if (allClients.length === 0) {
                self.clients.openWindow(
                    `${self.location.origin}/page/${notification?.data?.pageId}`,
                );
                notification.close();
                return;
            }

            allClients[0]?.navigate(`/page/${notification?.data?.pageId}`);
            allClients[0]?.focus();
            notification.close();
        }),
    );
});&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В результаті всіх цих маніпуляцій у нас є сервер, який вміє відправляти сповіщення та клієнт, який вміє створювати підписки, відображати сповіщення та обробляти кліки.&lt;/p&gt;
&lt;p&gt;Окремо ще можна поговорити про топіки та пріоритети сповіщень, але це виходить за рамки цієї статті. Можливо, розкажу про це пізніше :)&lt;/p&gt;
&lt;h2&gt;&lt;b&gt;Ресурси&lt;/b&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://web.dev/push-notifications-overview/"&gt;Оглад того як працюють пуші&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.tpeczek.com/2017/12/push-notifications-and-aspnet-core-part.html"&gt;Стаття з прикладом реалізації пушів за допомогою ASP.NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tpeczek/Demo.AspNetCore.PushNotifications"&gt;Репозиторій з прикладами використання бібліотеки&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
</item>

<item>
<title>Жизненный цикл запросов в ASP.NET Core</title>
<guid isPermaLink="false">224</guid>
<link>https://www.stefaniuk.website/all/zhiznenny-cikl-zaprosov-v-asp-net-core-mvc/</link>
<pubDate>Fri, 12 Jun 2020 16:38:19 -0400</pubDate>
<author></author>
<comments>https://www.stefaniuk.website/all/zhiznenny-cikl-zaprosov-v-asp-net-core-mvc/</comments>
<description>
&lt;p&gt;Жизненный цикл — это цепочка событий, этапов и компонентов, которые взаимодействуют друг с другом, чтобы обработать запрос пользователя и предоставить ему ответ.&lt;/p&gt;
&lt;p&gt;Общий вид на жизненный цикл ASP.NET Core MVC приложений:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://www.stefaniuk.website/pictures/main-cycle.png" width="850" height="216" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Он содержит в себе несколько этапов:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Middlewares&lt;/li&gt;
&lt;li&gt;Routing&lt;/li&gt;
&lt;li&gt;Controller Initialization&lt;/li&gt;
&lt;li&gt;Controller action execution&lt;/li&gt;
&lt;li&gt;Result Execution&lt;/li&gt;
&lt;li&gt;View Rendering&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Middlewares&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://www.stefaniuk.website/pictures/middlewares.png" width="580" height="249" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Middlewares представляют из себя базовые блоки, с помощью которых строится HTTP пайплайн. Такой пайплайн представляет из себя конвейер, который обрабатывает каждый запрос. Каждый блок получает запрос и смотрит на него, если может предоставить ответ — возвращает его, если нет, передаёт запрос следующему блоку.&lt;/p&gt;
&lt;p&gt;В ASP.NET Core у нас есть 4 метода, с помощью которых мы можем создавать свои middlewares.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use&lt;/li&gt;
&lt;li&gt;Run&lt;/li&gt;
&lt;li&gt;Map&lt;/li&gt;
&lt;li&gt;MapWhen&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1"&gt;Официальная документация&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Routing&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://www.stefaniuk.website/pictures/Routing.png" width="800" height="362" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Маршрутизация позволяет найти для каждого URL подходящий обработчик, а также извлекает все параметры из URL и красиво их нам предоставляет. Для работы роутинга нам нужны две middleware:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UseRouting&lt;/li&gt;
&lt;li&gt;UseEndpoints&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;Как все это работает?&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;UseRouting встраивает класс EndpointRoutingMiddleware, он смотрит на конечные точки, которые есть в приложении и выбирает подходящую. Выбор происходит на основании URL и заголовков. После того как нужный обработчик найден (Request delegate) его присваивают свойству Endpoint(IEndpointFeature) внутри текущего HttpContext. В дальнейшем мы можем получить его значения с помощью метода GetEndpoint.&lt;/p&gt;
&lt;p&gt;UseEndpoints встраивает EndpointMiddleware. Он отвечает за выполнение установленной ранее конечной точки.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-3.1"&gt;Официальная документация&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Controller initialization&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://www.stefaniuk.website/pictures/ControllerInitialization.png" width="611" height="320" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Для обеспечения работы контроллеров у есть два класса: ResourceInvoker, ControllerActionInvoker.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;ResourceInvoker&lt;/b&gt; является абстрактным классом, который содержит общую логику работы с ресурсом, обработку исключений и результата.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;ControllerActionInvoker&lt;/b&gt; наследует ResourceInvoker и добавляет в него логику работы с контроллерами и их методами.&lt;/p&gt;
&lt;p&gt;Каждый из этих классов содержит машину состояний, где каждое состояние отвечает за свой этап жизненного цикла контроллера.&lt;/p&gt;
&lt;p&gt;Исходники классов можно посмотреть на github (&lt;a href="https://github.com/dotnet/aspnetcore/blob/169b7db307935c3646b0d12906ba2c612b912b77/src/Mvc/Mvc.Core/src/Infrastructure/ResourceInvoker.cs"&gt;ResourceInvoker&lt;/a&gt;, &lt;a href="https://github.com/dotnet/aspnetcore/blob/b23ea5b6683b08e8d168ccf49e0fc8515077ee2e/src/Mvc/Mvc.Core/src/Infrastructure/ControllerActionInvoker.cs"&gt;ControllerActionInvoker&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;Action workflow&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://www.stefaniuk.website/pictures/action-life-cycle.png" width="800" height="459" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Работа каждого метода контроллера состоит из своего жизненного цикла. Основой которого являются фильтры. Каждый фильтр может отрабатывать как и перед запуском действия, так и после.&lt;/p&gt;
&lt;p&gt;Общий вид этого жизненного цикла:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Фильтры авторизации&lt;/li&gt;
&lt;li&gt;Фильтры ресурсов&lt;/li&gt;
&lt;li&gt;Привязки моделей&lt;/li&gt;
&lt;li&gt;Фильтры действий&lt;/li&gt;
&lt;li&gt;Выполнение метода контроллера&lt;/li&gt;
&lt;li&gt;Фильтры исключений&lt;/li&gt;
&lt;li&gt;Фильтры результатов&lt;/li&gt;
&lt;li&gt;Выполнение результата&lt;/li&gt;
&lt;li&gt;Фильтры результатов&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Детальнее про фильтры можно почитать &lt;a href="https://metanit.com/sharp/aspnet5/18.1.php"&gt;на метаните&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Result execution&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://www.stefaniuk.website/pictures/action-result.png" width="850" height="391" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Методы контроллера возвращают объекты результата, которые в дальнейшем преобразовываются в соответствующее представление. Объект результата должен наследовать абстрактный класс ActionResult. Фреймворк предоставляет нам большое множество готовых классов.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://www.stefaniuk.website/pictures/action-results.png" width="650" height="287" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Общий вид того как происходит рендеринг представлений:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://www.stefaniuk.website/pictures/Render-cycle.png" width="800" height="344" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Источники&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-3.1"&gt;Официальная документация&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/aspnet-core-3-mvc-request-life-cycle/table-of-contents"&gt;Курс на pluralsight&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
</item>

<item>
<title>Как я делал бота для Facebook workplace</title>
<guid isPermaLink="false">171</guid>
<link>https://www.stefaniuk.website/all/create-workplace-messenger-bot/</link>
<pubDate>Tue, 26 Nov 2019 10:22:26 -0400</pubDate>
<author></author>
<comments>https://www.stefaniuk.website/all/create-workplace-messenger-bot/</comments>
<description>
&lt;p&gt;Дали мне задачу: написать бота для мессенджера workplace, с помощью которого можно получать уведомления из нашей CRM и управлять разными вещами. Расскажу о разных интересных вещах с которыми я столкнулся. Бота писал с помощью ASP.NET Core Web API.&lt;/p&gt;
&lt;h2&gt;Вебхуки&lt;/h2&gt;
&lt;p&gt;Для того чтобы бот мог обрабатывать запросы и сообщения от Facebook нам надо настроить вебхуки. Webhook — механизм оповещения системы о событиях. Для того чтобы Facebook принял наш хук, он должен обрабатывать как GET так и POST запросы.&lt;/p&gt;
&lt;p class="note"&gt;&lt;a href="https://developers.facebook.com/docs/messenger-platform/getting-started/webhook-setup"&gt;Подробнее о Webhook в официальной документации&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;GET запрос служит для валидации работы нашего эндпоинта. POST принимает данные связанные с активностью пользователя, будь то нажатие на кнопки или другая активность.&lt;/p&gt;
&lt;p class="cut-button"&gt;Показать код метода-обработчика GET запроса&lt;/p&gt;
&lt;div class="cut-content"&gt;&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;public IActionResult Receive( [FromQuery(Name = &amp;quot;hub.mode&amp;quot;)] string mode, 
    [FromQuery(Name = &amp;quot;hub.challenge&amp;quot;)] string challenge, 
    [FromQuery(Name = &amp;quot;hub.verify_token&amp;quot;)] string verifyToken)
{ 
   if (string.IsNullOrEmpty(verifyToken)) { 
       return Unauthorized(); 
    }  
    if (verifyToken.Equals(FacebookEnvironment.FacebookVToken)) { 
        return Ok(challenge); 
    } 
    return Unauthorized(); 
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В качестве verify_token используется токен, который мы указали при регистрации нашего хука.&lt;/p&gt;
&lt;/div&gt;&lt;p class="cut-button"&gt;Показать код метода-обработчика POST запроса&lt;/p&gt;
&lt;div class="cut-content"&gt;&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;public async Task&amp;lt;IActionResult&amp;gt; Receive([FromBody]FbResponse response = null)
{
    if (response is null) {
        return BadRequest();
    }

    if (response.Object != &amp;quot;page&amp;quot;) {
        return Ok();
    }

    foreach (var entry in response.Entries) {
        foreach (var message in entry.Messaging) {
            await PrepareMessageAsync(message);
        }
    }
    return Ok(&amp;quot;EVENT_RECEIVED&amp;quot;);
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2&gt;Авторизация запросов Facebook&lt;/h2&gt;
&lt;p&gt;Для авторизации Facebook использует специальный http заголовок (X-Hub-Signature), в нем он передает некую сигнатуру с помощью которой мы можем авторизовать запрос. Для того чтобы добавить такую функциональность в наш контроллер, добавим фильтр.&lt;/p&gt;
&lt;p class="cut-button"&gt;Пример кода, для проверки подписи&lt;/p&gt;
&lt;div class="cut-content"&gt;&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;private const string Sha1Prefix = &amp;quot;sha1=&amp;quot;;

public static bool Validate(string signature, string contentString) {
    if (!signature.StartsWith(Sha1Prefix, StringComparison.OrdinalIgnoreCase)) {
        return false;
    }
    var secret = Encoding.ASCII.GetBytes(FacebookEnvironment.AppSecret);
    var signatureWithoutPrefix = signature.Substring(Sha1Prefix.Length);
    var content = Encoding.ASCII.GetBytes(contentString);
    return GetIsHashValid(secret, signatureWithoutPrefix, content);
}

private static bool GetIsHashValid(byte[] secret, string signature, byte[] content) {
    using var hmac = new HMACSHA1(secret);
    var hash = hmac.ComputeHash(content);
    var hashString = ToHexString(hash);
    return hashString.Equals(signature);
}

private static string ToHexString(IReadOnlyCollection&amp;lt;byte&amp;gt; bytes)
{
    var builder = new StringBuilder(bytes.Count * 2);
    foreach (var b in bytes)
    {
        builder.AppendFormat(&amp;quot;{0:x2}&amp;quot;, b);
    }

    return builder.ToString();
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2&gt;Тестирование бота&lt;/h2&gt;
&lt;p class="note-md"&gt;&lt;a href="http://winitpro.ru/index.php/2017/11/03/ustanovka-besplatnogo-ssl-sertifikata-lets-encrypt-na-iis-v-windows-server-2012-r2/"&gt;Как установить letsencrypt сертификат для IIS&lt;/a&gt;. Если же вы используете связку в виде ubuntu и nginx вам подойдет &lt;a href="https://www.digitalocean.com/community/tutorials/nginx-let-s-encrypt-ubuntu-18-04-ru"&gt;эта инструкция&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Для тестирования нужно развернуть бот на сервере, который смотрит в мир. Также необходимо чтобы у сервера было доменное имя и валидный SSL сертификат. В моем случае, в качестве сервера выступала машина на винде, так как другой внутри нашей сети не было. Как мне показалось захостить приложение написанное на .NET Core намного проще под Ubuntu + nginx нежели под Windows + IIS. В качестве поставщика сертификатов выбрал letsencrypt, так как они предоставляют бесплатный сертификат на 3 месяца, с возможностью дальнейшего обновления.&lt;/p&gt;
</description>
</item>

<item>
<title>CancellationToken в C#</title>
<guid isPermaLink="false">66</guid>
<link>https://www.stefaniuk.website/all/cancellationtoken/</link>
<pubDate>Mon, 27 May 2019 06:26:41 -0400</pubDate>
<author></author>
<comments>https://www.stefaniuk.website/all/cancellationtoken/</comments>
<description>
&lt;p&gt;CancellationToken являются частью механизма отмены асинхронных операций. Например пользователь сидит на сайте и делает запрос на какой-то сложный ресурс. Не дожидаясь ответа он закрывает вкладку браузера, который в свою очередь отменяет запрос. Но наш код не знает что запрос отменен и можно не делать лишнюю работу. А теперь представим что пользователь заходит на сайт и 5 раз перегружает страницу, теперь сервер 5 раз выполняет одну и ту же работу.&lt;/p&gt;
&lt;p&gt;Чтобы решить эту проблему нужно использовать механизм отмены задач. Состоит он из двух компонентов:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CancellationTokenSource — позволяет создавать специальные токены, который содержат информацию про состояние текущей отмены.&lt;/li&gt;
&lt;li&gt;CancellationToken — собственно сам токен, у него есть свойство IsCancellationRequested, которое показывает состояние отмены, а также есть метод, который выбрасывает исключение (OperationCanceledException) в случае отмены операции.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Как это работает?&lt;/h2&gt;
&lt;p&gt;Большенство методов стандартной библиотеки уже имеют перегрузку, которая принимает токен. Чтобы его создать, нужно сначала создать фабрику CancellationTokenSource. Дальше с ее помощью сгенерировать токен и передать его в качестве аргумента в наш асинхронный метод.&lt;/p&gt;
&lt;p&gt;Чтобы отменить задачу нужно вызвать метод Cancel у экземпляра CancellationTokenSource. Он переведет все выпущенные токены в отмененное состояние. Внутри асинхронного метода сработает проверка токена и работа будет завершена.&lt;/p&gt;
&lt;p&gt;Также CancellationTokenSource позволяет установить таймаут по истечению которого автоматически отменяться все токены.&lt;/p&gt;
&lt;p&gt;Простой пример реализации метода, который принимает токен и проверяет его состояние.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;public static void Main() 
{
    CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
    CancellationToken token = cancelTokenSource.Token;
    
    var task = Task.Run(() =&amp;gt; Task(cancelTokenSource.Token), cancelTokenSource.Token);

    cancelTokenSource.Cancel();
}

public static void Task(CancellationToken cancellationToken) 
{
    while (true) 
    {
        // делает полезную работу.

        cancellationToken.ThrowIfCancellationRequested();
    }
    
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Советы&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;В ASP.NET Core приложениях стоит использовать токены отмены для эндпоинтов, которые долго выполняются или сильно нагружают систему. Это позволит отменять такие операции в случае тайм-аутов или когда закрылось подключение.&lt;/li&gt;
&lt;li&gt;Если операция может быть отменена сразу же без дополнительных действий то для проверки отмены стоит использовать ThrowIfCancellationRequested. Если же нам нужно после вызова отмены сделать некую дополнительную работу (очистить ресурсы), то стоит проверять состояние с помощью свойства IsCancellationRequested и если оно равно true — выполнить дополнительную логику очистки.&lt;/li&gt;
&lt;li&gt;Если метод уже выполнил свою работу и была вызвана отмена, то не нужно бросать исключение OperationCanceledException. Нужно просто вернуть результат работы и пускай вызывающий код решать что делать с этим результатом.&lt;/li&gt;
&lt;li&gt;Можно вообще обойтись без использования механизма отмены если код отрабатывает очень быстро.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Полезные ссылки по теме:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/19010856/should-we-use-cancellationtoken-with-mvc-web-api-controllers"&gt;Should we use CancellationToken with MVC/Web API controllers?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://habr.com/ru/post/168669/"&gt;Задачи и отмена в .Net — tips &amp; tricks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devblogs.microsoft.com/premier-developer/recommended-patterns-for-cancellationtoken/"&gt;Recommended patterns for CancellationToken&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
</item>

<item>
<title>Best Practices for Building Async APIs with ASP.NET Core</title>
<guid isPermaLink="false">15</guid>
<link>https://www.stefaniuk.website/all/best-practices-for-building-async-apis-with-asp-net-core/</link>
<pubDate>Fri, 25 Jan 2019 06:41:54 -0400</pubDate>
<author></author>
<comments>https://www.stefaniuk.website/all/best-practices-for-building-async-apis-with-asp-net-core/</comments>
<description>
&lt;p&gt;На днях был вебинар от JetBrains на тему: «Лучшие практики по построению асинхронных API в ASP.NET Core».&lt;br /&gt;
В скором времени опубликую заметку на эту тему.&lt;/p&gt;
&lt;div class="e2-text-video"&gt;
&lt;iframe src="https://www.youtube.com/embed/_T3kvAxAPpQ?enablejsapi=1" allow="autoplay" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
</description>
</item>


</channel>
</rss>