BlazorToolkit
A comprehensive utility library designed to streamline Blazor application development.
BlazorToolkit provides developers with reusable components and services that reduce boilerplate code and enhance productivity when building interactive web applications. The toolkit simplifies common tasks like service execution, state management, and HTTP communication.
Core Features
Network Communication
Easy-to-use methods for handling HTTP requests and responses, reducing boilerplate code and potential errors.
Middleware Services
Implement middleware logic as injectable services, promoting clean separation of concerns and modularity.
Form Validators
Flexible validation mechanisms that integrate into Blazor forms, enabling immediate user feedback.
Installation
Package Manager:
Install-Package DevInstance.BlazorToolkit
.NET CLI:
dotnet add package DevInstance.BlazorToolkit
License
BlazorToolkit is released under the MIT License, allowing free usage, modification, and distribution.
Documentation
BlazorToolkit Usage Guide
A comprehensive guide to using the BlazorToolkit library for service execution, state management, and HTTP communication in Blazor applications.
Table of Contents
- Installation & Setup
- Service Execution
- HTTP API Context
- State Management
- Error Handling
- Complete Examples
Installation & Setup
1. Register Services
BlazorToolkit uses attribute-based service discovery. Mark your services with [BlazorService] and register them in Program.cs:
// Program.cs (Server)
builder.Services.AddHttpClient("BlazorToolkitClient");
builder.Services.AddBlazorServices();
// Program.cs (Client/WASM)
builder.Services.AddHttpClient("MyAppClient",
client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
builder.Services.AddScoped<HttpApiContextFactory>(sp =>
{
var factory = sp.GetRequiredService<IHttpClientFactory>();
return new HttpApiContextFactory(factory, "MyAppClient", "api");
});
builder.Services.AddBlazorServices();
2. Create a Service
Services are marked with the [BlazorService] attribute and automatically registered during startup. By default, services are registered with Scoped lifetime.
using DevInstance.BlazorToolkit.Tools;
using DevInstance.BlazorToolkit.Services;
// Default: Scoped lifetime
[BlazorService]
public class TodoService : ITodoService
{
// Service implementation
}
Service Lifetimes
You can specify different service lifetimes using the attribute constructor or property:
// Singleton - single instance for the entire application
[BlazorService(ServiceLifetime.Singleton)]
public class CacheService : ICacheService
{
// Shared state across all requests
}
// Transient - new instance for each injection
[BlazorService(ServiceLifetime.Transient)]
public class TemporaryService : ITemporaryService
{
// Fresh instance every time
}
// Scoped - instance per request/scope (default)
[BlazorService(ServiceLifetime.Scoped)]
public class UserContextService : IUserContextService
{
// Shared within a request
}
// Alternative property syntax
[BlazorService(Lifetime = ServiceLifetime.Singleton)]
public class ConfigService : IConfigService
{
// Configuration service
}
Choosing the Right Lifetime:
- Singleton: Use for stateless services, caches, or configuration that should be shared globally
- Scoped: Use for services that maintain state during a user session or request (default for most Blazor services)
- Transient: Use for lightweight, stateless services where you need a fresh instance each time
Service Execution
The service execution pattern provides automatic progress tracking, error handling, and state management for async operations.
Core Concepts
IServiceExecutionHost- Interface implemented by components to handle service call stateServiceExecutionHandler- Orchestrates service calls with automatic state managementServiceActionResult<T>- Unified result type for all service operations
Using ServiceReadAsync
Use ServiceReadAsync for data fetching operations:
@inherits ServiceExecutionHostComponent
@inject ITodoService TodoService
@code {
private ModelList<TodoItem>? todos;
protected override async Task OnInitializedAsync()
{
await LoadData();
}
private async Task LoadData()
{
await this.ServiceReadAsync(
handler: async () => await TodoService.GetItemsAsync(),
success: result => todos = result
);
}
}
Using ServiceSubmitAsync
Use ServiceSubmitAsync for create/update/delete operations:
private TodoItem newTodo = new();
private async Task AddTodo()
{
await this.ServiceSubmitAsync(
handler: async () => await TodoService.AddAsync(newTodo),
success: result =>
{
todos = result;
newTodo = new TodoItem(); // Reset form
}
);
}
Full Method Signatures
// Read operation
await this.ServiceReadAsync<T>(
handler: async () => await service.GetAsync(), // Required: the async operation
success: result => { }, // Optional: success callback
stateKey: "myKey", // Optional: key for state persistence
sucessAsync: async result => { }, // Optional: async success callback
error: errors => { return false; }, // Optional: error handler (return true to suppress)
before: () => { }, // Optional: runs before the call
enableProgress: true, // Optional: show progress indicator
log: scopeLog // Optional: logging
);
// Submit operation (same signature)
await this.ServiceSubmitAsync<T>(...);
Using CallContext
For cleaner code with many parameters, use CallContext<T>:
await this.ServiceReadAsync(new CallContext<ModelList<TodoItem>>
{
Handler = async () => await TodoService.GetItemsAsync(),
Success = result => todos = result,
StateKey = nameof(todos),
EnableProgress = true,
Error = errors =>
{
// Custom error handling
return false;
}
});
Advanced CallContext Usage: Reusable Actions
CallContext is particularly powerful when you need to reuse the same action across multiple sequential operations. A common pattern is refreshing a list after modifying it:
// Define a reusable refresh action
private CallContext<ModelList<TodoItem>> refreshTodos = new()
{
Handler = async () => await TodoService.GetItemsAsync(currentQuery),
Success = result => todos = result,
StateKey = nameof(todos)
};
// Delete an item and refresh the list
private async Task DeleteTodo(string id)
{
await this.BeginServiceCall(ServiceExecutionType.Submitting)
.DispatchCall(
handler: async () => await TodoService.DeleteAsync(id),
success: _ => { /* Delete succeeded */ }
)
.DispatchCall(refreshTodos) // Reuse the refresh action
.ExecuteAsync();
}
// Update an item and refresh the list
private async Task UpdateTodo(TodoItem item)
{
await this.BeginServiceCall(ServiceExecutionType.Submitting)
.DispatchCall(
handler: async () => await TodoService.UpdateAsync(item),
success: _ => { /* Update succeeded */ }
)
.DispatchCall(refreshTodos) // Reuse the same refresh action
.ExecuteAsync();
}
Key Benefits:
- DRY Principle: Define the refresh logic once, reuse it everywhere
- Consistency: All operations use the same refresh mechanism
- Maintainability: Change the refresh logic in one place
Manual Service Execution
For advanced scenarios, use BeginServiceCall directly:
await this.BeginServiceCall(ServiceExecutionType.Reading)
.DispatchCall(
handler: async () => await Service1.GetAsync(),
success: r => result1 = r
)
.DispatchCall(
handler: async () => await Service2.GetAsync(),
success: r => result2 = r
)
.ExecuteAsync();
Sequential Execution Guarantee
Important: All DispatchCall operations execute sequentially in the order they are chained. This means:
- Each call completes before the next one starts
- You can safely use results from previous calls
- If any call fails, subsequent calls are not executed
private TodoItem? selectedItem;
// Example: Load item details, then load related comments
await this.BeginServiceCall(ServiceExecutionType.Reading)
.DispatchCall(
handler: async () => await TodoService.GetByIdAsync(itemId),
success: result => selectedItem = result // Store the result
)
.DispatchCall(
handler: async () => await CommentService.GetCommentsAsync(selectedItem.Id),
success: result => comments = result // Safely use selectedItem from previous call
)
.ExecuteAsync();
This sequential behavior makes it safe to chain dependent operations without worrying about race conditions or synchronization.
HTTP API Context
The HTTP layer provides a fluent API for building and executing HTTP requests.
Creating an API Context
Via Factory (Recommended)
// In Program.cs
builder.Services.AddScoped(sp =>
{
var factory = sp.GetRequiredService<HttpApiContextFactory>();
return factory.Create<TodoItem>("MyAppClient", "api/todos");
});
// Or with default configuration
builder.Services.AddScoped<HttpApiContextFactory>(sp =>
{
var httpFactory = sp.GetRequiredService<IHttpClientFactory>();
return new HttpApiContextFactory(httpFactory, "MyAppClient", "api");
});
// Then create contexts with relative URLs
var todosApi = factory.CreateDefault<TodoItem>("todos");
var usersApi = factory.CreateDefault<User>("users");
Basic CRUD Operations
[BlazorService]
public class TodoService : ITodoService
{
private readonly IApiContext<TodoItem> Api;
public TodoService(IApiContext<TodoItem> api)
{
Api = api;
}
// GET all items
public async Task<ServiceActionResult<ModelList<TodoItem>?>> GetAllAsync()
{
return await ServiceUtils.HandleWebApiCallAsync(
async (log) => await Api.Get().ExecuteListAsync()
);
}
// GET single item by ID
public async Task<ServiceActionResult<TodoItem?>> GetByIdAsync(string id)
{
return await ServiceUtils.HandleWebApiCallAsync(
async (log) => await Api.Get(id).ExecuteAsync()
);
}
// POST new item
public async Task<ServiceActionResult<TodoItem?>> CreateAsync(TodoItem item)
{
return await ServiceUtils.HandleWebApiCallAsync(
async (log) => await Api.Post(item).ExecuteAsync()
);
}
// PUT update item
public async Task<ServiceActionResult<TodoItem?>> UpdateAsync(TodoItem item)
{
return await ServiceUtils.HandleWebApiCallAsync(
async (log) => await Api.Put(item, item.Id).ExecuteAsync()
);
}
// DELETE item
public async Task<ServiceActionResult<bool>> DeleteAsync(string id)
{
return await ServiceUtils.HandleWebApiCallAsync(
async (log) =>
{
await Api.Delete(id).ExecuteAsync();
return true;
}
);
}
}
URL Building
Using ApiUrlBuilder
var url = ApiUrlBuilder.Create("api/companies")
.Path(companyId)
.Path("employees")
.Query("department", "Engineering")
.Query("active", true)
.ToString();
// Result: api/companies/123/employees?department=Engineering&active=True
Fluent URL Building with IApiContext
// Build complex URLs
var result = await Api
.Get()
.Path("active")
.Parameter("category", "work")
.Parameter("priority", "high")
.ExecuteListAsync();
Query Parameters
Manual Parameters
var result = await Api
.Get()
.Parameter("top", 10)
.Parameter("page", 0)
.Parameter("search", "meeting")
.ExecuteListAsync();
Using Extension Methods
using DevInstance.BlazorToolkit.Http.Extensions;
var result = await Api
.Get()
.Top(10)
.Page(0)
.Search("meeting")
.Sort("createdAt", ascending: false)
.ExecuteListAsync();
Using Query Objects
Define a query model:
public class TodoQueryModel
{
public int Top { get; set; }
public int Page { get; set; }
public string? Search { get; set; }
[QueryName("filter")] // Custom parameter name
public string? Category { get; set; }
public string[]? Include { get; set; } // Arrays become comma-separated
}
Use with the Query() extension:
var query = new TodoQueryModel
{
Top = 10,
Page = 0,
Search = "meeting",
Include = new[] { "subtasks", "comments" }
};
var result = await Api.Get().Query(query).ExecuteListAsync();
// URL: api/todos?top=10&page=0&search=meeting&include=subtasks,comments
State Management
Prerendering State Persistence
BlazorToolkit supports state persistence across prerendering using the stateKey parameter:
// The state key identifies data to persist
await this.ServiceReadAsync(
handler: async () => await TodoService.GetItemsAsync(),
success: result => todos = result,
stateKey: nameof(todos) // Data is cached and restored after prerendering
);
Progress and State Tracking
Components inheriting from ServiceExecutionHostComponent have automatic state tracking:
@inherits ServiceExecutionHostComponent
@if (InProgress)
{
<LoadingSpinner />
}
else if (IsError)
{
<ErrorMessage Text="@ErrorMessage" />
}
else
{
<TodoList Items="@todos" />
}
<!-- Or check specific operation types -->
@if (ServiceState == ServiceExecutionType.Reading)
{
<span>Loading...</span>
}
else if (ServiceState == ServiceExecutionType.Submitting)
{
<span>Saving...</span>
}
Disabling Progress Indication
For background operations that shouldn't show loading states:
await this.ServiceReadAsync(
handler: async () => await Service.RefreshAsync(),
success: result => data = result,
enableProgress: false // Won't set InProgress = true
);
Error Handling
ServiceActionResult
All service operations return ServiceActionResult<T>:
public class ServiceActionResult<T>
{
public T? Result { get; set; }
public bool Success { get; set; }
public ServiceActionError[]? Errors { get; set; }
public bool IsAuthorized { get; set; }
}
Creating Results in Services
// Success
return ServiceActionResult<TodoItem>.OK(newTodo);
// Failure with message
return ServiceActionResult<TodoItem>.Failed("Item not found");
// Failure with validation error
return ServiceActionResult<TodoItem>.Failed(new ServiceActionError
{
ErrorType = ServiceActionErrorType.Validation,
PropertyName = nameof(TodoItem.Title),
Message = "Title is required"
});
// Unauthorized
return ServiceActionResult<TodoItem>.Unauthorized();
Custom Error Handling in Components
await this.ServiceSubmitAsync(
handler: async () => await TodoService.AddAsync(newTodo),
success: result => todos = result,
error: errors =>
{
// Handle validation errors
foreach (var error in errors)
{
if (error.ErrorType == ServiceActionErrorType.Validation)
{
validationMessages[error.PropertyName] = error.Message;
}
}
// Return true to suppress the default error display
// Return false to show the error in ErrorMessage
return true;
}
);
Integration with Form Validation
Use ServiceResultValidation to display errors in forms:
<EditForm Model="@newTodo" OnValidSubmit="@AddTodo">
<ServiceResultValidationEx @ref="validator" CssProvider="new BootstrapFieldCssClassProvider()" />
<InputText @bind-Value="newTodo.Title" class="form-control" />
<BoostrapValidationMessage For="@(() => newTodo.Title)" />
<button type="submit" disabled="@InProgress">Add</button>
</EditForm>
@code {
private ServiceResultValidationEx<BootstrapFieldCssClassProvider>? validator;
private async Task AddTodo()
{
validator?.ClearErrors();
await this.ServiceSubmitAsync(
handler: async () => await TodoService.AddAsync(newTodo),
success: result => todos = result,
error: errors => validator?.DisplayErrors(errors) ?? false
);
}
}
Complete Examples
Server-Side Service
using DevInstance.BlazorToolkit.Tools;
using DevInstance.BlazorToolkit.Services;
using DevInstance.BlazorToolkit.Services.Server;
[BlazorService]
public class TodoService : ITodoService
{
private readonly TodoRepository _repository;
public TodoService(TodoRepository repository)
{
_repository = repository;
}
public async Task<ServiceActionResult<ModelList<TodoItem>?>> GetItemsAsync(TodoQueryModel query)
{
return await ServiceUtils.HandleServiceCallAsync(
async (log) => await _repository.GetItemsAsync(query.Top, query.Page, query.Search)
);
}
public async Task<ServiceActionResult<ModelList<TodoItem>?>> AddAsync(TodoItem newTodo)
{
// Validation
if (string.IsNullOrWhiteSpace(newTodo.Title))
{
return ServiceActionResult<ModelList<TodoItem>?>.Failed(new ServiceActionError
{
ErrorType = ServiceActionErrorType.Validation,
PropertyName = nameof(newTodo.Title),
Message = "Title is required"
});
}
// Check for duplicates
if (await _repository.ExistsAsync(newTodo.Title))
{
return ServiceActionResult<ModelList<TodoItem>?>.Failed(new ServiceActionError
{
ErrorType = ServiceActionErrorType.Validation,
PropertyName = nameof(newTodo.Title),
Message = "A todo with this title already exists"
});
}
return await ServiceUtils.HandleServiceCallAsync(
async (log) => await _repository.AddAsync(newTodo)
);
}
}
Client-Side Service (WASM)
using DevInstance.BlazorToolkit.Tools;
using DevInstance.BlazorToolkit.Services;
using DevInstance.BlazorToolkit.Services.Wasm;
using DevInstance.BlazorToolkit.Http;
[BlazorService]
public class TodoService : ITodoService
{
private readonly IApiContext<TodoItem> _api;
public TodoService(IApiContext<TodoItem> api)
{
_api = api;
}
public async Task<ServiceActionResult<ModelList<TodoItem>?>> GetItemsAsync(TodoQueryModel query)
{
return await ServiceUtils.HandleWebApiCallAsync(
async (log) => await _api.Get().Query(query).ExecuteListAsync()
);
}
public async Task<ServiceActionResult<ModelList<TodoItem>?>> AddAsync(TodoItem newTodo)
{
return await ServiceUtils.HandleWebApiCallAsync(
async (log) => await _api.Post(newTodo).ExecuteListAsync()
);
}
public async Task<ServiceActionResult<ModelList<TodoItem>?>> UpdateAsync(TodoItem todo)
{
return await ServiceUtils.HandleWebApiCallAsync(
async (log) => await _api.Put(todo, todo.Id).ExecuteListAsync()
);
}
public async Task<ServiceActionResult<ModelList<TodoItem>?>> DeleteAsync(string id)
{
return await ServiceUtils.HandleWebApiCallAsync(
async (log) => await _api.Delete(id).ExecuteListAsync()
);
}
}
Component
@page "/todos"
@inherits ServiceExecutionHostComponent
@inject ITodoService TodoService
<h1>Todo List</h1>
<ErrorMessageBanner IsError="@IsError" Message="@ErrorMessage" />
<EditForm Model="@newTodo" OnValidSubmit="@AddTodo">
<ServiceResultValidationEx @ref="validator"
CssProvider="new BootstrapFieldCssClassProvider()" />
<div class="input-group mb-3">
<InputText @bind-Value="newTodo.Title"
class="form-control"
placeholder="New todo..."
disabled="@InProgress" />
<button type="submit" class="btn btn-primary" disabled="@InProgress">
@if (IsSubmitting)
{
<span class="spinner-border spinner-border-sm"></span>
<span>Adding...</span>
}
else
{
<span>Add</span>
}
</button>
</div>
<BoostrapValidationMessage For="@(() => newTodo.Title)" />
</EditForm>
@if (InProgress && ServiceState == ServiceExecutionType.Reading)
{
<div class="text-center p-4">
<div class="spinner-border"></div>
<p>Loading todos...</p>
</div>
}
else if (todos?.Items != null)
{
<ul class="list-group">
@foreach (var todo in todos.Items)
{
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="@(todo.IsCompleted ? "text-decoration-line-through" : "")">
@todo.Title
</span>
<button class="btn btn-sm btn-danger"
@onclick="() => DeleteTodo(todo.Id)"
disabled="@InProgress">
Delete
</button>
</li>
}
</ul>
<DataPager PagesCount="@todos.TotalPages"
SelectedPage="@currentPage"
OnPageChanged="@ChangePage"
InProgress="@InProgress" />
}
@code {
private ModelList<TodoItem>? todos;
private TodoItem newTodo = new();
private int currentPage = 0;
private const int PageSize = 10;
private ServiceResultValidationEx<BootstrapFieldCssClassProvider>? validator;
private bool IsSubmitting => ServiceState == ServiceExecutionType.Submitting;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await ChangePage(0);
}
private async Task ChangePage(int page)
{
currentPage = page;
await this.ServiceReadAsync(
handler: async () => await TodoService.GetItemsAsync(
new TodoQueryModel { Top = PageSize, Page = page }
),
success: result => todos = result,
stateKey: nameof(todos)
);
}
private async Task AddTodo()
{
validator?.ClearErrors();
await this.ServiceSubmitAsync(
handler: async () => await TodoService.AddAsync(newTodo),
success: result =>
{
todos = result;
newTodo = new TodoItem();
},
error: errors => validator?.DisplayErrors(errors) ?? false
);
}
private async Task DeleteTodo(string id)
{
await this.ServiceSubmitAsync(
handler: async () => await TodoService.DeleteAsync(id),
success: result => todos = result
);
}
}
API Reference
Key Types
| Type | Namespace | Description |
|---|---|---|
IServiceExecutionHost |
Services |
Interface for components handling service state |
ServiceExecutionHandler |
Services |
Orchestrates service call execution |
ServiceActionResult<T> |
Services |
Unified result wrapper |
ServiceActionError |
Services |
Error information |
CallContext<T> |
Services |
Parameter container for service calls |
IApiContext<T> |
Http |
HTTP request builder interface |
HttpApiContextFactory |
Http |
Creates API context instances |
ApiUrlBuilder |
Http |
Fluent URL construction |
Extension Methods
| Method | Description |
|---|---|
ServiceReadAsync<T>() |
Execute a read operation |
ServiceSubmitAsync<T>() |
Execute a submit operation |
BeginServiceCall() |
Start manual service execution |
SetException() |
Set error state from exception |
Top(), Page(), Search(), Sort() |
Query parameter helpers |
Query<T>() |
Serialize object to query parameters |