Application Migrations for ASP.NET Core: A Small Library for a Common Problem
Managing Application Updates in ASP.NET Core
You know how it goes. You build an app, you deploy it, everything’s great. Then you need to push an update. Maybe you’re adding a new feature that requires some initial data. Or you need to transform existing records because you changed how something works. Or you just need to create a folder structure on first run.
If you’ve been doing this for a while, you probably have your own hacky solutions. A boolean flag in the database that says “did we run the v2 setup?”. Some code that checks if a file exists before creating default config. A bunch of if statements scattered across your Program.cs.
I’ve written all of these. They work, until they don’t.
The real issue is that we don’t have a proper way to version our application’s state — not the database schema, but the application itself.
EF Core Migrations Don’t Cover Everything
EF Core migrations exist and they’re great for what they do: managing database schema changes. Need a new table? Migration. New column? Migration. Index? Migration.
But EF Core migrations run before your application code. They deal with structure, not data. They don’t handle things like:
Seeding initial data after a schema change
Sending a one-time notification when a feature goes live
Creating file system structures
Running cleanup tasks when upgrading from v1 to v2
Transforming existing data when you change how something is stored
You could try cramming this stuff into EF migrations using raw SQL, but honestly, that’s a mess. No dependency injection, no access to your services, no proper C# code. And good luck testing any of it.
A Small Library for Application Migrations
I wrote a library to handle this. The idea is simple: give your application the same versioning semantics that your database schema already has.
Here’s what a migration looks like:
Notice a few things:
- Constructor injection works: Your migrations are full citizens of the DI container.
- Versions are explicit: No date-based naming, no magic strings. Just System.Version.
- The *FirstTime *flag: This tells you if this version has ever been registered before.
That last one is particularly useful. During development, the current version gets re-executed on every startup so you can iterate quickly. But the FirstTime flag lets you guard operations that should genuinely only happen once.
The Lifecycle Hooks
Sometimes you need more control. Maybe you’re doing a complex data transformation where you need to capture data before a schema change and apply it after. The library gives you hooks for this:
The cache dictionary is shared with your migrations, so you can read that captured data in your UpAsync() method and transform it however you need.
Setting It Up
The setup is pretty minimal:
When you configure a DbContext, the library automatically runs Database.MigrateAsync() before executing your application migrations. So you don’t need to call it yourself — EF Core schema changes are applied first, then your application migrations run.
You do need to implement the storage part yourself — where do you want to track which versions have been applied? Most people use a database table, but you could use a file, Redis, whatever makes sense for your setup.
Here’s a basic database implementation:
Multi-Server Deployments
If you’re running multiple instances of your app (load balancing, replicas, etc.), you probably don’t want all of them trying to run migrations at the same time. The engine has a ShouldRun property you can override to control this.
A common pattern is to designate one instance as the “master” via configuration:
Then in your deployment, only the primary instance gets Migrations:IsMaster = true. The others will skip migrations entirely and start normally. This way you avoid race conditions and duplicate executions without needing distributed locks.
Real-World Use Cases
Here are some scenarios where this actually helps:
Feature rollout with data seeding: You’re adding a new “Categories” feature. The EF migration creates the table, but you need to populate it with default categories and assign existing products. One migration handles both the data creation and the assignments.
Configuration evolution: Your app used to store settings one way, now it stores them differently. Write a migration that reads the old format and writes the new one.
Environment setup: First deployment to a new server? Create required directories, generate default config files, set up whatever your app needs to run.
Notification on upgrade: Want to email admins when a major version is deployed? Put it in a migration, guarded by FirstTime.
Why No Down() Method?
There’s no Down() method for rollbacks. In my experience, rollback methods are rarely tested and often broken when you actually need them. I find it cleaner to write a new forward migration that undoes whatever you need to undo. Version 1.1 messed something up? Version 1.2 fixes it.
Getting It
The package is on NuGet:
dotnet add package AreaProg.AspNetCore.Migrations
It targets .NET 6, 8, 9, and 10. Source is on GitHub if you want to poke around: https://github.com/ssougnez/aspnet-migrations
— —
This isn’t revolutionary stuff. It’s just filling a gap that I kept running into project after project. If you’ve ever written a “run once on first deploy” hack, maybe give it a try.
Questions? Feedback? I’d love to hear how others have been solving this problem.
Top comments (0)