Skip to main content

EF Core Session Store

Vali-Blob.EFCore provides EfCoreResumableSessionStore, which persists resumable upload sessions in a relational database via Entity Framework Core. It works with any EF Core-supported database: PostgreSQL, MySQL, SQL Server, and SQLite.

Use the EF Core store when your application already uses EF Core and you prefer a SQL database for session persistence over running a separate Redis instance.


Installation

dotnet add package Vali-Blob.EFCore

Also install the EF Core provider for your target database:

# PostgreSQL
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

# MySQL / MariaDB
dotnet add package Pomelo.EntityFrameworkCore.MySql

# SQL Server
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

# SQLite (embedded)
dotnet add package Microsoft.EntityFrameworkCore.Sqlite

Setup

Step 1: Add to DbContext

Add ResumableUploadSessionEntity to your existing DbContext and call modelBuilder.ConfigureVali-Blob() to apply the table schema:

using Vali-Blob.EFCore;

public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

// Vali-Blob resumable session table
public DbSet<ResumableUploadSessionEntity> ResumableUploadSessions
=> Set<ResumableUploadSessionEntity>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ConfigureVali-Blob(); // registers table, indexes, column types
// ... your other entity configurations ...
}
}

Step 2: Register in DI

// PostgreSQL example
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseNpgsql(builder.Configuration.GetConnectionString("Default")!));

builder.Services
.AddVali-Blob(o => o.DefaultProvider = "aws")
.AddProvider<AWSS3Provider>("aws", o =>
{
o.BucketName = builder.Configuration["AWS:BucketName"]!;
o.Region = builder.Configuration["AWS:Region"]!;
o.AccessKey = builder.Configuration["AWS:AccessKey"]!;
o.SecretKey = builder.Configuration["AWS:SecretKey"]!;
})
.AddEfCoreSessionStore<AppDbContext>();

Step 3: Create and Apply Migration

dotnet ef migrations add AddVali-Blob
dotnet ef database update

This creates the ResumableUploadSessions table alongside your existing application tables.


Database Connection Strings

PostgreSQL

{
"ConnectionStrings": {
"Default": "Host=localhost;Port=5432;Database=myapp;Username=postgres;Password=secret"
}
}
opts.UseNpgsql(connectionString);

MySQL / MariaDB

{
"ConnectionStrings": {
"Default": "Server=localhost;Port=3306;Database=myapp;User=root;Password=secret;"
}
}
opts.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));

SQL Server

{
"ConnectionStrings": {
"Default": "Server=localhost,1433;Database=myapp;User Id=sa;Password=Secret123!;TrustServerCertificate=True"
}
}
opts.UseSqlServer(connectionString);

SQLite (embedded, no server required)

{
"ConnectionStrings": {
"Default": "Data Source=app.db"
}
}
opts.UseSqlite(connectionString);
SQLite for single-instance deployments

SQLite is ideal for on-premise applications, edge deployments, or simple single-instance services that want restart-safe sessions without running a database server or Redis. The database is a single .db file.


Table Schema

ConfigureVali-Blob() creates the following schema:

ColumnTypeConstraintsDescription
Idint (PK)Auto-incrementInternal surrogate key
UploadIdvarchar(128)Unique, Not nullSession identifier
Pathvarchar(2048)Not nullDestination storage path
TotalSizebigintNot nullTotal file size in bytes
UploadedBytesbigintNot null, Default 0Bytes received so far
ContentTypevarchar(256)NullableMIME type
CreatedAtdatetimeNot nullSession creation timestamp (UTC)
UpdatedAtdatetimeNot nullLast chunk timestamp (UTC)
Statusvarchar(32)Not nullPending, InProgress, Complete, Aborted

Indexes:

  • Unique index on UploadId (for fast per-session lookups)
  • Composite index on (Status, UpdatedAt) (for stale session cleanup queries)

Custom table name or schema

modelBuilder.ConfigureVali-Blob(tableName: "upload_sessions", schema: "vali");

Using a Dedicated DbContext

To keep Vali-Blob tables separate from your application schema:

public class ValiDbContext : DbContext
{
public ValiDbContext(DbContextOptions<ValiDbContext> options) : base(options) { }

public DbSet<ResumableUploadSessionEntity> ResumableUploadSessions
=> Set<ResumableUploadSessionEntity>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.ConfigureVali-Blob();
}

// Registration
builder.Services.AddDbContext<ValiDbContext>(opts =>
opts.UseNpgsql(builder.Configuration.GetConnectionString("Default")!));

builder.Services
.AddVali-Blob(...)
.AddEfCoreSessionStore<ValiDbContext>();

Run the migration against the dedicated context:

dotnet ef migrations add AddVali-Blob --context ValiDbContext
dotnet ef database update --context ValiDbContext

Cleaning Up Stale Sessions

Unlike Redis, EF Core does not expire rows automatically. Add a background IHostedService to clean up sessions that were never completed or aborted:

public class StaleSessionCleanupService(
IServiceProvider services,
ILogger<StaleSessionCleanupService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromHours(1), ct);

try
{
await using var scope = services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var cutoff = DateTime.UtcNow.AddHours(-48); // sessions older than 48h

var deleted = await db.ResumableUploadSessions
.Where(s => s.Status == "InProgress" && s.UpdatedAt < cutoff)
.ExecuteDeleteAsync(ct);

if (deleted > 0)
logger.LogInformation("Cleaned up {Count} stale upload sessions.", deleted);
}
catch (Exception ex)
{
logger.LogError(ex, "Error during stale session cleanup.");
}
}
}
}

// Register
builder.Services.AddHostedService<StaleSessionCleanupService>();

Querying Sessions Directly

A key advantage over Redis: sessions are queryable via LINQ or raw SQL:

// All in-progress sessions for a user's files
var activeSessions = await db.ResumableUploadSessions
.Where(s => s.Path.StartsWith($"users/{userId}/") && s.Status == "InProgress")
.OrderByDescending(s => s.CreatedAt)
.ToListAsync();

// Dashboard: total bytes currently in-flight
var inFlightBytes = await db.ResumableUploadSessions
.Where(s => s.Status == "InProgress")
.SumAsync(s => s.UploadedBytes);

// Abandoned sessions (in-progress for > 24h)
var abandoned = await db.ResumableUploadSessions
.Where(s => s.Status == "InProgress" && s.UpdatedAt < DateTime.UtcNow.AddHours(-24))
.ToListAsync();

Comparison: EF Core vs Redis

ConcernEF CoreRedis
Automatic expiryNo (background job required)Yes (sliding TTL)
Queryable via LINQ/SQLYesNo (key-based only)
Requires separate infrastructureNo (uses existing DB)Yes (Redis server)
Per-operation latencyHigher (~1–10 ms)Lower (~0.5–2 ms)
Multi-instance safeYesYes
Best forSQL-centric apps, audit queriesHigh-throughput, stateless pods

Migration Notes

  • dotnet ef migrations add AddVali-Blob scaffolds only the ResumableUploadSessions table — your existing entities are not affected.
  • When upgrading Vali-Blob to a version that changes the schema, a new migration is required.
  • If you use dotnet ef migrations bundle for production deployments, include the Vali-Blob migration in your bundle.