{"id":678,"date":"2023-01-21T21:39:04","date_gmt":"2023-01-21T20:39:04","guid":{"rendered":"https:\/\/blog.adameczek.pl\/?p=678"},"modified":"2026-01-12T12:03:22","modified_gmt":"2026-01-12T11:03:22","slug":"stronicowanie-w-razor-pages","status":"publish","type":"post","link":"https:\/\/blog.adameczek.pl\/index.php\/2023\/01\/21\/stronicowanie-w-razor-pages\/","title":{"rendered":"Komponent Razor do stronicowania"},"content":{"rendered":"\n<p>Na pocz\u0105tek s\u0142owo wyja\u015bnienia. W tytule nie chodzi o Razor jako Razor Pages \u2013 wzorzec aplikacyjny, kt\u00f3ry Microsoft wprowadzi\u0142 przed ASP.NET MVC. Tu mam na my\u015bli silnik Razor generuj\u0105cy kod html. Ten silnik jest u\u017cywany w ASP.NET MVC do generowania strony z komponent\u00f3w Razor. Skoro ju\u017c to wyja\u015bni\u0142em, to teraz g\u0142adko przechodzimy do meritum \ud83d\ude09<\/p>\n\n\n\n<p>W aplikacjach webowych stronicowanie (ang. <em>paging<\/em>) przydaje si\u0119, kiedy szczodry serwer chce zasypa\u0107 u\u017cytkownika du\u017ca ilo\u015bci\u0105 danych. Du\u017c\u0105, to jest tak\u0105, \u017ce wy\u015bwietlenie ich na raz mog\u0142o by tego\u017c u\u017cytkownika zniech\u0119ci\u0107 do zapoznania si\u0119 z nimi. O ile jest to lista przyj\u0119tych na Wydzia\u0142 Informatyki UW, to pal sze\u015b\u0107. Ale je\u015bli nasze dane to sklep internetowy z ofert\u0105 tysi\u0119cy oprawek do okular\u00f3w (kto wybiera\u0142 oprawki przez Internet, to wie o czym m\u00f3wi\u0119 ;), to aspektowi <em>user experience <\/em>trzeba po\u015bwi\u0119ci\u0107 wi\u0119cej uwagi. Aby strona nie by\u0142a niczym zw\u00f3j papirusu, to ca\u0142\u0105 list\u0119 dzieli si\u0119 na mniejsze kawa\u0142ki i wy\u015bwietla w porcjach w nadziei, \u017ce klient \u0142atwiej to zniesie. Stronicowanie ma te\u017c swoj\u0105 praktyczn\u0105 stron\u0119. Pomaga ogranicza\u0107 obci\u0105\u017cenie \u0142\u0105cza i \u017ar\u00f3d\u0142a danych, bo kwerenda paginacji zwraca tylko tyle rekord\u00f3w, ile mamy zamiar wy\u015bwietli\u0107 na stronie i ani jednego wi\u0119cej.<\/p>\n\n\n\n<p>ASP.NET Core nie oferowa\u0142o paginacji \u201ez pude\u0142ka\u201d. I chyba nadal nie oferuje, bo samouczek Microsoftu dla .NET7 nadal pokazuje <a rel=\"noreferrer noopener\" href=\"https:\/\/learn.microsoft.com\/en-us\/aspnet\/core\/data\/ef-mvc\/sort-filter-page?view=aspnetcore-7.0\" target=\"_blank\">w\u0142asn\u0105 implementacj\u0119<\/a>. Jest ona o tyle pouczaj\u0105ca, co bardzo nieu\u017cyteczna. Implementacja w takiej postaci w aplikacji webowej oznacza ka\u017cdorazowo modyfikacj\u0119 kodu z wielu miejscach. Dlatego proponuj\u0119 wykona\u0107 prac\u0119 raz, ale w taki spos\u00f3b, aby \u0142atwo j\u0105 zastosowa\u0107 do dowolnej kolekcji. Wykonamy w\u0142asny <em>tag helper<\/em>. Do wykorzystanie go w widoku, b\u0119dzie potrzebna jedna linijka kodu.<\/p>\n\n\n\n<p>Na pocz\u0105tek potrzebujemy reprezentacji nawigacji do naszej listy. B\u0119dzie ona zawiera\u0142a w\u0142a\u015bciwo\u015bci potrzebne do generowania klawiszy nawigacji.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism off-numbers lang-csharp\" data-lang=\"C#\" data-show-lang=\"1\"><code>public interface IPagination\n{\n    int CurrentPage { get; }\n    int TotalPages { get; }\n    int PageSize { get; }\n    int TotalCount { get; }\n    bool HasPrevious { get; }\n    bool HasNext { get; }\n}<\/code><\/pre><\/div>\n\n\n\n<p>Nast\u0119pnie zdefiniujemy obiekt listy. Generyczny parametr b\u0119dzie mo\u017cna zast\u0105pi\u0107 typem odpowiednim w konkretnej aplikacji (np. <em>GlassesFrame<\/em>). Tu b\u0119dzie logika odpowiedzialna za ustawienie warto\u015bci w\u0142a\u015bciwo\u015bci paginacji. <em>PagedList <\/em>jak na przyzwoity obiekt przysta\u0142o sam zadba o utworzenie egzemplarza (linia 19, metoda <code>Create()<\/code>). Metoda bierze parametry strony (numer i ilo\u015b\u0107 rekord\u00f3w) oraz \u017ar\u00f3d\u0142o danych jako <code>IQueryable<T><\/code>. Interfejs <code>IQueryable<\/code> powinien by\u0107 tak zaimplementowany przez Data Provider, \u017ceby kwerenda by\u0142a generowana dopiero po zdefiniowaniu zapytania Linq i wywo\u0142aniu polecenia materializacji wynik\u00f3w (linia 21, polecenie <code>Count()<\/code>). Oznacza to, \u017ce Linq-to-SQL przeanalizuje wyra\u017cenie i nie wykona przeliczenia kolekcji \u015bci\u0105gni\u0119tej z serwera, tylko zoptymalizuje zapytanie. To serwer SQL policzy, ile mamy r\u00f3\u017cnych oprawek do okular\u00f3w i zwr\u00f3ci warto\u015b\u0107 skalarn\u0105. <br>Napisa\u0142em \u201epowinno\u201d, bo to nie my mamy kontrol\u0119 nad implementacj\u0105 interfejsu, a dostawca Data Providera. My mo\u017cemy tylko liczy\u0107, \u017ce kontrakt (interfejs) b\u0119dzie wykonany. Spokojnie, je\u015bli to b\u0119dzie <em>Entity Framework<\/em>, to nie mamy czego si\u0119 obawia\u0107 ;). Z wywodu mo\u017cna wysnu\u0107 wniosek (prawid\u0142owy), \u017ce nie ma jednego Linq. Linq zale\u017cy od \u017ar\u00f3d\u0142a danych, na kt\u00f3rym operuje wyra\u017cenie.<br>To jest klasa <em>PagedList<\/em><\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\" data-show-lang=\"1\"><code>using System.Collections.Generic;\nusing System.Linq;\n\npublic class PagedList<T> : List<T>, IPagination where T: class\n{\n    public int CurrentPage { get; }\n    public int TotalPages { get; }\n    public int PageSize { get; }\n    public int TotalCount { get; }\n    public bool HasPrevious => CurrentPage > 1;\n    public bool HasNext => CurrentPage < TotalPages;\n\n    private PagedList(IEnumerable<T> items, int count, int pageNumber, int pageSize)\n    {\n        TotalCount = count;\n        PageSize = pageSize;\n        CurrentPage = pageNumber;\n        TotalPages = (int)Math.Ceiling(count \/ (double)pageSize);\n        AddRange(items);\n    }\n    \n    public static PagedList<T> Create(IQueryable<T> source, int pageNumber, int pageSize)\n    {\n        var count = source.Count();\n        var items = source\n            .Skip((pageNumber - 1) * pageSize)\n            .Take(pageSize)\n            .ToList();\n        return new PagedList<T>(items, count, pageNumber, pageSize);\n    }\n}<\/code><\/pre><\/div>\n\n\n\n<p>By\u0142o ju\u017c o tym, \u017ce obiekt <em>PagedList<\/em>, sam policzy sobie warto\u015bci potrzebne do generowania stronicowania (CurrentPage, HasPrevious itd). Wr\u00f3cimy do tego, sk\u0105d bierze potrzebne do tego informacje? Na razie wiemy, \u017ce przechowuje sw\u00f3j stan. Skoro tak, to mo\u017cemy u\u017cy\u0107 go do przygotowania customowego Tag helpera. Robi si\u0119 to w obiekcie dziedzicz\u0105cym po abstrakcyjnej klasie <em>TagHelper<\/em>. <br>W aplikacjach MVC du\u017c\u0105 rol\u0119 odgrywaj\u0105 konwencje. Dotyczy to tak\u017ce mapowania atrybut\u00f3w tagu z kodzie html na w\u0142a\u015bciwo\u015bci obiektu tag helpera. Nie ma silnego typowania, r\u00f3\u017cne j\u0119zyki, trzeba sobie jako\u015b radzi\u0107\u2026  Wywo\u0142anie tag helpera b\u0119dzie wygl\u0105da\u0142o tak:<\/p>\n\n\n\n<p><code><<strong>paging <\/strong>pagination=\"@Model\" <strong>first-page-text<\/strong>=\"Pierwsza\" <strong>last-page-text<\/strong>=\"Ostatnia\" <strong>controller<\/strong>=\"FramesController\" <strong>action<\/strong>=\"GetFrames\"><\/<strong>paging<\/strong>><\/code><\/p>\n\n\n\n<p>Obiekt tag helpera (z grubsza, bo wyci\u0105\u0142em to co w tym momencie zb\u0119dne) wygl\u0105da tak, jak ni\u017cej. \u0141atwo si\u0119 zorientowa\u0107, \u017ce nazwa tagu to pierwszy cz\u0142on nazwy obiektu <em>PagingTagHelper<\/em>, a atrybuty s\u0105 mapowane na w\u0142a\u015bciwo\u015bci. W ten spos\u00f3b kiedy proces renderuj\u0105cy stron\u0119 natrafi na tag <strong><paging>,<\/strong> to utworzy instancj\u0119 <em>PagingTagHelper<\/em> i zainicjuje w\u0142a\u015bciwo\u015bci warto\u015bciami atrybut\u00f3w tagu.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism off-numbers lang-csharp\" data-lang=\"C#\" data-show-lang=\"1\"><code>public class PagingTagHelper : TagHelper\n{\n    public IPagination Pagination { get; set; }\n    public string FirstPageText { get; set; } = &quot;First&quot;;\n    public string LastPageText { get; set; } = &quot;Last&quot;;\n    public string Controller { get; set; }\n    public string Action { get; set; }\n\n    public override void Process(TagHelperContext context, TagHelperOutput output)\n    {\n        BuildMainTag(output);\n        BuildFirstLastPageButton(FirstPageText, 1, Pagination.CurrentPage == 1, output);\n        BuildFirstLastPageButton(@&quot;\u00ab&quot;,  Pagination.CurrentPage - 1, Pagination.CurrentPage == 1, output);\n        BuildPageButtons(Pagination.CurrentPage, Pagination.TotalPages, output);\n        BuildFirstLastPageButton(@&quot;\u00bb&quot;,  Pagination.CurrentPage + 1, Pagination.CurrentPage == Pagination.TotalPages, output);\n        BuildFirstLastPageButton(LastPageText, Pagination.TotalPages, Pagination.CurrentPage == Pagination.TotalPages, output);\n    }\n}<\/code><\/pre><\/div>\n\n\n\n<p>Potem uruchamiana jest metoda <em>Process()<\/em>, ktora w kolejnych krokach buduje obiekt <em>TagHelperOutput<\/em>. Jeden z tych krok\u00f3w pokazuje listing poni\u017cej. Obiekt <em>TagHelperOutput<\/em> zawiera w\u0142a\u015bciwo\u015b\u0107 <em>TagHelperContent:Content<\/em>, kt\u00f3ra z kolei ma metod\u0119 rozszerzaj\u0105c\u0105 <em>AppendHtml()<\/em>. Budowanie htmla polega na wywo\u0142ywaniu metody <em>AppendHtml() <\/em>przyjmuj\u0105cej enkodowany html. Metod\u0119 <em>AppendHtml()<\/em> wywo\u0142uje si\u0119 wielokrotnie 'doklejaj\u0105c\u2019 kawa\u0142ki htmla. Na zako\u0144czenie metoda <em>Process() <\/em>zwraca wynik do silnika Razor. W ten spos\u00f3b kolejno budowane s\u0105 klawisze nawigacji. <\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>private void BuildPageButtons(int pageNumber, int totalPages,  TagHelperOutput output)\n{\n    if (totalPages < 8)\n    {\n        for (var i = 1; i <= totalPages; i++)\n        {\n            BuildPageButton(pageNumber, i, output);\n        }\n    }\n    else\n    {\n        BuildSpacerButton(Pagination.HasPrevious, output);\n        for (var i = Math.Max(1, pageNumber - 2); i <= Math.Min(totalPages, pageNumber + 2); i++)\n        {\n            BuildPageButton(pageNumber, i, output);\n        }\n        BuildSpacerButton(Pagination.HasNext, output);\n    }\n}<\/code><\/pre><\/div>\n\n\n\n<p>Powy\u017cej jest kod jednej z prywatnych metod naszego tag helpera, pokazuj\u0105ca jak budowany jest kod html. Metoda generuje przyciski z numerami stron 1, 2 do 7 Je\u015bli stron jest wi\u0119cej ni\u017c 7, to wida\u0107 tylko 7 numerowanych klawiszy i symbol \u2026 <br>Efekt wygl\u0105da tak:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img decoding=\"async\" width=\"385\" height=\"46\" src=\"https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2023\/01\/paginacja-nawigacja-1.jpg\" alt=\"\" class=\"wp-image-684\" style=\"width:385px;height:46px\" srcset=\"https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2023\/01\/paginacja-nawigacja-1.jpg 385w, https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2023\/01\/paginacja-nawigacja-1-300x36.jpg 300w\" sizes=\"(max-width: 385px) 100vw, 385px\" \/><figcaption class=\"wp-element-caption\">Dla pierwszej strony klawisze nawigacji po lewej stronie s\u0105 nie aktywne<\/figcaption><\/figure>\n<\/div>\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img decoding=\"async\" width=\"494\" height=\"49\" src=\"https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2023\/01\/paginacja-nawigacja7.jpg\" alt=\"\" class=\"wp-image-685\" srcset=\"https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2023\/01\/paginacja-nawigacja7.jpg 494w, https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2023\/01\/paginacja-nawigacja7-300x30.jpg 300w\" sizes=\"(max-width: 494px) 100vw, 494px\" \/><figcaption class=\"wp-element-caption\">Je\u015bli jest wi\u0119cej ni\u017c 7 stron, to generowany jest symbol \u2026<\/figcaption><\/figure>\n<\/div>\n\n\n<p>Klasa <em>PaginationTagHelper <\/em>wyposa\u017cona jest oczywi\u015bcie w konstruktor i przeznaczona do pracy z kontenerem Dependency Injection. <em>LinkGenerator <\/em>jest dost\u0119pny automatycznie i udost\u0119pniany w przestrzeni nazw <em>Microsoft.AspNetCore.Routing<\/em>. Natomiast domy\u015bln\u0105 implementacj\u0119 <em>IHttpContextAccessor <\/em>nale\u017cy zarejestrowa\u0107 <code>services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();<\/code> <br>Tag helper trzeba zaimportowa\u0107 do widok\u00f3w dyrektyw\u0105 <strong>@addTagHelper<\/strong> (najcz\u0119\u015bciej robi si\u0119 to w pliku '_ViewImports.cshtml\u2019).<br>Ca\u0142y kod jest poni\u017cej:<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>using Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc.Rendering;\nusing Microsoft.AspNetCore.Razor.TagHelpers;\nusing Microsoft.AspNetCore.Routing;\n\npublic class PaginationTagHelper : TagHelper\n{\n    private IPagination Pagination { get; set; }\n    public string FirstPageText { get; set; } = &quot;First&quot;;\n    public string LastPageText { get; set; } = &quot;Last&quot;;\n    public string Controller { get; set; }\n    public string Action { get; set; }\n\n    private readonly LinkGenerator _linkGenerator;\n    private readonly IHttpContextAccessor _contextAccessor;\n    public PaginationTagHelper(LinkGenerator linkGenerator, IHttpContextAccessor contextAccessor)\n    {\n        _linkGenerator = linkGenerator;\n        _contextAccessor = contextAccessor;\n    }\n    public override void Process(TagHelperContext context, TagHelperOutput output)\n    {\n        BuildMainTag(output);\n        BuildFirstLastPageButton(FirstPageText, 1, Pagination.CurrentPage == 1, output);\n        BuildFirstLastPageButton(@&quot;\u00ab&quot;,  Pagination.CurrentPage - 1, Pagination.CurrentPage == 1, output);\n        BuildPageButtons(Pagination.CurrentPage, Pagination.TotalPages, output);\n        BuildFirstLastPageButton(@&quot;\u00bb&quot;,  Pagination.CurrentPage + 1, Pagination.CurrentPage == Pagination.TotalPages, output);\n        BuildFirstLastPageButton(LastPageText, Pagination.TotalPages, Pagination.CurrentPage == Pagination.TotalPages, output);\n    }\n\n    private static void BuildMainTag(TagHelperOutput output)\n    {\n        output.TagName = &quot;ul&quot;;\n        output.Attributes.Add(&quot;class&quot;, &quot;pagination&quot;);\n        output.Attributes.Add(&quot;aria-label&quot;, &quot;Stronicowanie&quot;);\n    }\n\n    private void BuildFirstLastPageButton(string pageText, int pageNumber, bool disabled, TagHelperOutput output)\n    {\n        var li = new TagBuilder(&quot;li&quot;);\n        li.Attributes.Add(&quot;class&quot;, disabled ? &quot;page-item disabled&quot; : &quot;page-item&quot;);\n        li.Attributes.Add(&quot;aria-label&quot;, pageText);\n        li.TagRenderMode = TagRenderMode.StartTag;\n        output.Content.AppendHtml(li);\n\n        var path = _linkGenerator.GetPathByAction(_contextAccessor.HttpContext, Action, Controller, new {pageNumber, pageSize = Pagination.PageSize});\n        var link = $@&quot;<a class=&quot;&quot;page-link&quot;&quot; href=&quot;&quot;{path}&quot;&quot;>{pageText}<\/a>&quot;;\n        output.Content.AppendHtml(link);\n        output.Content.AppendHtml(&quot;<\/li>&quot;);\n    }\n\n    private void BuildPageButtons(int pageNumber, int totalPages,  TagHelperOutput output)\n    {\n        if (totalPages < 8)\n        {\n            for (var i = 1; i <= totalPages; i++)\n            {\n                BuildPageButton(pageNumber, i, output);\n            }\n        }\n        else\n        {\n            BuildSpacerButton(Pagination.HasPrevious, output);\n            for (var i = Math.Max(1, pageNumber - 2); i <= Math.Min(totalPages, pageNumber + 2); i++)\n            {\n                BuildPageButton(pageNumber, i, output);\n            }\n            BuildSpacerButton(Pagination.HasNext, output);\n        }\n    }\n\n    private void BuildPageButton(int pageNumber, int buttonNumber, TagHelperOutput output)\n    {\n        var li = new TagBuilder(&quot;li&quot;);\n        li.Attributes.Add(&quot;class&quot;, buttonNumber == pageNumber ? &quot;page-item active&quot; : &quot;page-item&quot;);\n        li.Attributes.Add(&quot;aria-label&quot;, buttonNumber.ToString());\n        li.TagRenderMode = TagRenderMode.StartTag;\n        output.Content.AppendHtml(li);\n        \n        var path = _linkGenerator.GetPathByAction(_contextAccessor.HttpContext, Action, Controller, new {pageNumber = buttonNumber, pageSize = Pagination.PageSize});\n        var link = $@&quot;<a class=&quot;&quot;page-link&quot;&quot; href=&quot;&quot;{path}&quot;&quot;>{buttonNumber}<\/a>&quot;;\n        output.Content.AppendHtml(link);\n        output.Content.AppendHtml(&quot;<\/li>&quot;);\n    }\n\n    private static void BuildSpacerButton(bool isVisible, TagHelperOutput output)\n    {\n        const string span = @&quot;<span class=&quot;&quot;page-link&quot;&quot;>...<\/span>&quot;;\n\n        if (!isVisible) return;\n        \n        var li = new TagBuilder(&quot;li&quot;);\n        li.Attributes.Add(&quot;class&quot;, &quot;page-item disabled&quot;);\n        li.Attributes.Add(&quot;aria-hidden&quot;, bool.TrueString);\n        li.TagRenderMode = TagRenderMode.StartTag;\n        output.Content.AppendHtml(li);\n        output.Content.AppendHtml(span);\n        output.Content.AppendHtml(&quot;<\/li>&quot;);\n    }\n}<\/code><\/pre><\/div>\n\n\n\n<p>To ju\u017c prawie koniec, ale ca\u0142y czas nie mamy danych, kt\u00f3re b\u0119d\u0105 stronicowane. To niedopatrzenie rozwi\u0105zuje si\u0119 w warstwie aplikacji odpowiedzialnej za persystencj\u0119 danych. Tam budujemy <em>PagedList<\/em> w oparciu o dane z kontrolera (pageSize, pageNumber) i dane z bazy.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>public PagedList<GlassesFrame> GetFrames(int pageSize, int pageNumber)\n{\n    var frames= _productManager.GlassesFrames\n            .Include(x => x.GlassesFramesSizes)\n            .ThenInclude(x => x.Sizes);\n\n    return PagedList<GlassesFrame>.Create(frames, pageNumber, pageSize);\n}<\/code><\/pre><\/div>\n\n\n\n<p>Taki tag helper jest w pe\u0142ni funkcjonalny. Wro\u017cenie jest b\u0142yskawiczne. Potrzebny jest plik z kodem interfejsu i klasy <em>PagedList <\/em>oraz plik z klas\u0105 <em>PaginationTagHelper <\/em>oraz rejestracja jednego serwisu i zaimportowanie tag heplera. Wykorzystanie w kodzie to jedna linijka przytoczona ju\u017c wy\u017cej w tek\u015bcie.<\/p>\n\n\n\n<p>Mi\u0142ego kodzenia \ud83d\ude42<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>Repozytorium z przyk\u0142adow\u0105 aplikacj\u0105 wykorzystuj\u0105c\u0105 PaginationTagHelper: <a href=\"https:\/\/github.com\/madameczek\/TagHelperDemo\">github<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Paginacja po polsku w sosie generycznym, czyli tag helpery od kuchni.<\/p>\n","protected":false},"author":1,"featured_media":718,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"ocean_post_layout":"","ocean_both_sidebars_style":"","ocean_both_sidebars_content_width":0,"ocean_both_sidebars_sidebars_width":0,"ocean_sidebar":"0","ocean_second_sidebar":"0","ocean_disable_margins":"enable","ocean_add_body_class":"","ocean_shortcode_before_top_bar":"","ocean_shortcode_after_top_bar":"","ocean_shortcode_before_header":"","ocean_shortcode_after_header":"","ocean_has_shortcode":"","ocean_shortcode_after_title":"","ocean_shortcode_before_footer_widgets":"","ocean_shortcode_after_footer_widgets":"","ocean_shortcode_before_footer_bottom":"","ocean_shortcode_after_footer_bottom":"","ocean_display_top_bar":"default","ocean_display_header":"default","ocean_header_style":"","ocean_center_header_left_menu":"0","ocean_custom_header_template":"0","ocean_custom_logo":0,"ocean_custom_retina_logo":0,"ocean_custom_logo_max_width":0,"ocean_custom_logo_tablet_max_width":0,"ocean_custom_logo_mobile_max_width":0,"ocean_custom_logo_max_height":0,"ocean_custom_logo_tablet_max_height":0,"ocean_custom_logo_mobile_max_height":0,"ocean_header_custom_menu":"0","ocean_menu_typo_font_family":"0","ocean_menu_typo_font_subset":"","ocean_menu_typo_font_size":0,"ocean_menu_typo_font_size_tablet":0,"ocean_menu_typo_font_size_mobile":0,"ocean_menu_typo_font_size_unit":"px","ocean_menu_typo_font_weight":"","ocean_menu_typo_font_weight_tablet":"","ocean_menu_typo_font_weight_mobile":"","ocean_menu_typo_transform":"","ocean_menu_typo_transform_tablet":"","ocean_menu_typo_transform_mobile":"","ocean_menu_typo_line_height":0,"ocean_menu_typo_line_height_tablet":0,"ocean_menu_typo_line_height_mobile":0,"ocean_menu_typo_line_height_unit":"","ocean_menu_typo_spacing":0,"ocean_menu_typo_spacing_tablet":0,"ocean_menu_typo_spacing_mobile":0,"ocean_menu_typo_spacing_unit":"","ocean_menu_link_color":"","ocean_menu_link_color_hover":"","ocean_menu_link_color_active":"","ocean_menu_link_background":"","ocean_menu_link_hover_background":"","ocean_menu_link_active_background":"","ocean_menu_social_links_bg":"","ocean_menu_social_hover_links_bg":"","ocean_menu_social_links_color":"","ocean_menu_social_hover_links_color":"","ocean_disable_title":"default","ocean_disable_heading":"default","ocean_post_title":"","ocean_post_subheading":"","ocean_post_title_style":"","ocean_post_title_background_color":"","ocean_post_title_background":0,"ocean_post_title_bg_image_position":"","ocean_post_title_bg_image_attachment":"","ocean_post_title_bg_image_repeat":"","ocean_post_title_bg_image_size":"","ocean_post_title_height":0,"ocean_post_title_bg_overlay":0.5,"ocean_post_title_bg_overlay_color":"","ocean_disable_breadcrumbs":"default","ocean_breadcrumbs_color":"","ocean_breadcrumbs_separator_color":"","ocean_breadcrumbs_links_color":"","ocean_breadcrumbs_links_hover_color":"","ocean_display_footer_widgets":"default","ocean_display_footer_bottom":"default","ocean_custom_footer_template":"0","_jetpack_memberships_contains_paid_content":false,"ocean_post_oembed":"","ocean_post_self_hosted_media":"","ocean_post_video_embed":"","ocean_link_format":"","ocean_link_format_target":"self","ocean_quote_format":"","ocean_quote_format_link":"post","ocean_gallery_link_images":"off","ocean_gallery_id":[],"footnotes":""},"categories":[10],"tags":[16,29],"class_list":["post-678","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-net","tag-asp-net","tag-mvc","entry","has-media"],"jetpack_featured_media_url":"https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2023\/03\/NET-thumb.png","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts\/678","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/comments?post=678"}],"version-history":[{"count":10,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts\/678\/revisions"}],"predecessor-version":[{"id":1101,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts\/678\/revisions\/1101"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/media\/718"}],"wp:attachment":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/media?parent=678"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/categories?post=678"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/tags?post=678"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}