microsoft/managing-blazor-server-data-access
> Manages data access and state in Blazor Server applications. Solves critical Blazor Server cart/wizard state loss, scoped lifetime issues, Session NullReferenceException. Configures IDbContextFactory for long-lived circuits (required for ANY Blazor Server app using EF Core), Session/ViewState/Application state alternatives (ProtectedSessionStorage, scoped services, database-backed), and DataSource to service injection migration. Covers Web.config to appsettings.json. Use when handling data access in Blazor Server, fixing session/DbContext/cart errors, resolving build errors for System.Data.Entity namespaces or EntityFramework package references, or troubleshooting data access failures in Blazor Server applications.
npx skills add https://github.com/microsoft/upgrade-agent-plugins --skill managing-blazor-server-data-access
Manages data access and state in Blazor Server applications. Covers critical Blazor Server patterns required for long-lived circuits (IDbContextFactory, Session state alternatives, shopping cart persistence).
Reference guide for complementary patterns. The workflow steps below contain explicit instructions on when to invoke each skill.
| Skill | Use For |
|-------|---------|
| migrating-webforms-to-blazor-server | Initial WebForms to Blazor migration (project setup, Routes, markup conversion) |
| managing-blazor-server-authentication | Authentication setup and patterns in Blazor Server |
| migrating-ef6-code-first-to-ef-core | Entity Framework 6 Code-First to EF Core migration → Step 1 |
| migrating-mvc-configuration | Web.config to appsettings.json migration → Step 4 |
This skill works with any Blazor Server application.
If migrating from WebForms, the migrating-webforms-to-blazor-server skill handles initial Blazor Server setup.
Configure Blazor Server data and state management in order:
Configuration Progress:
- [ ] Step 1: Set up data infrastructure (DbContext factory)
- [ ] Step 2: Configure data binding (DataSource to services)
- [ ] Step 3: Configure state management (Session to storage)
- [ ] Step 4: Migrate configuration (Web.config to appsettings.json)
If the Web Forms project uses Entity Framework, check Web.config connection strings to determine the database provider. CRITICAL: Preserve the original database provider - do NOT default to SQLite unless the original project specifically used it. SQL Server (including LocalDB) is the most common Web Forms database.
Detect provider from Web.config:
| Web.config Indicator | Database Provider | EF Core Package to Install |
|---------------------|-------------------|----------------------------|
| providerName="System.Data.SqlClient" | SQL Server | Microsoft.EntityFrameworkCore.SqlServer |
| providerName="System.Data.SQLite" | SQLite | Microsoft.EntityFrameworkCore.Sqlite |
| providerName="Npgsql" | PostgreSQL | Npgsql.EntityFrameworkCore.PostgreSQL |
| providerName="MySql.Data.MySqlClient" | MySQL | Pomelo.EntityFrameworkCore.MySql |
Add the detected provider to your Blazor project:
# Example for SQL Server (most common)
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
> Note: If you previously completed migrating-webforms-to-blazor-server Step 1, the provider packages are already installed - skip to the next section.
Identify your data access pattern and execute the appropriate skill:
| Original Pattern | Skill to Execute | Returns Here? |
|-----------------|------------------|---------------|
| EF6 Code-First (no .edmx files) | migrating-ef6-code-first-to-ef-core | ✅ Yes |
| EDMX (Database-First/Model-First) | migrating-edmx-to-code-first | ✅ Yes |
| LINQ to SQL (System.Data.Linq) | migrating-linq-to-sql-to-ef-core | ✅ Yes |
All EF Core migration skills handle:
DbContextOptions)⚠️ Critical: Those skills use AddDbContext (scoped lifetime), which does NOT work in Blazor Server long-lived circuits. After completing any EF migration skill, you must apply the Blazor-specific configuration below.
⚠️ CRITICAL: Replace the AddDbContext registration from the EF migration skill with AddDbContextFactory:
The EF migration skills use AddDbContext (scoped lifetime), which is correct for MVC/API but WILL FAIL in Blazor Server. You must replace it.
Before (from EF migration skill - DO NOT USE):
// ❌ WRONG for Blazor Server
builder.Services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
After (Blazor Server pattern - REQUIRED):
// ✅ CORRECT for Blazor Server — use IDbContextFactory for long-lived Blazor circuits
builder.Services.AddDbContextFactory<SchoolContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// ↑ Replace with UseNpgsql(), UseMySql(), UseSqlite(), etc. to match original provider
Why IDbContextFactory? Blazor Server circuits are long-lived. A single DbContext instance would accumulate stale data and tracking issues. The factory pattern creates short-lived contexts per operation.
Verification: After making this change, search Program.cs for AddDbContext. If found, you missed the replacement - it MUST be AddDbContextFactory for Blazor Server.
If the WebForms project uses raw ADO.NET (SqlCommand, SqlDataReader) without Entity Framework:
Execute migrating-to-microsoft-data-sqlclient skill to upgrade the provider (System.Data.SqlClient → Microsoft.Data.SqlClient). This works in Blazor Server with standard DI-registered services - no IDbContextFactory needed.
Example service pattern:
public class ProductRepository(IConfiguration configuration)
{
public async Task<List<Product>> GetProductsAsync()
{
using var connection = new SqlConnection(configuration.GetConnectionString("DefaultConnection"));
using var command = new SqlCommand("SELECT * FROM Products", connection);
await connection.OpenAsync();
// ... read data
}
}
Create services to encapsulate database operations:
// ProductService.cs
public class ProductService(IDbContextFactory<SchoolContext> factory)
{
public async Task<List<Product>> GetProductsAsync()
{
using var db = factory.CreateDbContext();
return await db.Products.ToListAsync();
}
public async Task<Product?> GetProductAsync(int id)
{
using var db = factory.CreateDbContext();
return await db.Products.FindAsync(id);
}
}
// Program.cs
builder.Services.AddScoped<IProductService, ProductService>();
Before DbContext can use GetConnectionString(), migrate connection strings from Web.config to appsettings.json:
Web.config:
<connectionStrings>
<add name="DefaultConnection"
connectionString="Data Source=(LocalDb)\MSSQLLocalDB;Initial Catalog=MyApp;Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
Create or update appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=(LocalDb)\\MSSQLLocalDB;Initial Catalog=MyApp;Integrated Security=True"
}
}
Key changes:
providerName attribute (provider specified in code via UseSqlServer(), etc.)\ → \\ConnectionStrings (capital C and S)> Note: Complete configuration migration (appSettings, custom sections, secrets, environment files) will be done in Step 4.
Web Forms declarative DataSource controls (SqlDataSource, ObjectDataSource, EntityDataSource) have no equivalent in Blazor. Replace with dependency-injected services that load data programmatically in component lifecycle methods.
<!-- Web Forms — declarative data binding -->
<asp:SqlDataSource ID="ProductsDS" runat="server"
ConnectionString="<%$ ConnectionStrings:DefaultConnection %>"
SelectCommand="SELECT * FROM Products" />
<asp:GridView DataSourceID="ProductsDS" runat="server" />
@* Blazor — service injection + @foreach table *@
@inject IProductService ProductService
<table>
<thead><tr><th>Name</th><th>Price</th></tr></thead>
<tbody>
@foreach (var p in products)
{
<tr>
<td>@p.Name</td>
<td>@p.UnitPrice.ToString("C")</td>
</tr>
}
</tbody>
</table>
@code {
private List<Product> products = [];
protected override async Task OnInitializedAsync()
{
products = await ProductService.GetProductsAsync();
}
}
> For advanced scenarios (sorting, filtering, pagination), use Microsoft.AspNetCore.Components.QuickGrid (dotnet add package Microsoft.AspNetCore.Components.QuickGrid).
// Program.cs — use the provider that matches the original Web Forms database
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddDbContextFactory<ProductContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// ↑ Match the original provider: UseNpgsql(), UseMySql(), UseSqlite(), etc.
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<ICategoryService, CategoryService>();
builder.Services.AddScoped<IOrderService, OrderService>();
// ... after builder.Build() ...
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
> CRITICAL: When using <Routes @rendermode="InteractiveServer" /> (global interactive server mode), HttpContext.Session is NULL during WebSocket rendering. Session state access inside Blazor components will throw NullReferenceException or silently fail.
Why: After the initial HTTP request establishes the SignalR circuit, Blazor communicates over WebSocket. There is no HTTP request/response, and therefore no session middleware processing during component interactions.
Solution: Replace Session state with one of four patterns based on your requirements:
| Pattern | Use For | Survives Refresh? | Survives Disconnect? |
|---------|---------|-------------------|----------------------|
| Option A: Minimal API Endpoints | Form submissions, business operations | ✅ Yes | ✅ Yes (if persisted) |
| Option B: Scoped Services | Transient UI state (wizard progress, filters) | ❌ No | ❌ No |
| Option C: Database-Backed | Business-critical state (cart, preferences) | ✅ Yes | ✅ Yes |
| Option D: ProtectedSessionStorage | Browser tab-specific state | ✅ Yes | ❌ No |
For detailed implementation examples and decision guidance, read ref/session-state-patterns.md.
Quick reference:
Complete configuration migration from Web.config to appsettings.json. This step handles all configuration beyond the connection strings already migrated in Step 1.
Execute the migrating-mvc-configuration skill to migrate:
Execute that skill to complete configuration migration, then return here.
Configuration access in Blazor components uses @inject directive syntax instead of constructor injection:
// Blazor component
@inject IConfiguration Configuration
@inject IOptions<AppSettings> AppSettingsOptions
@code {
protected override void OnInitialized()
{
var apiUrl = Configuration["AppSettings:ApiBaseUrl"];
var maxPageSize = AppSettingsOptions.Value.MaxPageSize;
}
}
The underlying concepts (IConfiguration, IOptions<T>, registration in Program.cs) are identical to the general skill's guidance.
For detailed implementation patterns and code examples:
Blazor Server circuits are long-lived. Always use IDbContextFactory and create short-lived DbContext instances per operation.
⚠️ CRITICAL VALIDATION: After completing EF migration steps, search Program.cs for the pattern AddDbContext<. If found, you have NOT correctly applied the Blazor Server pattern - it MUST be AddDbContextFactory< instead. The referenced EF migration skills use AddDbContext (correct for MVC/API), which will cause runtime failures in Blazor Server.
Web Forms SelectMethod runs inside a page lifecycle. Blazor doesn't have this. Use explicit transaction scopes in services if needed:
using var db = factory.CreateDbContext();
using var transaction = await db.Database.BeginTransactionAsync();
// ... operations
await transaction.CommitAsync();
Web Forms SelectMethod returns IQueryable synchronously. Blazor services should be async:
// WRONG: return db.Products.ToList();
// RIGHT: return await db.Products.ToListAsync();
ConfigurationManager.AppSettings["key"] doesn't exist. Inject IConfiguration or use the Options pattern.
Web Forms often has static helper classes that access HttpContext.Current. These must be refactored to accept dependencies via constructor injection.
Take microsoft/managing-blazor-server-data-access from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.