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.
A context is an owned unit, not the database itself
A context holds an operation’s tracked identities and pending changes as well as access to a configured provider. Treating it as a long-lived shared collection obscures those responsibilities. The example therefore uses one context for creation and saving, then a different context for reading the saved row. Both address one deliberately held SQLite connection. That distinction matters for private in-memory SQLite: closing its connection ends that database, while disposing a context borrowing an already open connection does not mean the outer owner intended to discard it. Begin with these two owners before introducing dependency injection or request scopes.
Read the real provider setup and saved observation
Program opens Microsoft.Data.Sqlite.SqliteConnection with Data Source=:memory: and foreign-key enforcement enabled. DbContextOptionsBuilder selects UseSqlite with that real connection, and LabDb derives from the actual EF DbContext. EnsureCreated initializes only this transient fixture. The write scope adds rune and awaits SaveChangesAsync before disposal. A new read scope uses AsNoTracking and SingleAsync, then a Check verifies the stored name before printing saved:rune. A second context’s successful materialization supplies real provider evidence that an ordinary object retained from the first scope could not provide. No EF InMemory provider is present in the project.
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;
await using (var write = new LabDb(options))
{
await write.Database.EnsureCreatedAsync(); write.Items.Add(new Item { Name = "rune" }); await write.SaveChangesAsync();
}
await using (var read = new LabDb(options))
{
var item = await read.Items.AsNoTracking().SingleAsync(); Check(item.Name == "rune", "fresh context sees saved row");
Console.WriteLine("saved:" + item.Name);
}
static void Check(bool condition, string name) { if (!condition) throw new InvalidOperationException("CHECK: " + name); }
Trace the .NET9 ownership ledger honestly
The portable ledger increments Active when Open creates a disposable lease and decrements it only once when that lease is disposed. Two sequential using scopes never overlap, so Active ends at zero and Peak is one. This is normal BCL object-lifetime bookkeeping. The class does not inherit DbContext, issue SQL, hold a connection or enforce cross-thread usage. Its usefulness is the explicit ownership invariant: an operation has a beginning and an end even when several operations share some outer resource. Keep the small model’s output separate from the external project’s saved-row assertion.
Await sequential work and identify concurrency ownership
DbContext is not thread-safe, and running two outstanding operations on one instance is not made safe by using async syntax. Await an operation before beginning another on that same context. Separate contexts are necessary for independent concurrent units, but this demonstration also shares one connection sequentially; it is not a recommended parallel-connection strategy. A singleton service retaining a context can accumulate stale tracking state even without a visible thread race. Some EF InvalidOperationExceptions indicate programming errors that leave a context unsuitable for continued use. Fix the ownership violation rather than wrapping it in an automatic retry loop.
Practice: distinguish context disposal from connection closure
Run the downloaded project first and preserve its assertion. In a separate disposable variation, close the in-memory connection after the write scope and attempt to read through a newly opened private connection. Predict why the earlier schema and row are unavailable instead of expecting a disk database to have been saved. Restore the baseline before testing a file-backed SQLite database in an owned temporary directory. Those are different storage lifetimes, and the successful in-memory check is not persistence-across-restart evidence. Label the external project and its actual provider when recording the result.
- Hint: the outer connection owns the private in-memory database lifetime.
- Hint: use fresh provider queries rather than retained entity references for verification.
Practice: make a lease invariant observable
Add nested leases to the portable ledger and predict a peak of two while retaining a final active count of zero. Dispose one lease twice and verify that the owner is decremented once. Then introduce an exception inside a using scope and check that disposal still restores the count. These are independent ownership checks, not a reason to label the class a context simulator. If a reusable ledger must support multiple threads, it needs an explicitly designed synchronization contract; do not infer thread safety from this sequential demonstration or from an integer counter looking correct once.
- Hint: idempotent disposal protects the owner from repeated decrements.
- Hint: logical ownership and database thread safety remain different contracts.
Preserve provider and framework boundaries in a handoff
The external result depends on net10.0, the pinned EF Core10 SQLite package, a native relational connection and two short awaited context scopes. SharpRune’s practice runs on .NET9 and proves only its BCL ledger outputs. Neither a single saved row nor a clean lease count establishes production connection pooling, authentication, migration rollout or another provider’s behavior. A useful review names the context owner, connection owner, schema strategy and materialization point. Keeping those facts explicit makes later tracking, transactions and factory lessons easier to assess without letting a framework type stand in for an unexplained lifetime policy.
.NET10 / EF Core10 / real SQLite — One context for one owned unit of work
Set up the SDK project
- 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.
- 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.
- 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.
- 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.csproj
Download 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.cs
Download 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;
await using (var write = new LabDb(options))
{
await write.Database.EnsureCreatedAsync(); write.Items.Add(new Item { Name = "rune" }); await write.SaveChangesAsync();
}
await using (var read = new LabDb(options))
{
var item = await read.Items.AsNoTracking().SingleAsync(); Check(item.Name == "rune", "fresh context sees saved row");
Console.WriteLine("saved:" + item.Name);
}
static void Check(bool condition, string name) { if (!condition) throw new InvalidOperationException("CHECK: " + name); }
Model.cs
Download file ↗using Microsoft.EntityFrameworkCore;
public sealed class LabDb : DbContext
{
public LabDb(DbContextOptions<LabDb> options) : base(options) { }
public DbSet<Item> Items => Set<Item>();
}
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: saved:rune 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 ledger = new LeaseLedger();
using (ledger.Open()) { }
using (ledger.Open()) { }
Console.WriteLine($"active:{ledger.Active}"); Console.WriteLine($"peak:{ledger.Peak}");
sealed class LeaseLedger
{
public int Active { get; private set; } public int Peak { get; private set; }
public IDisposable Open() { Active++; Peak = Math.Max(Peak, Active); return new Lease(this); }
private sealed class Lease : IDisposable { private LeaseLedger? owner; public Lease(LeaseLedger owner) { this.owner = owner; } public void Dispose() { if (owner is null) return; owner.Active--; owner = null; } }
}
C# practice output
active:0 peak:1
A context lifetime and an externally supplied connection lifetime are separate contracts.