Caching in Blazor server
I have worked with Microsoft products for more than twenty years. I haven't done all the time web projects, but enough to get familiar with different technologies. For example, in .aspx web forms, you had the application object - initialized at the first request and shared by all the website's users.
In ASP.NET MVC, a common technique for storing global objects was cache. ViewState was never a preferred solution to me. Something similar cannot be found in the Blazor server project type (at least not yet), and here is an alternative to get more or less the same result.
Real-life, simple examples are blog categories from the right side of the screen. There is no reason to query the database (where categories are stored) to get the same result in 99% of the cases. The database used is SQL Server. My personal preference is to use Dapper instead of Entity Framework. First, let's create a singleton service to hold the global data or provide access to cached data:
public sealed class GlobalCachedData : IDisposable, IGlobalCachedData
{
private IEnumerable<BlogCategoryModel>? _BlogCategories;
private readonly Timer _refreshTimer;
private readonly ISQLDataAccess _db;
public Task Initialization { get; private set; }
public GlobalCachedData(ISQLDataAccess db)
{
_db = db;
_refreshTimer = new Timer(RefreshData, null, TimeSpan.Zero, TimeSpan.FromMinutes(60));
Initialization = InitializeAsync();
}
private async Task InitializeAsync() => _BlogCategories = await LoadBlogCategories();
private async Task<IEnumerable<BlogCategoryModel>?> LoadBlogCategories() => await _db.LoadData<BlogCategoryModel, dynamic>("Blog.LoadCategories", null);
private async void RefreshData(object? state) => _BlogCategories = await LoadBlogCategories();
public IEnumerable<BlogCategoryModel>? GetBlogCategories() => _BlogCategories;
public void Dispose() => _refreshTimer.Dispose();
}
public interface IGlobalCachedData
{
Task Initialization { get; }
void Dispose();
IEnumerable<BlogCategoryModel>? GetBlogCategories();
}
BlogCategoryModel is the object mapped to a single row in the blog category table. _refreshTimer will refresh the data periodically, 60 minutes in the code above. LoadBlogCategories - use the ISQLDataAccess service to load the data from the SQL Server database. The initialization task is necessary because I cannot use 'await' in the class constructor - I don't know a better way of doing it. Also, from the class above, I extract the interface - it can work without it, of course - but it's just good practice. Register the GlobalDataService as a singleton in the Blazor application's dependency injection container (Program.cs file):
builder.Services.AddSingleton<IGlobalCachedData, GlobalCachedData>();
The last step is to use a razor component named CategoryWidget with the following content:
@inject IGlobalCachedData cachedData;
<div class="card-header">Categories</div>
<div class="card-body">
<div class="row border-1 bg-white">
@if (categories is not null)
{
@foreach (var cat in categories)
{
<div class="col-sm-6 mt-1 mb-1">
<a href="@string.Concat("category/",@cat.CategoryURL)">@cat.CategoryName</a></div>
}
}
</div>
</div>
// CategoryWidget.razor.cs
public partial class CategoryWidget
{
private IEnumerable<BlogCategoryModel>? categories;
protected override async Task OnInitializedAsync()
{
await cachedData.Initialization;
categories = cachedData.GetBlogCategories();
}
}
And this is it. The result is the component used to show blog categories on this website.
