Operate
Two connection planes — EF Core and Tracon
Tracon does not use Entity Framework Core internally, and does not ship an EF
Core package. Its store layer talks to the database through raw ADO.NET
(DbDataSource, DbCommand) on purpose — that keeps Tracon.PostgreSql AOT
compatible and keeps the store contract at the same abstraction level EF Core
itself sits at, so there is nothing an EF integration would add. If your
application already uses EF Core for its own schema, this page is about the
connection plane the two share, not about making Tracon use EF Core.
One data source, two consumers
Section titled “One data source, two consumers”Give both sides the exact same NpgsqlDataSource instance and they share one real
connection pool — building two data sources from an identical connection string
does not do this; see
Two data planes, one connection pool or two
for the measurement:
var dataSource = new NpgsqlDataSourceBuilder(connectionString) // Token refresh, client certificates, custom type mappings: anything not // expressible in a connection string now applies to Tracon's traffic too. .Build();
builder.Services.AddDbContext<YourDbContext>(o => o.UseNpgsql(dataSource));
builder.AddTracon() .UsePostgreSql(o => o.DataSource = dataSource);TraconPostgreSqlOptions.ConnectionString is not required when DataSource is
set — giving both at once is a startup error, not a silent preference. Tracon
never disposes an instance it did not build: ownership stays with whoever created
it (your host, in this example), and it keeps working after Tracon’s own
ServiceProvider shuts down.
The same seam exists on TraconSqlServerOptions.DataSource and
TraconSqliteOptions.DataSource, for symmetry. Neither
Microsoft.Data.SqlClient nor Microsoft.Data.Sqlite ships a DbDataSource
implementation of its own today (measured against Microsoft.Data.SqlClient 7.0.2), so there is no equivalent “give EF Core’s data source to Tracon” call
for those two providers yet — the field is ready for the day the ecosystem catches
up, or for a DbDataSource adapter you write yourself.
There is no shared transaction
Section titled “There is no shared transaction”YourDbContext.SaveChangesAsync() and a Tracon run write through two
independent connections, even when they share one pool. A run’s recording never
becomes atomic with your own schema’s writes, and this is deliberate: Tracon’s
stores are singletons that outlive any one request, and “observability must never
block the run” is a standing rule — a run’s own stores never wait on your
transaction to commit, and yours never waits on Tracon’s.
Reference a run by RunId, not by copying its data
Section titled “Reference a run by RunId, not by copying its data”Point your own entity at the run instead of duplicating what Tracon already recorded:
public sealed class SupportTicket{ public Guid Id { get; set; } public required string RunId { get; set; } // reference, not a copy // ... your own domain fields}Read the run’s own data back through IRunStore/GET /api/runs/{id} when you need
it; do not copy fields across at write time — the two writes are not atomic (above),
so a copy can drift the moment either side fails after the other commits. If your
own write can retry, protect it with Idempotency-Key
(see Make supported HTTP submissions idempotent)
the same way any other retryable write in front of Tracon would be.
Two migration steps, one order that matters less than you think
Section titled “Two migration steps, one order that matters less than you think”Your own dotnet ef database update and Tracon’s tracon migrate apply to
two schemas with no foreign key between them (tracon versus yours), so the
order they run in does not matter. What does matter is running both with
automatic migration off, so exactly one mechanism touches the database at deploy
time:
dotnet ef database updatetracon migrate --provider postgres --connection "$TRACON_CONNECTION"See Choose a migration strategy
for AutoApplyMigrations and the rest of the deployment-pipeline shape.
Reading through a keyless entity instead
Section titled “Reading through a keyless entity instead”The two connection planes above are about your own writes. When you only need to
read run data next to your own entity — a cost column in your own report, a
status filter in your own dashboard — a third, narrower option exists: runs_v1,
a versioned, read-only SQL view opt in with EnableReadViews. Map it as a keyless
entity and query it with LINQ, without giving Tracon’s internal runs table
shape a dependency on your code:
builder.AddTracon() .UsePostgreSql(o => { o.ConnectionString = connectionString; o.EnableReadViews = true; });modelBuilder.Entity<TraconRun>() .HasNoKey() .ToView("runs_v1", "tracon");See Read contract views for the full column list, the
total_cost null-preserving rule, and why the view is not a tenant boundary.
What you do not get
Section titled “What you do not get”- No EF Core global query filter reaches Tracon’s rows — its own tenant
isolation runs through its own
tenant_idcolumn andITenantContext, a separate mechanism from EF Core’s. dotnet ef migrationsnever sees Tracon’s schema. It is not an EF model; there is noDbSetto scaffold it from, and none should be added.- No
DbContextowns a Tracon table. Thetraconschema is written only by Tracon’s own store layer, from either connection plane.
Read next
Section titled “Read next”- Read contract views — the full
runs_v1column list and compatibility rule - Embedding into a host application — the two data planes and connection pool sharing measurement
- Production deployment — the migration strategy this page’s CI snippet belongs to