EF Core 10 Relational Data · Lesson 15

Factory leases and pooling state boundaries

Use a real pooled context factory without pretending custom fields reset themselves.

External net10.0 console; SDK10.0.403; Microsoft.EntityFrameworkCore.Sqlite 10.0.14; native SQLite relational provider. Portable practice is a separate .NET9 BCL-only program.

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

Pooling changes reuse, not ownership or thread safety

A factory-created context is owned by the operation that requested it and must be disposed. With a pooled factory, disposal can return the instance for a later lease after EF resets its internal state. That reuse is not permission for two callers to share the same instance concurrently, and it is different from database-connection pooling. The lesson chooses a one-entry pool and strictly sequential scopes to make one reuse observation controlled. It does not introduce tenant-specific configuration into pooled state or claim that pooling is necessary for this tiny workload. Measure a concrete application before adding that optimization boundary.

Observe an EF reset and an application field that survives

The first genuine pooled context creates and saves rune, verifies one tracked entity and sets CustomNote to lease-one. Its InstanceId is recorded before disposal. The second lease in this controlled one-entry fixture verifies reuse, an empty EF ChangeTracker and the retained custom field. It also queries the existing SQLite row, then explicitly clears CustomNote. These assertions demonstrate why internal reset is not a blanket application-state reset. The outer connection remains owned and open across both sequential leases. A successful test does not turn that shared connection into a parallel access strategy.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

await using var connection = new SqliteConnection("Data Source=:memory:;Foreign Keys=True");
await connection.OpenAsync();
var options = new DbContextOptionsBuilder<LabDb>().UseSqlite(connection).Options;
var factory = new Microsoft.EntityFrameworkCore.Infrastructure.PooledDbContextFactory<LabDb>(options, poolSize: 1);
Guid firstInstance;
await using (var first = await factory.CreateDbContextAsync())
{
    await first.Database.EnsureCreatedAsync(); first.Items.Add(new Item { Name = "rune" }); await first.SaveChangesAsync();
    Check(first.ChangeTracker.Entries().Count() == 1, "first lease tracks entity"); first.CustomNote = "lease-one"; firstInstance = first.ContextId.InstanceId;
}
await using (var second = await factory.CreateDbContextAsync())
{
    Check(second.ContextId.InstanceId == firstInstance, "controlled sequential pool reuse");
    Check(!second.ChangeTracker.Entries().Any(), "EF tracker reset"); Check(second.CustomNote == "lease-one", "custom field persists");
    Check(await second.Items.CountAsync() == 1, "SQLite still shared through open connection");
    Console.WriteLine("tracked-after-return:0"); Console.WriteLine("custom-field:" + second.CustomNote); second.CustomNote = "";
}

static void Check(bool condition, string name) { if (!condition) throw new InvalidOperationException("CHECK: " + name); }

Supply operation configuration as an independent value

The .NET9 companion creates two Operation records containing A and B and prints each intended tenant. It demonstrates that distinct operations can receive distinct immutable configuration values without relying on mutable shared state. There is no context pool, EF service reset, connection or tenant authorization system in that program. The small result can specify a requirement for an adapter, but the genuine pooled test is needed to expose what EF resets and what a custom field retains. Avoid naming a list of reusable objects a DbContext pool and treating its expected behavior as evidence about the actual factory.

Custom fields and driver state need a reviewed lease policy

EF resets its own context state when returning a pooled instance, but it does not know every application property or external driver operation. A per-request tenant, user identifier or open transaction manually stored on a reusable object needs deliberate setup and cleanup. OnConfiguring is not a per-request hook that can be assumed to rerun for each lease. The fixture’s harmless CustomNote makes persistence visible without leaking real tenant data. Production code should not retain a scoped dependency accidentally inside a long-lived factory configuration, and a context should never escape beyond the operation that currently owns its lease.

Practice: add explicit per-lease state initialization

Replace the demonstration note with an operation label supplied at acquisition and cleared before return, keeping that policy in one adapter rather than scattered callers. Verify that a second lease receives its own label and that no tracked entity survives. Keep the original intentionally leaking case as a separate educational test instead of claiming that EF automatically performs the new cleanup. If the application uses tenant filtering, design its factory and model parameterization with stronger dedicated tests before combining it with pooling. One sequential context-reuse observation is not enough to certify concurrent tenant isolation.

  • Hint: lease acquisition is where operation-specific state must be established deliberately.
  • Hint: disposal returns a pooled instance; it does not reset unknown application fields.

Practice: make operation configuration immutable and validated

Extend the pure Operation record with a required nonblank tenant and a bounded page size, using a factory that returns either valid configuration or a named failure. Test A then B, repeated creation and invalid input without any global current-tenant variable. The model cannot demonstrate factory pooling, but it can establish what each operation is entitled to receive. Keep identity and authority separate: validating a string’s shape does not authorize a user to choose it. When a real adapter adopts this configuration, verify that its lease initialization and cleanup preserve those rules on the actual pooled implementation.

  • Hint: immutable configuration simplifies ownership but does not itself grant authorization.
  • Hint: do not let one operation mutate configuration retained by another.

Review the optimization with its added state burden

The actual provider run observes a tracker reset and retained application note in a controlled EF10 pooled factory. It does not benchmark throughput or certify another provider’s connection behavior. The portable run demonstrates only separate supplied operation values. A useful review names context lease ownership, pool configuration, operation-specific state, connection ownership and measurements that justify reuse. Preserve those facts instead of treating pooling as a default performance upgrade with no semantic consequences. A simpler unpooled factory can be the right choice when its ownership is clearer and no representative measurement establishes a meaningful benefit from pooling.

External net10.0 console; SDK10.0.403; Microsoft.EntityFrameworkCore.Sqlite 10.0.14; native SQLite relational provider. Portable practice is a separate .NET9 BCL-only program.

.NET10 / EF Core10 / real SQLite — Factory leases and pooling state boundaries

Set up the SDK project

  1. Use an external .NET10 console environment. This downloadable project targets net10.0 and pins Microsoft.EntityFrameworkCore.Sqlite 10.0.14; it is not the SharpRune .NET9 browser practice.
  2. Restore this project with: dotnet restore EfLab.csproj --source https://api.nuget.org/v3/index.json. Use an installed compatible .NET10 SDK; validation used the isolated official SDK10.0.403.
  3. Run: dotnet run --project EfLab.csproj --no-restore. The source opens a real SQLite :memory: connection and keeps that connection alive across its sequential context scopes. No EF InMemory provider or fake ORM is used.
  4. Read the embedded Check assertions and the expected output below. EnsureCreated is used only for disposable classroom schema unless this is the dedicated migration lesson. A passing SQLite run is not evidence for another database provider.

Download complete SDK project ↗

Project files

EfLab.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.14" />
  </ItemGroup>
</Project>
Program.csDownload file ↗
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

await using var connection = new SqliteConnection("Data Source=:memory:;Foreign Keys=True");
await connection.OpenAsync();
var options = new DbContextOptionsBuilder<LabDb>().UseSqlite(connection).Options;
var factory = new Microsoft.EntityFrameworkCore.Infrastructure.PooledDbContextFactory<LabDb>(options, poolSize: 1);
Guid firstInstance;
await using (var first = await factory.CreateDbContextAsync())
{
    await first.Database.EnsureCreatedAsync(); first.Items.Add(new Item { Name = "rune" }); await first.SaveChangesAsync();
    Check(first.ChangeTracker.Entries().Count() == 1, "first lease tracks entity"); first.CustomNote = "lease-one"; firstInstance = first.ContextId.InstanceId;
}
await using (var second = await factory.CreateDbContextAsync())
{
    Check(second.ContextId.InstanceId == firstInstance, "controlled sequential pool reuse");
    Check(!second.ChangeTracker.Entries().Any(), "EF tracker reset"); Check(second.CustomNote == "lease-one", "custom field persists");
    Check(await second.Items.CountAsync() == 1, "SQLite still shared through open connection");
    Console.WriteLine("tracked-after-return:0"); Console.WriteLine("custom-field:" + second.CustomNote); second.CustomNote = "";
}

static void Check(bool condition, string name) { if (!condition) throw new InvalidOperationException("CHECK: " + name); }
Model.csDownload file ↗
using Microsoft.EntityFrameworkCore;
public sealed class LabDb : DbContext
{
    public LabDb(DbContextOptions<LabDb> options) : base(options) { }
    public DbSet<Item> Items => Set<Item>();
    public string CustomNote { get; set; } = "";
}
public sealed class Item
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
}

Expected result

The external program must complete its Check assertions and print: tracked-after-return:0 custom-field:lease-one It uses actual SQLite materialization and saving; no external service or production database is contacted.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

var first = new Operation("A"); var second = new Operation("B");
Console.WriteLine(first.Tenant); Console.WriteLine(second.Tenant);
record Operation(string Tenant);
Open C# practice

C# practice output

A
B
Try this
EF resets its internal tracking state; custom fields and external driver state need their own lease policy.

Further reading