Práce s Local Storage a Session Storage v Blazor aplikacích
Článek se vztahuje k verzi produktu ASP.NET Core 5.0 Preview 8 +
Tento článek byl napsán v roce 2020. Vývojářské technologie se neustále inovují a článek již nemusí popisovat aktuální stav technologie, ideální řešení a můj současný pohled na dané téma.
S příchodem nejnovějších preview verzí UI frameworku Blazor přišlo i několik větších vylepšení. Jedním z nich je podpora prohlížečových úložišť - Session Storage a Local Storage. Podporu zajišťují nové C# třídy, které lze registrovat do DI a následně používat pro přístup do úložišť.
Správa stavu
Správa stavu Blazor aplikací je komplikovanější než u běžných aplikací nebo REST API. V případě REST API dokonce žádný stav neuchováváme, protože REST omezení nám ukládají stav uchovat na klientovi a nikoliv na serveru. U tradiční aplikace (např.: ASP NET MVC) se zpracovává každý request a končí vyrenderováním celého HTML, takže zde je správa stavu celkem jednoduchá. Směrem na klienta posíláme například response společně s cookies. Uvnitř aplikace pak udržujeme stav krátkodobě (instance s lifetime transient), per web request (implicitní výchozí scoped lifetime) nebo dlouhodobě pro všechny uživatele (singleton lifetime, static members).
V případě Blazoru se o stavu musíme bavit s ohledem na to, zda jsme na klientovi (Blazor WASM) nebo na serveru (Blazor Server). Dále budu již popisovat primárně variantu Blazor Server. V případě Blazor Server platí totožná funkcionalita pro transient a singleton lifetime, ovšem zcela jiná situace je v případě scoped lifetime.
Blazor Server a Scoped Lifetime
Implicitní scope v Blazor server aplikaci se váže na tzv. circuit. Ten vzniká iniciací ze strany klienta a jedná se o connection jednoho klienta k serveru. Circuit má výrazně delší životnost než běžný request scope. To by nebyl takový problém, kdyby lifetime bylo možné zaručit po celou dobu práce uživatele se serverem. Bohužel když uživatel ztratí konektivitu nebo v aplikaci nastane například neošetřená výjimka, circuit končí a tím i náš scope. Příkladem měkkého ukončení circuitu může být i pouhý refresh stránky. Jako vývojáři se k tomu můžeme postavit různě, například:
- stavu v rámci circuit se vyhýbáme (nepotřebujeme to)
- stav v rámci circuit ukládáme průběžně do úložiště (SQL Server, Redis, Memcached, Blob Storage)
- stav v rámci circuit ukládáme na klientovi (Session Storage, Local Storage)
Možná by se našlo více řešení kolem circuitu. Představme si ale, že uživatel vyplňuje vícekrokovou anketu nebo kvíz. Stav si chceme uchovat, protože uživatel bude otázkami listovat a občas odbočí na jinou související stránku. Databáze nám nevoní, protože rozdělaná anketa je nehodnotná nebo by způsobila nekonzistenci v datech. Ani vlastně žádnou DB možná nemáme. Čekáme na vyplnění ankety a tu pak pošleme přes REST API na sběrný endpoint.
Pořád ale můžeme stav uchovávat v prohlížeči.
Session Storage a Local Storage
Pakliže chceme změny prováděné na klientovi udržet i v případě ukončení circuit, musíme si pomoci s prohlížečovým úložištěm. Principielně tedy po vyrenderování stránky koukneme do browser storage, zda už tam nemáme data a případně je vyzvedneme. S každou smysluplnou změnou v komponentě pak kontinuálně ukládáme změny do browser storage. Dalo by se říci, že to funguje podobně jako cache a i implementace je mnohdy podobná. Máme na výběr dvě úložiště:
- Session Storage: Data v rámci jedné relace, která se vážou na okno prohlížeče, respektive tab. Když uživatel otevře jiný tab, má jinou session a opět jiná data.
- Local Storage: Data sdílená přes všechny taby a okna prohlížeče. Uživatel může okno prohlížeče zavřít a po návratu má opět data dostupná.
Další varianta jsou cookies. Ty oceníme například v podobě HttpOnly cookies pro uchování různých tokenů. Session Storage a Local Storage jsou méně bezpečná úložiště z pohledu uchování dat. Zejména varianta Local Storage nese riziko poměrně vysoké a není vhodné do tohoto úložiště ukládat jakákoliv citlivá data. Jako vývojáři navíc u LocalStorage více riskujeme vznik bugů v souvislosti se změnou uchovávaných dat.
Existují i další místa, jako například URL nebo operační paměť, ale pro naše účely už další možnosti nebudete řešit.
ProtectedBrowserStorage
A konečně ta magie. Práce s browser storage je z hlediska životního cyklu komponent vhodná v metodě OnAfterRender nebo následně při obsluze událostí, když máme k dispozici DOM. Nemusíme přitom provádět explicitní interops. Vše je zabaleno ve třídách:
- ProtectedLocalStorage
- ProtectedSessionStorage
přičemž pro zajištění podpory stačí registrace do DI:
services.AddProtectedBrowserStorage();
K tomu všemu je potřeba:
- pro .NET 5 Prev 8 - nuget Microsoft.AspNetCore.Components.Web.Extensions
- pro .NET 5 RC 1 - nuget Microsoft.AspNetCore.Components.ProtectedBrowserStorage
- pro .NET 5 RC 2 a snad i GA - nic, protože je ProtectedBrowserStorage součástí frameworku
(ano, Microsoft nezklamal a v každé verzi vrazil tyto třídy někam jinam).
Samotné API je přímočaré. Zmíněné třídy se klasicky injectnou do komponenty pomocí [Inject] atributu a následně můžeme volat nad oběma třídami shodné metody:
- SetAsync, GetAsync, DeleteAsync a další
Jak to funguje pod pokličkou
Obě třídy používají na pozadí abstraktní ProtectedBrowserStorage, která používá běžné interops. Uchovávat můžeme nejen primitivní typy ale i běžné třídy. Na pozadí se objekt serializuje do JSONu s využitím nového System.Text.Json serializeru. JSON pak ještě protéká přes jeden vybraný Data Protector, který provádí nad řetězcem kryptografii s ohledem na účel uchování dat. Pro načtení dat se používá stejný princip unprotect + deserialize.