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.

Intermediate105–125 minutesreproducible lab + configuration/logging/secrets labEF Core 10.0.11 · .NET 10SQLite 3.46.1+ baselineLast reviewed: August 2026

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.

01

Organize the course solution so project, tool, model, migration, configuration, and disposable database state have clear ownership.

02

Load a connection string from configuration while keeping credentials and other secrets out of committed source/configuration files.

03

Configure structured EF Core logging that exposes command/category evidence without enabling sensitive-data logging by default.

04

Seed deterministic ServiceHub data and provide a migration-aware reset workflow that later failure-injection labs can repeat safely.

05

Record .NET, EF Core, provider, and database versions so observations can be reproduced and compared across patches.

Course lab rule

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.

text · course repository layout
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.

text · add the hosting/configuration layer
dotnet add src/ServiceHub.EfLab package Microsoft.Extensions.Hosting --version 10.0.11
json · appsettings.json — safe defaults only
{  "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

csharp · Program.cs — configuration, DI, logging, and DbContext registration
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.

Sensitive logging boundary

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.

text · cross-platform configuration override
# 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:

text · development-only Secret Manager example
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.

csharp · Data/ServiceHubSeed.cs
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.

csharp · Data/ServiceHubDiagnostics.cs
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();        }    }}
text · record build/tool versions beside runtime evidence
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.

text · cross-platform reset workflow
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.

text · representative safe command log shape
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.

json · wrong: credential embedded in committed configuration
{  "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.

Security judgment

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.

text · Chapter 01 reproducibility acceptance sequence
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 __EFMigrationsHistory records InitialCreate.
  • 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

  1. Why is a deterministic seed preferable to uncontrolled random data for mechanism-focused lessons?
  2. Why does storing a connection string in configuration not automatically mean it is safe to commit?
  3. What is the purpose of the Microsoft.EntityFrameworkCore.Database.Command logging category?
  4. Why should EnableSensitiveDataLogging remain off by default?
  5. Why does a migration-driven reset teach a better lifecycle than repeated EnsureCreated/EnsureDeleted?
  6. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n