Chapter 01 · EF Core Foundations, .NET Integration, Providers, and the First Lab
Build a Reproducible Course Lab with Logging, Sample Data, Configuration, and Safe Secrets
Turn the ServiceHub prototype into a reproducible EF Core course environment with stable project structure, external configuration, safe secret handling, structured logging, deterministic seed/reset workflows, and recorded version evidence.
Learning outcomes
A lab is useful only if another learner—or CI—can reproduce it without guessing which SDK, packages, database state, credentials, or seed data were present. The final Chapter 01 lesson converts the ServiceHub prototype into a stable course environment that later chapters can safely evolve.
Organize the course solution so project, tool, model, migration, configuration, and disposable database state have clear ownership.
Load a connection string from configuration while keeping credentials and other secrets out of committed source/configuration files.
Configure structured EF Core logging that exposes command/category evidence without enabling sensitive-data logging by default.
Seed deterministic ServiceHub data and provide a migration-aware reset workflow that later failure-injection labs can repeat safely.
Record .NET, EF Core, provider, and database versions so observations can be reproduced and compared across patches.
Every destructive experiment in later chapters targets a disposable ServiceHub lab database. Never point reset, migration rollback, ExecuteDelete, concurrency injection, or security exercises at an unrelated database. A reproducible reset path is part of the experiment, not cleanup trivia.
1. Establish a stable solution layout before the model grows
Keep runtime source, EF tooling metadata, and generated database artifacts distinguishable. The exact folder structure is not sacred, but silent renaming between chapters destroys reproducibility. This course uses one solution and one primary context name until a later architectural lesson has a reason to introduce another boundary.
ServiceHubEfCourse/├─ global.json├─ .config/│ └─ dotnet-tools.json├─ ServiceHubEfCourse.slnx├─ .gitignore├─ src/│ └─ ServiceHub.EfLab/│ ├─ ServiceHub.EfLab.csproj│ ├─ Program.cs│ ├─ appsettings.json│ ├─ Domain/│ │ └─ WorkOrder.cs│ ├─ Data/│ │ ├─ ServiceHubContext.cs│ │ ├─ ServiceHubContextFactory.cs│ │ ├─ ServiceHubSeed.cs│ │ ├─ ServiceHubDiagnostics.cs│ │ └─ Migrations/└─ README.md
The SQLite database file is generated runtime state and should not become a source-of-truth artifact. Migration C# files and the model snapshot are source artifacts and should be reviewed/committed with model changes.
2. Add configuration and hosting without hiding the data-access boundary
The .NET generic host provides configuration, dependency
injection (DI), logging, and environment concepts without
requiring an ASP.NET web project. Add the current stable hosting
package, then let AddDbContext construct
short-lived contexts from external configuration.
dotnet add src/ServiceHub.EfLab package Microsoft.Extensions.Hosting --version 10.0.11
{ "ConnectionStrings": { "ServiceHub": "Data Source=servicehub-lab.db" }, "EfDiagnostics": { "SensitiveData": false }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.EntityFrameworkCore": "Warning", "Microsoft.EntityFrameworkCore.Database.Command": "Information" } }}
A local SQLite path is not a credential, so it is reasonable as a committed default for this learning lab. When the course later uses authenticated servers, passwords/tokens/certificates remain outside committed JSON. Configuration and secrets are related but not synonymous: ordinary non-sensitive settings still belong in configuration.
3. Resolve configuration once and inject DbContext options
using Microsoft.EntityFrameworkCore;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;using Microsoft.Extensions.Logging;using ServiceHub.EfLab.Data;var builder = Host.CreateApplicationBuilder(args);var connectionString = builder.Configuration.GetConnectionString("ServiceHub") ?? throw new InvalidOperationException( "Connection string 'ServiceHub' was not configured.");builder.Services.AddDbContext<ServiceHubContext>(options =>{ options.UseSqlite(connectionString); options.EnableDetailedErrors(); if (builder.Environment.IsDevelopment() && builder.Configuration.GetValue<bool>("EfDiagnostics:SensitiveData")) { options.EnableSensitiveDataLogging(); }});using var host = builder.Build();await using var scope = host.Services.CreateAsyncScope();var db = scope.ServiceProvider.GetRequiredService<ServiceHubContext>();var logger = scope.ServiceProvider .GetRequiredService<ILoggerFactory>() .CreateLogger("ServiceHub.CourseLab");logger.LogInformation("EF provider: {Provider}", db.Database.ProviderName);await ServiceHubSeed.SeedAsync(db);await ServiceHubDiagnostics.PrintVersionsAsync(db, logger);
AddDbContext registers a context with a scoped
lifetime by default. In this console app we create one explicit
scope to model one unit of work. Chapter 02 explains why
request-based web applications, background services,
Blazor-style lifetimes, factories, and context pooling require
different construction decisions.
EnableSensitiveDataLogging can place key values, query parameter values, or other confidential data into diagnostics. It is off by default for a reason. This course enables it only through an explicit development-only switch when a lesson specifically needs it; production examples leave it disabled.
4. Environment variables and user-secrets override committed defaults
.NET configuration uses hierarchical keys. For portable environment-variable overrides, use double underscores. This is valuable for CI and containers, but environment variables are not encrypted secret vaults; host/process access can expose them. Development Secret Manager is also a convenience store, not a production vault.
# Bash / zshexport ConnectionStrings__ServiceHub='Data Source=/tmp/servicehub-lab.db'dotnet run --project src/ServiceHub.EfLab# PowerShell$env:ConnectionStrings__ServiceHub = 'Data Source=C:\Temp\servicehub-lab.db'dotnet run --project src/ServiceHub.EfLab
For an authenticated database used later in development, initialize user secrets in the project and set the connection string there instead of committing credentials:
dotnet user-secrets init --project src/ServiceHub.EfLabdotnet user-secrets set "ConnectionStrings:ServiceHubServer" "Server=localhost;Database=ServiceHubLab;User Id=servicehub_app;Password=<local-secret>" --project src/ServiceHub.EfLab
The placeholder is intentionally not a real password. Do not paste production credentials into shell history for convenience; production secret delivery should use the platform's secure authentication/secret mechanism and least-privilege identities.
5. Seed deterministic data, not “whatever happened on my machine”
Deterministic sample records let later lessons reason about query counts, relationship shapes, migrations, and concurrency without hidden state. Use stable business values and timestamps. Avoid relying on random data as the only verification dataset; randomness makes failures harder to reproduce unless the seed is controlled and recorded.
using Microsoft.EntityFrameworkCore;using ServiceHub.EfLab.Domain;namespace ServiceHub.EfLab.Data;public static class ServiceHubSeed{ public static async Task SeedAsync( ServiceHubContext db, CancellationToken cancellationToken = default) { if (await db.WorkOrders.AnyAsync(cancellationToken)) return; db.WorkOrders.AddRange( new WorkOrder { CustomerName = "Northwind Workshop", Summary = "Replace vibration sensor on compressor C-14", Priority = WorkOrderPriority.High, OpenedUtc = DateTimeOffset.Parse("2026-08-20T08:00:00Z") }, new WorkOrder { CustomerName = "Contoso Cold Storage", Summary = "Investigate freezer temperature alarm", Priority = WorkOrderPriority.High, OpenedUtc = DateTimeOffset.Parse("2026-08-21T09:30:00Z") }, new WorkOrder { CustomerName = "Fabrikam Packaging", Summary = "Schedule preventive conveyor inspection", Priority = WorkOrderPriority.Normal, OpenedUtc = DateTimeOffset.Parse("2026-08-22T13:15:00Z") }); await db.SaveChangesAsync(cancellationToken); }}
The AnyAsync guard is adequate for this
single-process learning seed. It is not a general distributed
seeding protocol. Production deployments need explicit
ownership, idempotency, concurrency behavior, and auditability
for reference/bootstrap data.
6. Record the environment that produced an observation
When a learner reports “EF generated different SQL,” the first
debugging question should include versions. Print enough
evidence to distinguish SDK/runtime, EF assembly, provider
identity, and database library. The .NET SDK is discovered
outside the running process with dotnet --version;
runtime/provider/database evidence comes from the app.
using Microsoft.EntityFrameworkCore;using Microsoft.Extensions.Logging;using System.Data.Common;namespace ServiceHub.EfLab.Data;public static class ServiceHubDiagnostics{ public static async Task PrintVersionsAsync( ServiceHubContext db, ILogger logger, CancellationToken cancellationToken = default) { logger.LogInformation("Runtime: {Runtime}", Environment.Version); logger.LogInformation( "EF Core assembly: {EfVersion}", typeof(DbContext).Assembly.GetName().Version?.ToString()); logger.LogInformation("Provider: {Provider}", db.Database.ProviderName); DbConnection connection = db.Database.GetDbConnection(); await connection.OpenAsync(cancellationToken); try { await using var command = connection.CreateCommand(); command.CommandText = "SELECT sqlite_version();"; var sqliteVersion = await command.ExecuteScalarAsync(cancellationToken); logger.LogInformation("SQLite library: {Version}", sqliteVersion); } finally { await connection.CloseAsync(); } }}
dotnet --versiondotnet --list-runtimesdotnet tool run dotnet-ef -- --versiondotnet list src/ServiceHub.EfLab package
For the August 27, 2026 chapter baseline, the stable course packages/tool are EF Core/dotnet-ef 10.0.11, .NET runtime 10.0.11, and the latest .NET 10 SDK feature band is 10.0.400. These are generation-time checkpoints, not values to memorize forever. Patch before later execution and record what actually ran.
7. Reset through the migrations lifecycle
A reset should return the lab to a known schema and seed without
training learners to use
EnsureDeleted/EnsureCreated as production migration
management. EF tooling can drop the disposable SQLite database,
reapply reviewed migrations, and run the deterministic seed.
dotnet ef database drop --force --project src/ServiceHub.EfLabdotnet ef database update --project src/ServiceHub.EfLabdotnet run --project src/ServiceHub.EfLab
Before running the first command, inspect the effective connection string and confirm it points at the disposable lab. For server providers later in the course, the reset workflow must include a database/schema name allow-list and privileges appropriate to the experiment. “It is only a script” is not a safety boundary.
8. Structured logging means category + level + fields, not console noise
EF emits logs through Microsoft.Extensions.Logging.
The
Microsoft.EntityFrameworkCore.Database.Command
category is useful when teaching generated commands; other
categories expose model validation, change tracking, connection,
transaction, migration, and update events. Tune categories for
the question you are answering instead of enabling everything
forever.
info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (...) [Parameters=[@__priority_0='?' (DbType = Int32)], ...] SELECT "w"."Id", "w"."CustomerName", "w"."Summary" FROM "WorkOrders" AS "w" WHERE "w"."Priority" = @__priority_0
The placeholder demonstrates the safe expectation: parameter metadata may be visible while sensitive values remain hidden unless sensitive-data logging is explicitly enabled. Do not write tests that assert exact logger formatting or event text across patches.
9. Deliberately wrong approach: commit credentials and turn on sensitive logging globally
Imagine replacing the SQLite connection with a server connection
string containing a password in appsettings.json,
committing it, and calling
EnableSensitiveDataLogging() unconditionally. The
app may work perfectly while creating two independent disclosure
channels: source history/configuration and logs/traces.
{ "ConnectionStrings": { "ServiceHub": "Server=db.example;Database=ServiceHub;User Id=app;Password=RealPasswordHere" }}
Repair: revoke/rotate any exposed credential, remove it from active configuration, use a development secret store or environment-specific secure identity/secret delivery, keep sensitive EF logging disabled by default, and review existing logs for exposed values. Deleting one line from the latest Git commit does not guarantee a leaked secret is gone from history or downstream copies.
Parameterization protects SQL values from injection when the API supports parameters; it does not protect a password you intentionally log or commit. Configuration security, SQL injection resistance, least-privilege database authorization, tenant isolation, and logging hygiene are separate controls.
10. Hands-on lab: rebuild Chapter 01 from a clean checkout
Use this sequence as the Chapter 01 acceptance test. It intentionally starts from source/tool manifests and a disposable database rather than from a pre-existing local file.
dotnet --versiondotnet tool restoredotnet restoredotnet builddotnet ef migrations list --project src/ServiceHub.EfLabdotnet ef database update --project src/ServiceHub.EfLabdotnet run --project src/ServiceHub.EfLabdotnet list src/ServiceHub.EfLab packagedotnet tool run dotnet-ef -- --version
Verification checklist
-
The repository resolves the intended .NET 10 SDK policy and
restores local
dotnet-ef. - Microsoft EF runtime/provider/Design packages stay on one supported stable 10.0.x patch line.
-
The database is created by migrations and
__EFMigrationsHistoryrecordsInitialCreate. - The seed always creates the same three initial work orders after a clean reset.
- Structured EF command logs are visible, but sensitive-data logging is false by default.
- The app prints runtime, EF assembly, provider, and SQLite library identity; CLI commands record SDK/tool/package versions.
- No passwords/tokens/production hosts are committed to the course files.
- The reset workflow is explicitly scoped to the disposable ServiceHub lab.
Check your understanding
- Why is a deterministic seed preferable to uncontrolled random data for mechanism-focused lessons?
- Why does storing a connection string in configuration not automatically mean it is safe to commit?
- What is the purpose of the Microsoft.EntityFrameworkCore.Database.Command logging category?
- Why should EnableSensitiveDataLogging remain off by default?
- Why does a migration-driven reset teach a better lifecycle than repeated EnsureCreated/EnsureDeleted?
- Which version dimensions should you record when comparing generated SQL across machines?
Review the answers
Deterministic rows make queries, failures, and expected output reproducible; random datasets can hide or create behavior unless their seed/distribution is controlled.
Connection strings can contain credentials or endpoints that should be protected. Configuration is a mechanism for settings, not a guarantee that a value is non-sensitive.
It surfaces EF/provider database-command events so generated commands, timing metadata, and parameters can be correlated with application behavior.
It can expose confidential entity/key/parameter values in logs and exceptions; enable it only deliberately in safe development diagnostics.
Migrations preserve versioned schema intent, snapshots/history, reviewability, and an evolution path that matches how later schema changes are managed.
Record the .NET SDK/runtime, EF Core package/assembly, dotnet-ef, provider/ADO.NET driver where relevant, database engine/library version, and important environment assumptions.
11. Chapter 01 production judgment and bridge to Chapter 02
The completed lab has a precise boundary: one short-lived
ServiceHubContext, one declared provider, a
migration-owned disposable schema, deterministic data, external
configuration, safe logging defaults, and version evidence. That
is enough infrastructure to learn mechanisms without pretending
the ORM owns the database or the deployment environment.
Before using these patterns in production, decide who owns
migrations, which database identity performs DDL versus runtime
DML, how secrets/managed identity are delivered, how logs are
protected, which provider/database versions are supported, how
rollback and backup/recovery work, and how context lifetime maps
to application units of work. Chapter 02 begins there by
examining DbContext lifecycle, dependency
injection, factories, async boundaries, and pooling—especially
the state-leak and thread-safety failures that appear when a
context is treated like a global singleton.
Authoritative references
- Connection strings in EF Core — external configuration and warning not to place secrets in configuration files
- Safe storage of app secrets in development — Secret Manager, environment-variable caveats, and development secret guidance
- Using Microsoft.Extensions.Logging with EF Core — structured logging integration and sensitive-data diagnostics
- Simple logging — LogTo, detailed errors, categories, and sensitive data
- DbContext configuration — AddDbContext, options, and lifetime foundations
- Applying migrations — deployment choices and migration application tradeoffs
- Microsoft.Extensions.Hosting 10.0.11 — stable hosting/configuration/logging package used by the console lab
- .NET 10 downloads — current runtime and SDK feature-band checkpoint