Форум программистов, компьютерный форум, киберфорум
stackOverflow
Войти
Регистрация
Восстановить пароль

Создаем SPA на C# и Blazor

Запись от stackOverflow размещена 04.05.2025 в 19:06
Показов 3651 Комментарии 0

Нажмите на изображение для увеличения
Название: dad7ada3-c120-4b36-8914-aaae14ed1e72.jpg
Просмотров: 33
Размер:	115.8 Кб
ID:	10740
Мир веб-разработки за последние десять лет претерпел коллосальные изменения. Переход от традиционных многостраничных сайтов к одностраничным приложениям (Single Page Applications, SPA) — это настоящий тектонический сдвиг в индустрии. Если раньше каждый клик пользователя вызывал полную перезагрузку страницы со всеми её ресурсами, то теперь мы имеем дело с более "умными" веб-интерфейсами, которые подгружают только нужные данные и обновляют лишь части страницы.

SPA — это приложения, которые загружают единственный HTML-документ и затем динамически обновляют контент по мере взаимодействия пользователя с интерфейсом. Никаких лишних обращений к серверу, никакого мерцания экрана при переходах между страницами. Вместо этого — плавные анимации и моментальные отклики. Для end-юзера это выглядит как нативное приложение, а не традиционый веб-сайт.

Эволюция веб-разработки: от многостраничных сайтов к SPA-архитектуре



В начале были статические HTML-страницы. Помните те времена? Гифки-разделители, кнопки "Назад" и "Вперёд" как основная навигация, jQuery как величайшее технологическое достижение. Каждый переход — белый экран, загрузка всех скриптов, стилей и картинок заново. На это уходили драгоценные секунды, которые в современном мире стоят миллионы долларов и моря пользовательских нервов.

После пришла эпоха частично обновляемых страниц с AJAX, а потом и полноценных JavaScript-фреймворков. React, Angular, Vue.js перевернули представление о том, каким должен быть пользовательский опыт в вебе. Первые SPA не были идеальны: проблемы с SEO, трудности с начальной загрузкой, сложность в разработке. Но они решили главную головоломку — как создать интерактивные приложения, которые не заставляют пользователя ждать при каждом действии.

Получить код страницы с вебсайта (SPA)
После скачивания страницы запускается javascript, который изменяет html. Мне нужен итоговый html.

Windows authentication asp core 2.2 angular spa
Добрый вечер. При создании solution типа asp core angular spa нет возможности выбрать windows ...

Регулярное выражение - Нужно найти в тексте строки типа: spa:Ordinate X="379530"
Здравствуйте. Нужно найти в тексте строки типа: spa:Ordinate X="379530" Пишу регулярное...

SPA приложение работает медленно при нескольких вкладках
Всем привет. Разрабатываю SPA приложение на ASP.NET Core Vue.js. Linux (Ubuntu), Ngnix. Использую...


Сравнительный анализ Blazor и других SPA-фреймворков



JavaScript-фреймворки прочно обосновались в мире веб-разработки, но не все разработчики в восторге от экосистемы JS. Особенно те, кто привык к статически типизированым языкам с надёжной компиляцией и обширными библиотеками. И тут на сцену выходит Blazor. Есле React делает ставку на компонентный подход и однонаправленный поток данных, Angular предлагает полноценный фреймворк с двусторонним связыванием и TypeScript, а Vue.js представляет нечто среднее между ними, то Blazor идёт совсем другим путём. Он позволяет использовать C# и .NET напрямую в браузере! Больше никакого JavaScript, если вы того не хотите.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@page "/counter"
 
<h1>Счётчик</h1>
 
<p>Текущее значение: @currentCount</p>
 
<button class="btn btn-primary" @onclick="IncrementCount">Увеличить</button>
 
@code {
    private int currentCount = 0;
 
    private void IncrementCount()
    {
        currentCount++;
    }
}
Этот простенький пример показывает всю мощь подхода Blazor. Компонент написан полностью на C#, включая обработку событий. Никакого JavaScript, никакого преобразования, никаких хитрых трюков с межязыковыми мостами.

Blazor как ответ на проблемы JavaScript-фреймворков



JavaScript-фреймворки имеют целый вагон проблем: несогласованный синтаксис, разные парадигмы программирования, множество зависимостей, быстрое устаревание, проблемы с типизацией. Разработчики тратят часы на настройку сборшиков и транспайлеров, борются с несовместимыми версиями пакетов, пишут тонны бойлерплейт-кода. Blazor предлагает решение, которое фронтенд-разработчикам может показаться фантастическим — использовать всю мощь .NET-экосистемы прямо в браузере. Это становится возможным благодаря двум подходам:
1. Blazor WebAssembly — запускает .NET-код прямо в браузере через WebAssembly (Wasm).
2. Blazor Server — выполняет логику на сервере и отправляет обновления клиенту через SignalR.
Оба варианта позволяют писать клиенский код на C# вместо JavaScript, что для .NET-разработчиков как глоток свежего воздуха в душном JS-мире.

Преимущества Blazor в экосистеме .NET



Одно из ключевых преимуществ Blazor — тесная интеграция с экосистемой .NET. Разработчики получают:
  • Переиспользование моделей между бэкендом и фронтендом без утомительных преобразований.
  • Доступ к огромной экосистеме NuGet-пакетов.
  • Единый язык программирования на всех уровнях приложения.
  • Мощные инструменты разработки, включая статический анализ, интеллектуальные подсказки и рефакторинг.
  • Общую систему аутентификации и авторизации.
  • Единую стратегию для юнит-тестирования.

Тут надо признать, что Blazor — не панацея. У него есть свои ограничения: размер начального загружаемого бандла для WebAssembly версии довольно большой, а Server-вариант требует постоянного соединения с сервером. Однако Microsoft активно работает над оптимизацей и улучшением платформы. Для бизнеса использование Blazor может означать значительное сокращение времени разработки и упрощение поддержки кода. Команды больше не разделены на "фронтендеров" и "бэкендеров", говорящих на разных языках программирования и использующих разные инструменты.

Архитектурные основы Blazor SPA



Погружаясь в архитектуру Blazor, мы сталкиваемся с двумя фундаментально разными подходами к построению SPA. Это как выбор между электромобилем и гибридом — оба доедут до места назначения, но принцип работы отличается кардинально.

Сравнение Server и WebAssembly моделей



Blazor Server напоминает хитрый аттракцион с иллюзией автономности. Приложение физически работает на сервере, а клиент получает лишь минимальные обновления DOM через SignalR. Сложность кода, рендеринг компонентов, бизнес-логика — всё это остаётся на сервере.

Blazor WebAssembly — почти как нативное приложение в браузере. Весь runtime .NET загружается вместе с сборками вашего приложения, и код выполняется напрямую в браузере через Wasm.

Эта дихотомия порождает интересные возможности. Например, можно создать гибридную модель:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Программа настраивается по-разному в зависимости от среды
public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        
        // Регистрируем разные сервисы в зависимости от режима
        builder.Services.AddScoped<IDataService>(sp => 
        {
            if (builder.HostEnvironment.IsDevelopment())
                return new LocalMockDataService();
            else
                return new RemoteApiDataService();
        });
 
        await builder.Build().RunAsync();
    }
}

Компонентный подход к разработке



Blazor, как и современые фреймворки, использует компонентную модель, но с некоторыми особенностями. Компоненты в Blazor — это классы, которые наследуются от базового класса ComponentBase и инкапсулируют UI-логику и состояние. Эта архитектура обеспечивает переиспользуемость и модульность. Однако есть существенное отличие от React-подхода — Blazor компоненты не работают с виртуальным DOM, что иногда приводит к проблемам с оптимизацей рендеринга.

Stateful компоненты и их жизненный цикл



Жизненный цикл Blazor-компонента — это путешествие с множеством остановок, и знание каждой из них критически важно для создания эффективных приложений.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MyComponent : ComponentBase
{
    [Parameter]
    public int Value { get; set; }
    
    protected override void OnInitialized()
    {
        // Первичная инициализация
    }
    
    protected override async Task OnParametersSetAsync()
    {
        // Обработка изменения параметров
    }
    
    protected override void OnAfterRender(bool firstRender)
    {
        // Доступ к DOM через JSInterop
    }
    
    public void Dispose()
    {
        // Освобождение ресурсов
    }
}
Интересная особенность Blazor — различие в поведении жизненого цикла для Server и WebAssembly вариантов. В Server режиме компоненты живут на сервере и сохраняются между запросами пользователя. В WebAssembly же они существуют в памяти браузера до перезагрузки страницы или ручного удаления.

Межкомпонентное взаимодействие и управление состоянием



В мире Blazor компоненты общаются между собой несколькими способами:
1. Параметры — предсказуемый способ передачи данных от родительского компонента к дочернему.
2. Каскадные параметры — для передачи данных через всю иерархию компонентов.
3. Сервисы — для обмена данными между любыми компонентами.
4. События — когда дочерний компонент должен уведомить родительский.
Вот нетривиальный пример использования сервиса состояния с асинхронными уведомлениями:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StateContainer
{
    private readonly List<Func<Task>> _listeners = new();
    
    public async Task UpdateState()
    {
        foreach (var listener in _listeners.ToList())
        {
            await listener();
        }
    }
    
    public void Subscribe(Func<Task> listener)
    {
        _listeners.Add(listener);
    }
    
    public void Unsubscribe(Func<Task> listener)
    {
        _listeners.Remove(listener);
    }
}
И использование его в компоненте:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@implements IDisposable
@inject StateContainer State
 
@code {
    private Func<Task> _listener;
    
    protected override void OnInitialized()
    {
        _listener = async () => {
            await InvokeAsync(StateHasChanged);
        };
        State.Subscribe(_listener);
    }
    
    public void Dispose()
    {
        State.Unsubscribe(_listener);
    }
}
Такой подход лучше простого события, так как позволяет выполнять асинхронные операции при обновлении состояния.

Инверсия управления и внедрение зависимостей



Blazor плотно интегрирован с системой DI из .NET Core. Это дает прекрасную возможность применять SOLID-принципы и паттерны проектирования из мира бэкенда к фронтенду.

C#
1
2
3
4
5
6
7
8
9
10
11
public void ConfigureServices(IServiceCollection services)
{
    // Синглтон - один экземпляр на всё приложение
    services.AddSingleton<IConfigService, ConfigService>();
    
    // Scoped - один экземпляр на запрос/сессию
    services.AddScoped<ICartService, CartService>();
    
    // Transient - новый экземпляр каждый раз при запросе сервиса
    services.AddTransient<IProductFormatter, ProductFormatter>();
}
Использование DI в компонентах выглядит элегантно:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@inject IProductService ProductService
@inject IJSRuntime JSRuntime
 
<h1>Товары</h1>
 
@if (products == null)
{
    <p>Загрузка...</p>
}
else
{
    foreach (var product in products)
    {
        <ProductCard Product="product" OnAddToCart="AddToCart" />
    }
}
 
@code {
    private List<Product> products;
    
    protected override async Task OnInitializedAsync()
    {
        products = await ProductService.GetProductsAsync();
    }
    
    private async Task AddToCart(Product product)
    {
        await JSRuntime.InvokeVoidAsync("showToast", $"{product.Name} добавлен в корзину");
    }
}
Некоторые разработчики недооценивают мощь внедрения зависимостей в UI-компонентах, считая это избыточным. Но на практике это сильно упрощает тестирование и поддержку, особенно в масштабных приложениях.
Архитектура Blazor-приложений — это неожиданно гибкая система, сочетающая лучшие практики из мира серверной и клиентской разработки. Она не лишена недостатков, но позволяет строить масштабируемые, поддерживаемые и производительные SPA-приложения с использованием единой технологической платформы.

Микрофронтенды на Blazor: модульная архитектура масштабируемых приложений



С ростом размера проекта, неизбежно возникает проблема его масштабирования. Монолитные SPA начинают трещать по швам уже на втором году жизни, когда в них накапливается критическая масса функциональности. Именно тут концепция микрофронтендов приходит на помощь, и Blazor оказывается удивительно подходящей платформой для этого паттерна.

Микрофронтенды — это декомпозиция фронтенда на полунезависимые приложения, каждое из которых отвечает за свою функциональную область. В контексте Blazor есть несколько способов реализовать этот подход:

C#
1
2
3
4
5
6
7
// Регистрация модуля как assembly в главном приложении
public static IEnumerable<Assembly> GetAssemblies()
{
    yield return typeof(Program).Assembly;
    yield return typeof(OrderModule.OrderEntryPoint).Assembly;
    yield return typeof(CatalogModule.CatalogEntryPoint).Assembly;
}
Преимущества такого подхода сразу бросаются в глаза: разные команды могут работать над разными модулями, не мешая друг другу, а сборка и развертывание могут происходить независимо.
Микрофронтенды создают новый класс проблем: согласование стилей, общие компоненты, разделение доменной логики. Для их решения в экосистеме Blazor есть интересные паттерны:
1. Использование Razor Class Libraries (RCL) для выноса повторяющихся компонентов.
2. Shared стили через SCSS/CSS переменные и БЭМ-методологию.
3. Feature Toggle механизмы для контроля за выкаткой новых функций.
Моя любимая практика — это разработка "витрины компонентов" (component showcase), которая служит и документацией, и песочницей для тестирования:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@page "/showcase"
 
<h1>Витрина компонентов</h1>
 
<h2>Кнопки</h2>
<Showcase Title="Основная кнопка">
    <PrimaryButton>Нажми меня</PrimaryButton>
</Showcase>
 
<Showcase Title="Кнопка с иконкой">
    <IconButton Icon="save" Text="Сохранить" />
</Showcase>
 
@code {
    // Простой вспомогательный компонент для демонстрации
    private class Showcase : ComponentBase
    {
        [Parameter] public string Title { get; set; }
        [Parameter] public RenderFragment ChildContent { get; set; }
    }
}

Маршрутизация и навигация в Blazor: продвинутые техники и обработка ошибок



Маршрутизация в Blazor — это отдельная песня со своими переливами и нюансами. Базовая навигация простая, но когда доходит до вложенных роутов, параметризованных путей и обработки ошибок 404, тут начинается настоящее веселье.
Вот пример продвинутой настройки маршрутизации:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<Router AppAssembly="@typeof(App).Assembly" 
       AdditionalAssemblies="@GetAdditionalAssemblies()">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
            <NotAuthorized>
                <RedirectToLogin />
            </NotAuthorized>
            <Authorizing>
                <LoadingIndicator />
            </Authorizing>
        </AuthorizeRouteView>
    </Found>
    <NotFound>
        <CascadingAuthenticationState>
            <LayoutView Layout="@typeof(MainLayout)">
                <CustomNotFound />
            </LayoutView>
        </CascadingAuthenticationState>
    </NotFound>
</Router>
Обратите внимание, как изящно здесь решается проблема авторизации и обработки "страницы не найдена".
Ещё один хитрый трюк — программная навигация с сохранением состояния:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
@inject NavigationManager Navigation
@inject StateService State
 
<button @onclick="NavigateWithState">Перейти и сохранить состояние</button>
 
@code {
    private void NavigateWithState()
    {
        // Сохраняем текущее состояние формы перед переходом
        State.TemporarilyStore("currentForm", formData);
        Navigation.NavigateTo("/next-page");
    }
}
Есть в маршрутизации Blazor и свои грабли. Один из самых коварных — обработка "назад" в браузере для Blazor Server. Тут помогает Blazor's Location changing event:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@implements IDisposable
 
@code {
    protected override void OnInitialized()
    {
        Navigation.LocationChanged += HandleLocationChanged;
    }
 
    private void HandleLocationChanged(object sender, LocationChangedEventArgs e)
    {
        // Здесь можно восстановить состояние из хранилища
        // или выполнить другую логику при изменении URL
    }
 
    public void Dispose()
    {
        Navigation.LocationChanged -= HandleLocationChanged;
    }
}

Варианты хранения и персистентности состояния в долгоживущих SPA-приложениях



Хранение состояния — ахиллесова пята любого SPA-приложения. Blazor не исключение, особенно учитывая различия между Server и WebAssembly режимами. Вот основные стратегии, каждая со своими плюсами и минусами:
1. Browser Storage (localStorage/sessionStorage)
- Плюсы: простота, работает офлайн.
- Мнусы: ограниченный объем, несекретность.
2. Blazor-specific State Containers
- Кастомные сервисы с DI для управления состоянием.
- Могут стать привлекательными для утечек памяти, если не освобождать ресурсы.
3. IndexedDB через JSInterop
- Более структурированное и объемное хранилище.
- Требует обертки на C# для удобного использования.
4. Redux-подобные паттерны
- Централизованное хранение с предсказуемыми изменениями.
- Добавляют бойлерплейт-код.
5. State передача через URL
- Делает состояние шарибельным и букмаркабельным.
- Ограничен размером URL и публичностью.
Интересная реализация Redux-подобного хранилища может выглядеть так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class StoreService<TState> where TState : class, new()
{
    private TState _state = new TState();
    private readonly List<Action<TState>> _subscribers = new();
 
    public TState GetState() => _state;
 
    public void Dispatch<TAction>(TAction action) where TAction : IAction
    {
        var reducer = ResolveReducer(action);
        _state = reducer.Reduce(_state, action);
        NotifySubscribers();
    }
 
    public void Subscribe(Action<TState> subscriber)
    {
        _subscribers.Add(subscriber);
    }
 
    public void Unsubscribe(Action<TState> subscriber)
    {
        _subscribers.Remove(subscriber);
    }
 
    private void NotifySubscribers()
    {
        foreach (var subscriber in _subscribers)
        {
            subscriber(_state);
        }
    }
 
    private IReducer<TState, TAction> ResolveReducer<TAction>(TAction action) where TAction : IAction
    {
        // Тут можно использовать фабрику или DI-контейнер для получения нужного редьюсера
        // Упрощенная версия для примера
        return new DefaultReducer<TState, TAction>();
    }
}
И крошечный пример использования:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@inject StoreService<AppState> Store
@implements IDisposable
 
@code {
    private AppState currentState;
 
    protected override void OnInitialized()
    {
        currentState = Store.GetState();
        Store.Subscribe(StateHasChanged);
    }
 
    private void IncrementCounter()
    {
        Store.Dispatch(new IncrementAction { Amount = 1 });
    }
 
    public void Dispose()
    {
        Store.Unsubscribe(StateHasChanged);
    }
}
Одна из самых интересных стратегий для долгоживущих Blazor Server приложений — это гибридный подход с использованием кэширования состояния на сервере и периодической синхронизацией с клиентом. Это позволяет восстановить состояние даже после перезагрузки страницы или временного разрыва соединения.

Безопасная аутентификация в Blazor



Безопасность — это тот аспект, который никак нельзя оставить "на потом". Особенно когда речь идёт о SPA-приложениях, где аутентификация и авторизация приобретают новое измерение сложности. В традиционных веб-приложениях сервер контролирует каждый запрос и легко может определить, авторизован пользователь или нет. В мире SPA всё становится намного запутаннее, особенно с Blazor WebAssembly, где клиентский код может исполняться полностью в браузере.

Реализация JWT-аутентификации



JWT (JSON Web Tokens) — это стандарт передачи данных в виде JSON-объекта, ставший де-факто для современных SPA. Основное преимущество JWT в том, что он является самодостаточным — сервер может валидировать токен без обращения к базе данных. Для внедрения JWT-аутентификации в Blazor проект нам понадобится настроить серверную часть и клиент:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// В Startup.cs на сервере
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
   .AddJwtBearer(options =>
   {
       options.TokenValidationParameters = new TokenValidationParameters
       {
           ValidateIssuer = true,
           ValidateAudience = true,
           ValidateLifetime = true,
           ValidateIssuerSigningKey = true,
           ValidIssuer = Configuration["Jwt:Issuer"],
           ValidAudience = Configuration["Jwt:Issuer"],
           IssuerSigningKey = new SymmetricSecurityKey(
               Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
       };
   });
А на клиенте создаём AuthenticationStateProvider для работы с JWT:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class JwtAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly HttpClient _httpClient;
    private readonly ILocalStorageService _localStorage;
    
    public JwtAuthenticationStateProvider(
        HttpClient httpClient,
        ILocalStorageService localStorage)
    {
        _httpClient = httpClient;
        _localStorage = localStorage;
    }
    
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var savedToken = await _localStorage.GetItemAsync<string>("authToken");
        
        if (string.IsNullOrWhiteSpace(savedToken))
        {
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
        }
        
        _httpClient.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("bearer", savedToken);
            
        return new AuthenticationState(new ClaimsPrincipal(
            new ClaimsIdentity(ParseClaimsFromJwt(savedToken), "jwt")));
    }
    
    // Метод для разбора JWT и извлечения клэймов
    private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
    {
        var payload = jwt.Split('.')[1];
        var jsonBytes = ParseBase64WithoutPadding(payload);
        var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
        
        return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()));
    }
    
    // Вспомогательный метод для декодирования Base64Url
    private byte[] ParseBase64WithoutPadding(string base64)
    {
        switch (base64.Length % 4)
        {
            case 2: base64 += "=="; break;
            case 3: base64 += "="; break;
        }
        return Convert.FromBase64String(base64);
    }
    
    // Метод для установки аутентификационного состояния
    public void NotifyUserAuthentication(string token)
    {
        var authenticatedUser = new ClaimsPrincipal(
            new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt"));
            
        var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
        NotifyAuthenticationStateChanged(authState);
    }
    
    // Метод для сброса аутентификационного состояния
    public void NotifyUserLogout()
    {
        var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
        var authState = Task.FromResult(new AuthenticationState(anonymousUser));
        NotifyAuthenticationStateChanged(authState);
    }
}
Самая частая ошибка при работе с JWT — это игнорирование срока жизни токена. Во-первых, не стоит делать токены вечными, а во-вторых, хорошая практика — использовать refresh токены для продления сессии без необходимости повторного ввода учётных данных:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class AuthService
{
    private readonly HttpClient _httpClient;
    private readonly ILocalStorageService _localStorage;
    private readonly JwtAuthenticationStateProvider _authStateProvider;
    
    public AuthService(
        HttpClient httpClient,
        ILocalStorageService localStorage,
        AuthenticationStateProvider authStateProvider)
    {
        _httpClient = httpClient;
        _localStorage = localStorage;
        _authStateProvider = authStateProvider as JwtAuthenticationStateProvider;
    }
    
    public async Task<bool> TryRefreshToken()
    {
        var refreshToken = await _localStorage.GetItemAsync<string>("refreshToken");
        
        if (string.IsNullOrEmpty(refreshToken))
            return false;
            
        var response = await _httpClient.PostAsync("api/auth/refresh", 
            new StringContent(JsonSerializer.Serialize(new { 
                Token = refreshToken 
            }), Encoding.UTF8, "application/json"));
            
        if (!response.IsSuccessStatusCode)
            return false;
            
        var result = await response.Content.ReadFromJsonAsync<LoginResult>();
        
        await _localStorage.SetItemAsync("authToken", result.Token);
        await _localStorage.SetItemAsync("refreshToken", result.RefreshToken);
        
        _authStateProvider.NotifyUserAuthentication(result.Token);
        
        return true;
    }
}

Реализация OAuth 2.0 с использованием Identity Server



Для более сложных сценариев, особенно когда требуется поддержка внешних провайдеров аутентификации (Google, Facebook, Microsoft), Identity Server становится незаменимым инструментом. Пакет Microsoft.AspNetCore.Authentication.OpenIdConnect упрощает интеграцию Blazor с Identity Server. Но наивная настройка может привести к проблемам с CORS и CSRF, особенно в Blazor WebAssembly. Правильный подход — настроить опции OpenID Connect:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
services.AddAuthentication(options =>
{
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
    options.Authority = "https://localhost:5001";
    options.ClientId = "blazor";
    options.ResponseType = "code";
    options.SaveTokens = true;
    options.GetClaimsFromUserInfoEndpoint = true;
    
    // Важно для CSRF-защиты в Blazor WebAssembly
    options.CorrelationCookie.SameSite = SameSiteMode.None;
    
    options.Scope.Add("api");
    options.Scope.Add("offline_access");
});
Для подключения внешних провайдеров, таких как Google или GitHub, есть специальные расширения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
services.AddAuthentication()
    .AddGoogle("Google", options =>
    {
        options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
        options.ClientId = "your-google-client-id";
        options.ClientSecret = "your-google-client-secret";
    })
    .AddGitHub("GitHub", options =>
    {
        options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
        options.ClientId = "your-github-client-id";
        options.ClientSecret = "your-github-client-secret";
    });
Инициализация процесса аутентификации через внешний провайдер в Blazor выглядит так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@inject NavigationManager NavigationManager
 
<button @onclick="LoginWithGoogle">Войти через Google</button>
 
@code {
    private void LoginWithGoogle()
    {
        var callbackUrl = NavigationManager.BaseUri + "authentication/login-callback";
        var encodedUrl = Uri.EscapeDataString(callbackUrl);
        NavigationManager.NavigateTo(
            $"https://localhost:5001/connect/authorize?provider=Google&return_url={encodedUrl}",
            forceLoad: true);
    }
}

CSRF-защита в Blazor приложениях



Cross-Site Request Forgery (CSRF) — атака, заставляющая пользователя выполнить нежелательные действия на веб-сайте, где он уже аутентифицирован. В Blazor Server аутоматически применяется защита от CSRF благодаря встроенным механизмам .NET Core. Для Blazor WebAssembly ситуация иная, так как приложение выполняется полностью в браузере и использует обычно токены для аутентификации. Но даже здесь важно применять CSRF-токены при отправке форм:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@inject AntiforgeryService AntiforgeryService
 
<EditForm Model="@model" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    
    <input type="hidden" name="__csrfToken" value="@csrfToken" />
    
    <p>
        <label>
            Username:
            <InputText @bind-Value="model.Username" />
        </label>
    </p>
    <p>
        <label>
            Password:
            <InputText type="password" @bind-Value="model.Password" />
        </label>
    </p>
    
    <button type="submit">Submit</button>
</EditForm>
 
@code {
    private LoginModel model = new();
    private string csrfToken;
    
    protected override async Task OnInitializedAsync()
    {
        csrfToken = await AntiforgeryService.GetTokenAsync();
    }
    
    private async Task HandleValidSubmit()
    {
        // При отправке запроса добавляем CSRF-токен в заголовок
        using var message = new HttpRequestMessage(HttpMethod.Post, "api/login");
        message.Headers.Add("X-CSRF-TOKEN", csrfToken);
        message.Content = new StringContent(
            JsonSerializer.Serialize(model),
            Encoding.UTF8,
            "application/json");
            
        var response = await HttpClient.SendAsync(message);
        // Обработка ответа...
    }
}
Служба AntiforgeryService может быть реализована так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AntiforgeryService
{
    private readonly HttpClient _httpClient;
    
    public AntiforgeryService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    
    public async Task<string> GetTokenAsync()
    {
        var response = await _httpClient.GetAsync("api/antiforgery/token");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
}

Управление доступом на основе ролей и политик



Правильная аутентификация — это только полдела. Настоящий вызов начинается, когда требуется гибкое и детальное управление доступом. В Blazor для этого есть два мощных инструмента: роли и политики.
Роли — это классический подход "дал имя — получил набор прав". Чтобы ограничить доступ к компоненту по ролям, достаточно атрибута:

C#
1
2
3
4
5
@page "/admin-panel"
@attribute [Authorize(Roles = "Admin")]
 
<h1>Админ-панель</h1>
<p>Только админы могут видеть эту страницу</p>
Но что делать, если логика доступа сложнее, чем просто проверка имени роли? Тут на сцену выходят политики (policies) — более гибкий механизм. Например, можно определить политику "требуется двухфакторная аутентификация для важных операций":

C#
1
2
3
4
5
6
7
8
9
10
11
// В Startup.cs на сервере
services.AddAuthorization(options =>
{
 options.AddPolicy("RequireTwoFactor", policy =>
     policy.RequireClaim("amr", "mfa"));
     
 options.AddPolicy("SeniorDevelopers", policy =>
     policy.RequireAssertion(context => 
         context.User.HasClaim(c => c.Type == "EmploymentYears") &&
         int.Parse(context.User.FindFirst("EmploymentYears").Value) > 5));
});
И затем использовать эту политику в коде:

C#
1
2
3
4
5
@page "/critical-operation"
@attribute [Authorize(Policy = "RequireTwoFactor")]
 
<h1>Критическая операция</h1>
<p>Если вы это видите, значит у вас включена 2FA</p>
Этот механизм особенно полезен для реализации сложной бизнес-логики авторизации, например, "пользователь может редактировать документ только если он владелец или имеет роль редактора".
Ловушка, которую многие забывают — это доступ к роутам через URL. Часто разработчики проверяют только видимость элементов в UI, но забывают, что прямой доступ к URL страницы все ещё возможен. Blazor помогает решить эту проблему, перенаправляя пользователя при отсутствии прав:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Router AppAssembly="@typeof(Program).Assembly">
 <Found Context="routeData">
     <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
         <NotAuthorized>
             <h1>Доступ запрещен</h1>
             <p>У вас недостаточно прав для просмотра этой страницы.</p>
             
             <a href="login">Войдите</a> или <a href="/">вернитесь на главную</a>.
         </NotAuthorized>
     </AuthorizeRouteView>
 </Found>
 <NotFound>
     <LayoutView Layout="@typeof(MainLayout)">
         <h1>Страница не найдена</h1>
     </LayoutView>
 </NotFound>
</Router>

Защита от XSS и других типов веб-уязвимостей в Blazor



Хорошая новость — Blazor изначально спроектирован с учётом защиты от XSS (Cross-Site Scripting). В отличие от многих других фреймворков, Blazor автоматически экранирует все данные перед их отображением.

C#
1
2
3
4
5
6
7
8
9
10
@page "/user-comment"
 
<h3>Комментарий пользователя</h3>
 
@* Этот HTML не будет выполнен, а будет отрендерен как текст *@
<p>@userComment</p>
 
@code {
 private string userComment = "<script>alert('XSS!');</script>";
}
Но иногда требуется действительно вставить HTML — например, для текстового редактора или вывода форматированного контента. Тут можно использовать MarkupString:

C#
1
2
3
4
5
6
7
8
9
10
@page "/rich-content"
 
<h3>Форматированный контент</h3>
 
@((MarkupString)htmlContent)
 
@code {
 // ВНИМАНИЕ: никогда не делайте так с данными от пользователя!
 private string htmlContent = "<p>Это <strong>форматированный</strong> текст.</p>";
}
Самая опасная частая ошибка — использование MarkupString для отображения непроверенных данных от пользователя. Это открытые ворота для XSS. Всегда сначала обрабатывайте вводимый контент через хорошую библиотеку санитизации HTML.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@inject HtmlSanitizer Sanitizer
 
@page "/sanitized-content"
 
<h3>Безопасно отображенный пользовательский контент</h3>
 
@((MarkupString)sanitizedHtml)
 
@code {
 private string userInput = "<script>alert('XSS!');</script><p>Легитимный текст</p>";
 private string sanitizedHtml;
 
 protected override void OnInitialized()
 {
     // Очищаем HTML от потенциально опасных элементов
     sanitizedHtml = Sanitizer.Sanitize(userInput);
 }
}

Защита API-эндпоинтов



Безопасность API — это не только аутентификация, но и защита от массы других угроз: от инъекций до DoS-атак. Базовая защита API-эндпоинтов в Blazor приложении включает несколько уровней:
1. Аутентификация — проверка подлинности пользователя.
2. Авторизация — проверка прав доступа.
3. Валидация входных данных — защита от инъекций.
4. Rate limiting — защита от DoS.
Мой любимый нетривиальный приём — использование атрибута ApiController с собственным нестандартным фильтром действий для централизованной обработки ошибок и валидации:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[Route("api/[controller]")]
[ApiController]
[ServiceFilter(typeof(ApiExceptionFilter))]
public class ProductsController : ControllerBase
{
 private readonly IProductService _productService;
 
 public ProductsController(IProductService productService)
 {
     _productService = productService;
 }
 
 [HttpGet]
 [Authorize(Policy = "ReadProducts")]
 public async Task<ActionResult<List<ProductDto>>> GetProducts()
 {
     var products = await _productService.GetProductsAsync();
     return Ok(products);
 }
 
 [HttpPost]
 [Authorize(Policy = "CreateProducts")]
 [ValidateModelState]
 public async Task<ActionResult<ProductDto>> CreateProduct(CreateProductDto productDto)
 {
     var product = await _productService.CreateProductAsync(productDto);
     return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
 }
 
 [HttpGet("{id}")]
 [Authorize(Policy = "ReadProducts")]
 public async Task<ActionResult<ProductDto>> GetProduct(int id)
 {
     var product = await _productService.GetProductByIdAsync(id);
     
     if (product == null)
         return NotFound();
         
     return Ok(product);
 }
}
А вот как может выглядеть фильтр ApiExceptionFilter:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ApiExceptionFilter : IExceptionFilter
{
 private readonly IWebHostEnvironment _env;
 private readonly ILogger<ApiExceptionFilter> _logger;
 
 public ApiExceptionFilter(IWebHostEnvironment env, ILogger<ApiExceptionFilter> logger)
 {
     _env = env;
     _logger = logger;
 }
 
 public void OnException(ExceptionContext context)
 {
     _logger.LogError(context.Exception, context.Exception.Message);
     
     var error = new ApiError
     {
         Id = Guid.NewGuid().ToString(),
         Status = 500,
         Title = "Произошла ошибка при выполнении запроса"
     };
     
     if (_env.IsDevelopment())
     {
         error.Detail = context.Exception.StackTrace;
     }
     
     context.Result = new ObjectResult(error)
     {
         StatusCode = 500
     };
     
     context.ExceptionHandled = true;
 }
}

Особенности безопасности при разработке Progressive Web Apps (PWA) на Blazor



Progressive Web Apps (PWA) добавляют новый уровень сложности в обеспечение безопасности. Особенно если учесть, что PWA работают в офлайн-режиме и используют Service Workers. При создании PWA на базе Blazor WebAssembly важно учитывать следующие аспекты:

1. Безопасное хранение данных — все, что хранится в IndexedDB или localStorage, может быть доступно злоумышленикам при физическом доступе к устройству.
2. HTTPS — для PWA это не опция, а обязательное требование.
3. Обновление приложения — необходим механизм проверки новых версий и безопасное обновление.

Производительность и оптимизация



Когда SPA переходит из зоны "прототип для презентации" в "боевое приложение для тысяч пользователей", вопросы производительности становятся критически важными. Blazor при всей своей элегантности не является волшебной пилюлей — код всё равно нужно оптимизировать, особенно когда приложение растёт. Масштабные Blazor-проекты могут столкнуться со снижением responsiveness и заметными задержками, если не обращать внимания на производительность.

Стратегии кэширования и предварительной загрузки



Кэширование в Blazor работает на нескольких уровнях. Самый базовый — это HTTP-кэширование ресурсов. Но не стоит ограничиваться стандартным набором заголовков. В Blazor WebAssembly можно тонко настраивать кэширование через Service Worker:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// В файле service-worker.published.js
self.addEventListener('fetch', event => {
   if (event.request.method === 'GET') {
       // Стратегия cache-first для статических ресурсов
       if (event.request.url.includes('/css/') || 
           event.request.url.includes('/js/') || 
           event.request.url.includes('/fonts/')) {
           event.respondWith(
               caches.match(event.request)
                   .then(response => response || fetch(event.request)
                       .then(r => {
                           return caches.open('static-resources')
                               .then(cache => {
                                   cache.put(event.request, r.clone());
                                   return r;
                               });
                       }))
           );
           return;
       }
   }
   
   event.respondWith(
       fetch(event.request)
           .catch(() => caches.match(event.request))
   );
});
Для данных API можно использовать локальное кэширование с помощью IndexedDB. Блестяще работает связка с паттерном репозитория:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class CachedProductRepository : IProductRepository
{
   private readonly ApiProductRepository _apiRepository;
   private readonly LocalStorageProductRepository _localRepository;
   
   public CachedProductRepository(
       ApiProductRepository apiRepository,
       LocalStorageProductRepository localRepository)
   {
       _apiRepository = apiRepository;
       _localRepository = localRepository;
   }
   
   public async Task<IEnumerable<Product>> GetAllAsync()
   {
       try
       {
           // Пробуем получить свежие данные с API
           var products = await _apiRepository.GetAllAsync();
           // Кэшируем их локально
           await _localRepository.SaveAllAsync(products);
           return products;
       }
       catch (Exception)
       {
           // В случае ошибки используем локальный кэш
           return await _localRepository.GetAllAsync();
       }
   }
}
Предзагрузка данных — ещё одна недооценённая стратегия. Особенно хорошо работает в сочетании с IAsyncEnumerable для потоковой загрузки больших объёмов информации:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@inject IProductService ProductService
 
@foreach (var product in products)
{
   <ProductCard Product="product" />
}
 
@code {
   private List<Product> products = new();
   
   protected override async Task OnInitializedAsync()
   {
       await foreach (var batch in ProductService.GetProductBatchesAsync())
       {
           products.AddRange(batch);
           StateHasChanged(); // Обновляем UI после каждой порции
       }
   }
}

Ленивая загрузка модулей и компонентов



"Не грузи всё сразу" — золотое правило оптимизации. Blazor поддерживает ленивую загрузку через DynamicComponent и загрузку дополнительных сборок:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@page "/product/{id:int}"
@using System.Reflection
@implements IDisposable
 
@if (isLoaded)
{
   <DynamicComponent Type="@componentType" Parameters="@parameters" />
}
else
{
   <p>Загрузка редактора продукта...</p>
}
 
@code {
   [Parameter] public int Id { get; set; }
   private bool isLoaded;
   private Type componentType;
   private Dictionary<string, object> parameters;
   
   protected override async Task OnParametersSetAsync()
   {
       if (componentType == null)
       {
           try
           {
               // Загружаем дополнительную сборку "по требованию"
               var assemblies = await JsonSerializer.DeserializeAsync<string[]>(
                   await HttpClient.GetStreamAsync("_framework/lazyAssemblies.json"),
                   new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
                   
               if (assemblies.Contains("ProductEditor.dll"))
               {
                   await JSRuntime.InvokeVoidAsync("import", "_framework/ProductEditor.dll");
                   var assembly = Assembly.Load("ProductEditor");
                   componentType = assembly.GetType("ProductEditor.ProductEditorComponent");
               }
               
               parameters = new Dictionary<string, object>
               {
                   { "ProductId", Id }
               };
               
               isLoaded = true;
           }
           catch
           {
               // Обработка ошибок загрузки
           }
       }
   }
}
Для Blazor WebAssembly есть хитрый трюк с использованием нового режима AOT (Ahead-of-Time compilation), который значительно ускоряет первоначальную загрузку и выполнение. Тут уместен подход триммирования неиспользуемых частей фреймворка:

XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<PropertyGroup>
   <PublishTrimmed>true</PublishTrimmed>
   <TrimMode>link</TrimMode>
   <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
   <WasmBuildNative>true</WasmBuildNative>
</PropertyGroup>
 
<ItemGroup>
   <!-- Явно включаем только нужные локализации -->
   <TrimmerRootAssembly Include="System.Globalization" />
   
   <!-- Убираем из сборки неиспользуемые зависимости -->
   <BlazorWebAssemblyLazyLoad Include="ProductEditor.dll" />
</ItemGroup>

Профилирование и диагностика производительности



"Не оптимизируй, пока не измеришь" — мантра, которую стоит повторять ежедневно. Для профилирования Blazor есть несколько подходов:
1. Chrome DevTools для WebAssembly.
2. Встроенные диагностики .NET для Server.
3. Пользовательские мидлвары для точечных измерений.
Особенно полезна интеграция с Browser Performance API:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@inject IJSRuntime JS
 
@code {
   private async Task MeasureOperationAsync(Func<Task> operation, string operationName)
   {
       await JS.InvokeVoidAsync("performance.mark", $"{operationName}-start");
       await operation();
       await JS.InvokeVoidAsync("performance.mark", $"{operationName}-end");
       await JS.InvokeVoidAsync("performance.measure", 
           operationName, 
           $"{operationName}-start", 
           $"{operationName}-end");
       
       var measurements = await JS.InvokeAsync<Performance[]>("performance.getEntriesByType", "measure");
       Console.WriteLine($"{operationName} took {measurements[0].Duration}ms");
   }
}
Неочевидная проблема у многих Blazor-приложений — излишние перерендеринги компонентов. Для их отслеживания полезен паттерн аугментации компонентов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RenderTrackingComponent : ComponentBase
{
   private static readonly Dictionary<string, int> _renderCounts = new();
   
   protected override void OnAfterRender(bool firstRender)
   {
       var componentName = this.GetType().Name;
       
       if (!_renderCounts.ContainsKey(componentName))
       {
           _renderCounts[componentName] = 0;
       }
       
       _renderCounts[componentName]++;
       
       if (_renderCounts[componentName] % 10 == 0)
       {
           Console.WriteLine($"{componentName} rendered {_renderCounts[componentName]} times");
       }
   }
}
Унаследовав компоненты от этого базового класса, можно легко выявить те, которые перерисовываются чаще необходимого.

Серверный пререндеринг: улучшение First Contentful Paint



Самая большая проблема с Blazor WebAssembly — это "белый экран смерти" при начальной загрузке, пока runtime не загрузится. Решение — серверный пререндеринг:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
   services.AddMvc()
       .AddRazorPagesOptions(options =>
       {
           options.RootDirectory = "/";
       });
   
   // Включаем пререндеринг
   services.AddServerSideBlazor()
       .AddCircuitOptions(options =>
       {
           options.DetailedErrors = true;
       });
   
   // Обязательно регистрируем те же сервисы, что и в клиенте
   services.AddSingleton<WeatherForecastService>();
}
 
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   // ...
   app.UseEndpoints(endpoints =>
   {
       endpoints.MapBlazorHub();
       endpoints.MapFallbackToPage("/_Host");
   });
}
И в _Host.cshtml:

HTML5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@page "/"
@namespace MyBlazorApp
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
   Layout = null;
}
 
<!DOCTYPE html>
<html>
<head>
   <!-- Метатеги, стили и т.д. -->
</head>
<body>
   <app>
       @* Рендерим компоненты на сервере перед отправкой клиенту *@
       @(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
   </app>
   
   <script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

Тонкая настройка рендеринга компонентов



Знаете, в чём секрет реально быстрого Blazor приложения? В умном управлении рендерингом на компонентном уровне. Использование ShouldRender() — это как микрохирургическое вмешательство в процесс отрисовки:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@inherits ComponentBase
 
<div>
   <h3>@Title</h3>
   <p>@Content</p>
</div>
 
@code {
   [Parameter] public string Title { get; set; }
   [Parameter] public string Content { get; set; }
   
   private string _previousTitle;
   private string _previousContent;
   
   protected override bool ShouldRender()
   {
       // Перерисовываем только если реально изменились данные
       bool shouldRender = !string.Equals(Title, _previousTitle) ||
                         !string.Equals(Content, _previousContent);
                        
       _previousTitle = Title;
       _previousContent = Content;
       
       return shouldRender;
   }
}
Ещё более мощный механизм — виртуализация списков для огромных датасетов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<div style="height: 500px; overflow-y: auto;" @onscroll="OnScroll">
   @foreach (var item in visibleItems)
   {
       <ItemComponent Item="item" />
   }
</div>
 
@code {
   [Parameter] public List<Item> Items { get; set; }
   
   private List<Item> visibleItems = new();
   private int startIndex = 0;
   private readonly int pageSize = 20;
   
   protected override void OnParametersSet()
   {
       LoadVisibleItems();
   }
   
   private void OnScroll(EventArgs e)
   {
       // Определение текущей позиции скролла и загрузка новых элементов
       // при необходимости
   }
   
   private void LoadVisibleItems()
   {
       visibleItems = Items
           .Skip(startIndex)
           .Take(pageSize)
           .ToList();
   }
}
В Blazor 6.0 появился встроенный компонент Virtualize, который делает эту работу автоматически, даже с поддержкой "бесконечной" подгрузки:

C#
1
2
3
<Virtualize Context="product" Items="@allProducts" ItemSize="50" OverscanCount="10">
   <ProductListItem Product="product" />
</Virtualize>
Но его производительность не всегда идеальна. Часто ручная реализация работает быстрее, хотя требует больше кода.
Одна из самых мощных, но недокументированных оптимизаций — это передача рендеринга с сервера на клиент для тяжелых компонентов в Blazor Server:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@inject IJSRuntime JS
 
@if (useClientRendering)
{
   <div id="chart-container"></div>
}
else
{
   <HeavyServerChartComponent Data="data" />
}
 
@code {
   private bool useClientRendering;
   private List<DataPoint> data;
   
   protected override async Task OnInitializedAsync()
   {
       data = await DataService.GetDataAsync();
       
       // Определяем стратегию рендеринга в зависимости от размера данных
       useClientRendering = data.Count > 1000;
   }
   
   protected override async Task OnAfterRenderAsync(bool firstRender)
   {
       if (firstRender && useClientRendering)
       {
           await JS.InvokeVoidAsync("renderChart", data);
       }
   }
}
Этот подход распределяет вычислительную нагрузку и повышает отзывчивость приложения, особенно в сценариях с большим количеством пользователей на одном сервере.

В итоге оптимизация Blazor — это не столько следование правилам, сколько искусство балансирования между разными стратегиями и постоянное измерение результатов. Идеальное Blazor-приложение использует все уровни кэширования, загружает только то, что нужно прямо сейчас, и максимально эффективно управляет процессом рендеринга.

Реальные примеры и лучшие практики



Давайте рассмотрим несколько реальных примеров и лучших практик, которые помогут избежать самых распространённых ошибок.

Разбор типичных ошибок и их решений



Самая частая ошибка начинающих Blazor-разработчиков — это непонимание контекста выполнения компонентов. В Blazor Server все компоненты работают в контексте одного и того же потока на сервере, из-за чего возникают интересные эффекты:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@page "/concurrency-trap"
 
<button @onclick="StartLongRunningTask">Запустить долгую операцию</button>
<button @onclick="QuickOperation">Быстрая операция</button>
 
<p>Статус: @status</p>
 
@code {
   private string status = "Готов";
   
   private async Task StartLongRunningTask()
   {
       status = "Начинаем долгую операцию...";
       // Этот вызов заблокирует весь UI!
       await Task.Delay(5000);
       status = "Долгая операция завершена";
   }
   
   private void QuickOperation()
   {
       status = "Выполнена быстрая операция";
   }
}
Проблема этого кода в том, что клик по второй кнопке не сработает, пока выполняется длительная операция. Правильное решение — использовать механизм Task.Run() для переноса тяжёлых операций в другой поток:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
private async Task StartLongRunningTask()
{
    status = "Начинаем долгую операцию...";
    await Task.Run(async () => 
    {
        await Task.Delay(5000);
        // Важно: обновления UI должны происходить в исходном потоке
        await InvokeAsync(() => {
            status = "Долгая операция завершена";
            StateHasChanged();
        });
    });
}
Другая распространённая ошибка — злоупотребление событиями изменения состояния:

C#
1
2
3
4
5
// Так делать НЕ надо!
@foreach (var item in Items)
{
    <ItemComponent Item="item" OnChanged="@(() => StateHasChanged())" />
}
Этот код будет вызывать ререндер всего родительского компонента при изменении любого дочернего, что очень неэффективно. Вместо этого лучше пробросить коллбэк для конкретного элемента:

C#
1
2
3
4
5
6
7
8
9
10
11
12
@foreach (var item in Items)
{
    <ItemComponent Item="item" OnChanged="@(() => ItemChanged(item))" />
}
 
@code {
    private void ItemChanged(Item changedItem)
    {
        // Делаем что-то с конкретным измененным элементом,
        // без полной перерисовки родителя
    }
}

Непрерывная интеграция и доставка для Blazor SPA



CI/CD для Blazor-приложений имеет свои особенности, особенно при использовании WebAssembly. Вот простой, но эффективный пайплайн на базе GitHub Actions:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
name: Deploy Blazor App
 
on:
  push:
    branches: [ main ]
 
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        
      - name: Setup .NET
        uses: actions/setup-dotnet@v1
        with:
          dotnet-version: 6.0.x
          
      - name: Restore dependencies
        run: dotnet restore
        
      - name: Build
        run: dotnet build --configuration Release --no-restore
        
      - name: Test
        run: dotnet test --configuration Release --no-build
        
      - name: Publish
        run: dotnet publish --configuration Release --no-build -o publish
        
      - name: Deploy to Azure
        uses: azure/webapps-deploy@v2
        with:
          app-name: my-blazor-app
          publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}
          package: ./publish
Хитрость в том, что для Blazor WebAssembly нужно дополнительно настроить статический хостинг и правила перенаправления для поддержки маршрутизации на стороне клиента. Например, для Azure Static Web Apps достаточно добавить файл routes.json:

JSON
1
2
3
4
5
6
7
8
9
{
  "routes": [
    {
      "route": "/*",
      "serve": "/index.html",
      "statusCode": 200
    }
  ]
}

Тестирование Blazor-приложений



Тестирование — та область, где Blazor действительно блестит благодаря .NET экосистеме. Для модульного тестирования компонентов можно использовать bUnit:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Fact]
public void Counter_IncrementButton_IncrementsValue()
{
    // Arrange - рендерим компонент
    using var ctx = new TestContext();
    var cut = ctx.RenderComponent<Counter>();
    var initialCount = cut.Find("p").TextContent;
    
    // Act - нажимаем на кнопку
    cut.Find("button").Click();
    
    // Assert - проверяем, что счетчик увеличился
    Assert.NotEqual(initialCount, cut.Find("p").TextContent);
}
Для интеграционного тестирования API-эндпоинтов с аутентификацией есть элегантный паттерн с использованием WebApplicationFactory:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class ApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
 
    public ApiTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }
 
    [Fact]
    public async Task GetProducts_ReturnsSuccessAndProducts()
    {
        // Arrange
        var client = _factory
            .WithWebHostBuilder(builder =>
            {
                builder.ConfigureServices(services =>
                {
                    // Подменяем аутентификацию на тестовую
                    services.AddAuthentication("Test")
                        .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                            "Test", options => { });
                });
            })
            .CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
 
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Test");
 
        // Act
        var response = await client.GetAsync("/api/products");
 
        // Assert
        response.EnsureSuccessStatusCode();
        var products = await response.Content.ReadFromJsonAsync<List<ProductDto>>();
        Assert.NotEmpty(products);
    }
}
End-to-end тестирование часто проводят с помощью Selenium или, лучше, Playwright:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Fact]
public async Task Login_SuccessfulLogin_RedirectsToDashboard()
{
    // Arrange
    await using var browser = await Playwright.Chromium.LaunchAsync();
    var page = await browser.NewPageAsync();
    await page.GotoAsync("https://localhost:5001/login");
    
    // Act
    await page.FillAsync("#username", "test@example.com");
    await page.FillAsync("#password", "P@ssw0rd!");
    await page.ClickAsync("button[type=submit]");
    
    // Assert
    await page.WaitForURLAsync("**/dashboard");
    Assert.Contains("Welcome", await page.TextContentAsync("h1"));
}

Масштабирование Blazor Server в высоконагруженных проектах



Blazor Server поднимает интересные вопросы масштабирования из-за долгоживущих соединений и хранения состояния на сервере. Основные стратегии включают:
1. Sticky Sessions — направление запросов одного пользователя на один и тот же сервер,
2. Redis для хранения состояния сеанса — позволяет распределить нагрузку между несколькими серверами,
3. Предварительный рендеринг — уменьшает начальную нагрузку и время отклика.
Пример конфигурации SignalR с использованем Redis бэкплейна для масштабирования:

C#
1
2
3
4
5
6
7
services.AddSignalR()
    .AddStackExchangeRedis(options =>
    {
        options.Configuration.ChannelPrefix = "blazor_";
        options.Configuration.ClientName = "blazor-server";
        options.Configuration.EndPoints.Add("localhost:6379");
    });
При этом важно настроить балансировщик с поддержкой sticky sessions по cookie:

C#
1
2
3
4
5
services.AddSignalR().AddAzureSignalR(options =>
{
    options.ServerStickyMode = 
        Microsoft.Azure.SignalR.ServerStickyMode.Required;
});

Интеграция с внешними сервисами



Blazor отлично работает с внешними API, будь то REST, GraphQL или gRPC. Для GraphQL один из наиболее элегантных подходов — использование StrawberryShake:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Сгенерированный клиент
[global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "12.0.0")]
public partial class GetProductsQuery : IGetProductsQuery
{
    private readonly IOperationExecutor<IGetProductsResult> _operationExecutor;
 
    public GetProductsQuery(IOperationExecutor<IGetProductsResult> operationExecutor)
    {
        _operationExecutor = operationExecutor;
    }
 
    public async Task<IGetProductsResult> ExecuteAsync(
        CancellationToken cancellationToken = default)
    {
        return await _operationExecutor
            .ExecuteAsync(this, cancellationToken);
    }
}
И использование его в компоненте:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@inject IGetProductsQuery ProductsQuery
 
@if (products == null)
{
    <p>Loading...</p>
}
else
{
    <ProductList Products="products" />
}
 
@code {
    private IReadOnlyList<IProductInfo> products;
 
    protected override async Task OnInitializedAsync()
    {
        var result = await ProductsQuery.ExecuteAsync();
        products = result.Data?.Products;
    }
}
Для REST API идеально подходит Refit с его декларативным подходом:

C#
1
2
3
4
5
6
7
8
9
10
11
public interface IProductApi
{
    [Get("/api/products")]
    Task<List<ProductDto>> GetProductsAsync();
    
    [Get("/api/products/{id}")]
    Task<ProductDto> GetProductAsync(int id);
    
    [Post("/api/products")]
    Task<ProductDto> CreateProductAsync([Body] CreateProductDto product);
}
Регистрация и использование:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// В Program.cs
builder.Services.AddRefitClient<IProductApi>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));
    
// В компоненте
@inject IProductApi ProductApi
 
@code {
    private async Task CreateProduct(CreateProductDto dto)
    {
        try
        {
            var newProduct = await ProductApi.CreateProductAsync(dto);
            // Обработка успешного создания
        }
        catch (ApiException ex) when (ex.StatusCode == HttpStatusCode.BadRequest)
        {
            // Обработка ошибки валидации
        }
    }
}
В целом, создание SPA с использованем Blazor — это удивительно продуктивный процесс, если знать основные паттерны и избегать типичных ловушек. Комбинируя мощь .NET экосистемы с современными подходами к веб-разработке, можно создавать приложения, которые не уступают по функциональности и производительности тем, что написаны на React или Angular, но с значительно меньшими усилиями на поддержку и интеграцию.

Запуск React SPA из WebStorm и Visual Studio с включенным шифрованием
Всем привет. При создании проекта в Visual Studio выбрал React проект с поддержкой HTTPS. Из...

Оптимизация приложения SPA на Core
Дорогие, коллеги. Начал тягать недры разработки spa. Появились вопросы. Ищу ответы. В команде:...

Маршрутизация для SPA
день добрый. Подскажите, отличается ли чем-либо настройка маршрутов для SPA от маршрутов для...

asp net mvc spa. Не обновляется отображение страницы
Пишу одностраничное приложение. UI - Ext.Net. На главной странице в зависимости от значения...

В чем смысл писать SPA на ASP.NET CORE MVC?
В чем смысл писать веб-приложение на ASP.NET CORE MVC (исключая бэкэнд, базы данных и всякое...

Аутентификац­ия и авторизация SPA
Добрый день! Необходимо реализовать простенькую SPA и ASP.NET Core на сервере. Как правильно...

Создаем свой installer
Подскажите пожалуйста, как проще это сделать? Наверняка есть какие-либо готовые продукты. ...

Создаем "безопасное" приложение на ASP
Требуется сделать ASP-приложение, которое работает с БД SQL Server, как можно 'безопаснее'. Есть...

Создаем полупрозрачный прямоугольник на Image
Доброго времени суток :) Вот встала проблема нарисовать полупрозрачный прямоугольник на Image...

Создаем ярлык (Shortcut) приложения на рабочем столе, в пуске на C#
Создать ярлык приложения очень просто. Для этого нам понадобится библиотека...

Создаем архив в поток с DotNetZip.dll - ошибка сети в google chrome
Всем доброго дня! Используем библиотеку DotNetZip.dll для создания zip архива: ...

Создаем личную страницу пользователя
Как только пользователь авторизировался, то его перекидывает на форму, куда он вводит данные. После...

Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Чем асинхронная логика (схемотехника) лучше тактируемой, как я думаю, что помимо энергоэффективности - ещё и безопасность.
Hrethgir 14.05.2025
Помимо огромного плюса в энергоэффективности, асинхронная логика - тотальный контроль над каждым совершённым тактом, а значит - безусловная безопасность, где безконтрольно не совершится ни одного. . .
Многопоточные приложения на C++
bytestream 14.05.2025
C++ всегда был языком, тесно работающим с железом, и потому особеннно эффективным для многопоточного программирования. Стандарт C++11 произвёл революцию, добавив в язык нативную поддержку потоков,. . .
Stack, Queue и Hashtable в C#
UnmanagedCoder 14.05.2025
Каждый опытный разработчик наверняка сталкивался с ситуацией, когда невинный на первый взгляд List<T> превращался в узкое горлышко всего приложения. Причина проста: универсальность – это прекрасно,. . .
Как использовать OAuth2 со Spring Security в Java
Javaican 14.05.2025
Протокол OAuth2 часто путают с механизмами аутентификации, хотя по сути это протокол авторизации. Представьте, что вместо передачи ключей от всего дома вашему другу, который пришёл полить цветы, вы. . .
Анализ текста на Python с NLTK и Spacy
AI_Generated 14.05.2025
NLTK, старожил в мире обработки естественного языка на Python, содержит богатейшую коллекцию алгоритмов и готовых моделей. Эта библиотека отлично подходит для образовательных целей и. . .
Реализация DI в PHP
Jason-Webb 13.05.2025
Когда я начинал писать свой первый крупный PHP-проект, моя архитектура напоминала запутаный клубок спагетти. Классы создавали другие классы внутри себя, зависимости жостко прописывались в коде, а о. . .
Обработка изображений в реальном времени на C# с OpenCV
stackOverflow 13.05.2025
Объединение библиотеки компьютерного зрения OpenCV с современным языком программирования C# создаёт симбиоз, который открывает доступ к впечатляющему набору возможностей. Ключевое преимущество этого. . .
POCO, ACE, Loki и другие продвинутые C++ библиотеки
NullReferenced 13.05.2025
В C++ разработки существует такое обилие библиотек, что порой кажется, будто ты заблудился в дремучем лесу. И среди этого многообразия POCO (Portable Components) – как маяк для тех, кто ищет. . .
Паттерны проектирования GoF на C#
UnmanagedCoder 13.05.2025
Вы наверняка сталкивались с ситуациями, когда код разрастается до неприличных размеров, а его поддержка становится настоящим испытанием. Именно в такие моменты на помощь приходят паттерны Gang of. . .
Создаем CLI приложение на Python с Prompt Toolkit
py-thonny 13.05.2025
Современные командные интерфейсы давно перестали быть черно-белыми текстовыми программами, которые многие помнят по старым операционным системам. CLI сегодня – это мощные, интуитивные и даже. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru