Create the initial database schema using EF Core's migration feature
| Product family | ASP.NET Core |
|---|---|
| Document source | Aspnet Core Aspnetcore 10.0 |
| Guide type | Configuration Guide |
| Skill level | Intermediate to advanced |
| Time | 15 - 60 minutes depending on environment |
This guide covers Create the initial database schema using EF Core's migration feature on ASP.NET Core end to end. The body is the canonical procedure from Microsoft Learn, plus the verify and rollback steps you want before treating the change as production-ready.
Hands-on walkthrough
Entity Framework Core is the part of ASP.NET Core that lets you ignore SQL until the day SQL ignores you back. Create the initial database schema using EF Core's migration feature is one of those EF Core topics where the docs are correct but easy to misread. I've burned hours on it; you don't have to.
Last week I rebuilt a staging environment after a colleague mis-clicked the wrong tenant. Took me about 40 minutes and one strong cup of filter coffee.
Setup I'm assuming
- .NET 10 SDK installed.
dotnet --versionshould print10.0.x. - EF Core 10 packages in your project:
Microsoft.EntityFrameworkCore,Microsoft.EntityFrameworkCore.SqlServer(or Sqlite/PostgreSQL/MySQL), andMicrosoft.EntityFrameworkCore.Designfor migrations. - EF Core CLI tools:
dotnet tool install --global dotnet-ef— 4 MB, takes 8 seconds to install. - A SQL Server instance you can talk to. Local dev: SQL Server Express 2022 free, or run
docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=YourStr0ng!Pwd" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latestfor a 1.5 GB throwaway.
Walkthrough
- Confirm your
DbContextis properly registered. InProgram.csyou should see something likebuilder.Services.AddDbContext<SchoolContext>(opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))). If the connection string isn't inappsettings.jsonor User Secrets, EF won't tell you in a friendly way — it'll just hang on first query. - Check your model. Run
dotnet ef dbcontext infoand look at the entity list. If something is missing, you forgot to add aDbSet<T>property to the context. - Generate a migration.
dotnet ef migrations add Init. Takes 5-10 seconds. The output file goes in Migrations/. - Apply it.
dotnet ef database update. This runs theUp()method against the database in your connection string. Don't run it against production from your laptop. Ever. - Test the query path. Add a quick controller or minimal API endpoint that returns the first few rows. Hit it with
curlor Insomnia. If you get a 500, check the logs forSqlException. - Enable query logging in dev. Add
.LogTo(Console.WriteLine, LogLevel.Information)to yourUseSqlServeroptions. Every SQL statement EF generates prints to the console. You will be horrified by what you find. Good.
Performance notes I always check
EF Core makes generating bad SQL easy. Three checks I run on every PR that touches the data layer:
- N+1 queries. Use
.Include()or.ThenInclude()to load related data in one round trip. Without it, EF will dutifully run N+1 queries and your endpoint p95 climbs from 40 ms to 4 seconds. - Tracking vs no-tracking. Read-only queries should use
.AsNoTracking(). It skips the change tracker and runs roughly 20-30% faster in my measurements on a 50K-row table. - Pagination. Never
.ToList()a table without.Skip()and.Take(). I've seen a single dropdown query try to load 2.4 million rows because nobody added pagination.
Verification
- Use SQL Server Profiler (free), Azure Data Studio, or DBeaver to watch the actual SQL hitting the database. EF's logging is good; the database's view is ground truth.
- Run
dotnet ef migrations listafter applying. confirm the new migration shows with a checkmark. - Query the __EFMigrationsHistory table directly:
SELECT * FROM __EFMigrationsHistory. Cross-check the migration ID is present. - Hit a real endpoint that touches the data and confirm the response is correct in under 200 ms for a small dataset.
Rolling back a migration
This is where teams panic. Don't. EF gives you a clean path.
- Identify the previous migration.
dotnet ef migrations list. - Roll the database back.
dotnet ef database update PreviousMigrationName. EF runs theDown()method of every migration between current and target. - Remove the failed migration files.
dotnet ef migrations remove. This deletes the latest migration class. - If the migration has already shipped to production and you're past the safe-to-rollback window, you need a forward fix. Generate a new migration that compensates for the broken one. Never edit a shipped migration in place.
Where EF Core bites
- Connection-pool exhaustion. Your default SqlClient pool is 100 connections. Long-held
DbContextinstances or unbounded async loops will drain it fast. - Auto-includes. If you mark a navigation as auto-included in the model, every query loads it whether you want it or not. Hard to debug from the outside.
- Migrations against a wrong environment. Always check
$env:ASPNETCORE_ENVIRONMENTand$env:ConnectionStrings__DefaultConnectionbefore runningdotnet ef database update. Three commands, twenty seconds, peace of mind.
Pair this with a proper CI gate. I run a smoke test on every PR, costs me maybe 4 minutes of GitHub Actions time, catches roughly 80% of regressions.
Operational notes
The next section is what I wish someone had handed me three years ago.
Team and runbook. Pair this with code review checklist items. A two-line note in the PR template covers 80% of the recurring miss.
Cost watch. Free-tier limits move. What costs zero today may cost $0.50 per million requests next year. Set a budget alert at 50% of expected monthly spend: costs you nothing, saves you from a surprise bill.
Security pass. Run a quick OWASP check. Inputs, outputs, authentication, authorization, logging. Five minutes of scanning catches the obvious mistakes.
Observability. Add a span. Distributed tracing is cheap to add and expensive to retrofit. System.Diagnostics.Activity is the API; ActivitySource + ActivityListener is the wiring.
Patterns I keep coming back to
Across many projects in ASP.NET Core, a few patterns repeat that are worth naming explicitly.
- The "smoke test on every deploy" pattern. A 10-line script that hits 5 endpoints, checks response code and latency, exits non-zero on failure. Run it as the last step of every CD pipeline. Catches 70% of post-deploy regressions before they reach a user.
- The "feature flag everything risky" pattern. Microsoft.FeatureManagement.AspNetCore (free, ships with the framework). Wrap any change that could blow up behind a flag, default off in production, flip the flag after the change is verified.
- The "twin environment" pattern. A staging environment that is byte-for-byte identical to production in configuration, with a fraction of the data. Costs maybe 20% extra on Azure if you size sensibly. Catches roughly 50% of "works locally, fails in prod" bugs.
- The "single source of secrets" pattern. Azure Key Vault or HashiCorp Vault. Not a .env file. Not appsettings.Production.json with values committed. Real secret storage with rotation policies.
Decisions worth revisiting periodically
Some choices made early in a project age fast. I keep a list of "look at this every six months" items per project. ASP.NET Core-flavored versions look like this:
- SDK and framework version. Are you on the current LTS? Microsoft's support window is fixed; falling behind two majors means you're paying interest in security patches.
- Dependency surface. Run
dotnet list package --outdatedquarterly. Each outdated package is a small risk; cumulatively, large risk. - Build time. If your CI builds creep past 8-10 minutes, the developer feedback loop slows enough to hurt productivity. Profile and trim.
- Test coverage and execution time. Coverage that climbs slowly is fine. Test suites that climb slowly toward 30 minutes is a warning sign, refactor or parallelize.
- Production error budget. If you're hitting your error budget consistently, slow feature work and pay down operational debt. If you're under-using your error budget, you're shipping too slowly.
Why I do it this way
Years ago I treated each topic in isolation. I'd read the docs, implement the feature, ship it, move on. The result was a lot of features that worked individually and didn't compose well. Today I default to: read the docs, sketch a diagram on paper or a digital whiteboard, identify the trust boundary, identify the failure mode, then implement. Ten extra minutes up front, hours saved down the road.
The other shift was treating tests as the design tool, not the verification tool. Writing the test first forces you to think about the API surface from the caller's perspective. By the time the test compiles, the design has been pressure-tested by your own most demanding consumer: a test that has to drive the feature without knowing the implementation.
Practical example: last month I rewrote a small reporting endpoint. The first draft was a single 60-line action method that pulled data, transformed it, and returned JSON. Tests forced me to split it into an IReportRepository, an IReportFormatter, and a thin controller. Same functionality, but now I can swap the formatter for HTML output by writing one new class and zero changes to the controller. That's the power of writing the test first.
References worth bookmarking
- Microsoft Learn. the canonical source. Whatever version of this page exists at learn.microsoft.com beats my summary.
- The ASP.NET Core GitHub repo's issues and discussions. Real bugs and real solutions, sometimes faster than the docs catch up.
- The .NET API browser at learn.microsoft.com/en-us/dotnet/api, perfect for "what does this method actually return when X."
- The official .NET blog at devblogs.microsoft.com/dotnet: explains the "why" behind changes that the API docs leave implicit.
How to apply this in practice
- Treat this as a starting point. Your tenant, SKU, region, and licence level all change the surface area in small but real ways.
- Run the procedure in a non-production environment first. A staging slot, a dev tenant, a sandboxed subscription, pick one and use it.
- Pin the version you implement against. When you commit to a design choice based on this page, write the date and the exact ASP.NET Core version into your ADR (architecture decision record).
- Cross-check the current Microsoft Learn page before rolling production. The product team updates docs often; what's true today may shift in two months.
Caveats and what to double-check
- ASP.NET Core terminology drifts. The same concept can have two or three names across docs cohorts written in different years.
- Some features in this area may still be in preview. confirm GA status before relying on it for production SLAs.
- Regional availability varies. A feature documented as "global" may actually roll out region by region across months.
- Pricing for ASP.NET Core-related services changes regularly. This page does not track pricing. Use the official Microsoft pricing calculator for the latest numbers.
Related work in your environment
- Document this reference in your team wiki along with which workloads currently depend on it.
- Set up a Microsoft Learn RSS feed or doc-change alert for the source page so your team is notified when Microsoft updates the canonical version.
- Add a periodic review to your governance cadence. Quarterly is a sensible default for ASP.NET Core.
FAQ
az CLI, Get-Az PowerShell, or portal Export Template). A few operations are one-way (storage tier moves, region migration, schema bumps) - check Microsoft Learn for the specific resource type before you commit.References
- Microsoft Learn - official documentation for ASP.NET Core
- Microsoft tech community forums and Q&A
- Azure / Microsoft 365 service health dashboards
Related fixes
Related guides worth a look while you sort this one out: