<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Adrian Menegatti</title>
    <description>The latest articles on DEV Community by Adrian Menegatti (@amenegatti).</description>
    <link>https://dev.to/amenegatti</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3913911%2Fc5a34aef-cee9-4cde-b4ad-4f8eb0ba2161.png</url>
      <title>DEV Community: Adrian Menegatti</title>
      <link>https://dev.to/amenegatti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amenegatti"/>
    <language>en</language>
    <item>
      <title>How I Built a Smarter EF Core Migration CLI for Multi-Project Solutions</title>
      <dc:creator>Adrian Menegatti</dc:creator>
      <pubDate>Tue, 05 May 2026 16:04:02 +0000</pubDate>
      <link>https://dev.to/amenegatti/how-i-built-a-smarter-ef-core-migration-cli-for-multi-project-solutions-4k1f</link>
      <guid>https://dev.to/amenegatti/how-i-built-a-smarter-ef-core-migration-cli-for-multi-project-solutions-4k1f</guid>
      <description>&lt;p&gt;If you've worked with Entity Framework Core in real-world architectures, you've probably written commands like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet ef migrations add InitialCreate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt; src/MyApp.Infrastructure &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--startup-project&lt;/span&gt; src/MyApp.Api &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--context&lt;/span&gt; AppDbContext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And maybe this worked fine...&lt;/p&gt;

&lt;p&gt;Until your architecture started growing.&lt;/p&gt;

&lt;p&gt;Suddenly you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple APIs&lt;/li&gt;
&lt;li&gt;worker services&lt;/li&gt;
&lt;li&gt;separate infrastructure projects&lt;/li&gt;
&lt;li&gt;multiple DbContexts&lt;/li&gt;
&lt;li&gt;different migration folders&lt;/li&gt;
&lt;li&gt;multiple bounded contexts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And then running migrations becomes surprisingly annoying.&lt;/p&gt;

&lt;p&gt;You forget the startup project.&lt;/p&gt;

&lt;p&gt;You point to the wrong DbContext.&lt;/p&gt;

&lt;p&gt;You generate empty migrations.&lt;/p&gt;

&lt;p&gt;You have no quick visibility into which migrations are applied or pending.&lt;/p&gt;

&lt;p&gt;And every time you need to run a migration command, you have to remember a long list of arguments.&lt;/p&gt;

&lt;p&gt;That friction kept happening to me while working on a multi-tenant SaaS platform I'm currently building, so I decided to build a tool to simplify the workflow.&lt;/p&gt;

&lt;p&gt;That tool became EfPilot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;I wanted migrations to feel like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;efpilot init
efpilot add
efpilot remove
efpilot update
efpilot status
efpilot diff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple.&lt;/p&gt;

&lt;p&gt;Minimal.&lt;/p&gt;

&lt;p&gt;No memorizing paths.&lt;/p&gt;

&lt;p&gt;No repeated boilerplate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem: EF Core works great... until your architecture gets real
&lt;/h2&gt;

&lt;p&gt;In small demos, EF migrations are straightforward.&lt;/p&gt;

&lt;p&gt;But modern architectures often look more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/
 ├── identity/
 │   ├── api/
 │   ├── application/
 │   ├── infrastructure/
 │   └── domain/
 │
 ├── raterisk/
 │   ├── api/
 │   ├── application/
 │   ├── infrastructure/
 │   └── domain/
 │
 └── worker/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my case:&lt;/p&gt;

&lt;p&gt;separate APIs&lt;br&gt;
infrastructure projects&lt;br&gt;
background workers&lt;br&gt;
multiple DbContexts&lt;br&gt;
multi-tenant architecture&lt;/p&gt;

&lt;p&gt;At that point, running migrations manually becomes repetitive and error-prone.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Discovering the solution structure
&lt;/h2&gt;

&lt;p&gt;Before detecting any DbContext, the tool first needed to understand the solution structure.&lt;/p&gt;

&lt;p&gt;My initial implementation scanned directories looking for a traditional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;*.&lt;span class="n"&gt;sln&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That worked...&lt;/p&gt;

&lt;p&gt;Until I realized newer .NET versions can also generate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;*.&lt;span class="n"&gt;slnx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I updated the solution discovery logic to support both formats.&lt;/p&gt;

&lt;p&gt;Once the solution was found, EfPilot could scan all referenced projects automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Automatically detecting DbContexts
&lt;/h2&gt;

&lt;p&gt;After discovering all projects in the solution, the next challenge was identifying actual DbContext classes.&lt;/p&gt;

&lt;p&gt;My initial implementation parsed .cs files looking for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Something&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DbContext&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That worked well...&lt;/p&gt;

&lt;p&gt;Until I started detecting false positives like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;IdentityDbContextFactory&lt;/span&gt;
&lt;span class="n"&gt;RateRiskDbContextFactory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These implement:&lt;/p&gt;

&lt;p&gt;IDesignTimeDbContextFactory&lt;/p&gt;

&lt;p&gt;…but they are not actual DbContexts.&lt;/p&gt;

&lt;p&gt;So I added filtering logic to exclude them.&lt;/p&gt;

&lt;p&gt;That significantly improved detection accuracy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh9b4n4xykp2lhfsr6g77.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh9b4n4xykp2lhfsr6g77.png" alt=" " width="800" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Inferring the correct startup project
&lt;/h2&gt;

&lt;p&gt;This was probably the most interesting problem.&lt;/p&gt;

&lt;p&gt;When multiple projects exist, how do you determine which startup project belongs to a DbContext?&lt;/p&gt;

&lt;p&gt;I implemented a scoring system based on heuristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses Web SDK → higher score&lt;/li&gt;
&lt;li&gt;Contains Program.cs&lt;/li&gt;
&lt;li&gt;Contains appsettings.json&lt;/li&gt;
&lt;li&gt;Project name contains Api&lt;/li&gt;
&lt;li&gt;Folder proximity&lt;/li&gt;
&lt;li&gt;Same bounded context naming&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RateRiskDbContext
→ RateRisk.Api (score: 185)
→ RateRisk.Worker (score: 125)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This made the CLI smart enough to suggest the correct startup project automatically.&lt;/p&gt;

&lt;p&gt;Without forcing manual configuration every time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9fmqx5ozg1m6y96voppq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9fmqx5ozg1m6y96voppq.png" alt=" " width="800" height="94"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Generating config profiles
&lt;/h2&gt;

&lt;p&gt;After detection, EfPilot creates a configuration file like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"solution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyProject.slnx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"profiles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RateRiskDbContext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dbContext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RateRiskDbContext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"apps/raterisk/infrastructure/RateRisk.Infrastructure.csproj"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"startupProject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"apps/raterisk/api/RateRisk.Api.csproj"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"migrationsFolder"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows future commands to be extremely simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Preventing empty migrations
&lt;/h2&gt;

&lt;p&gt;This solved one of my biggest frustrations.&lt;/p&gt;

&lt;p&gt;Sometimes you run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet ef migrations add SomeMigration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and EF generates an empty migration because nothing actually changed.&lt;/p&gt;

&lt;p&gt;Now you have useless migration files polluting your repository.&lt;/p&gt;

&lt;p&gt;EfPilot solves this by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generating the migration&lt;/li&gt;
&lt;li&gt;Inspecting the generated Up() and Down() methods&lt;/li&gt;
&lt;li&gt;Detecting if they're empty&lt;/li&gt;
&lt;li&gt;Automatically removing the migration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If nothing changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No schema changes detected.
Migration automatically removed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small feature.&lt;/p&gt;

&lt;p&gt;Huge quality of life improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Building migration diff preview
&lt;/h2&gt;

&lt;p&gt;This became one of my favorite features.&lt;/p&gt;

&lt;p&gt;Before applying migrations, I wanted visibility into what was about to happen.&lt;/p&gt;

&lt;p&gt;So I added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;efpilot diff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool analyzes migration files and extracts operations like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AddColumn&lt;/li&gt;
&lt;li&gt;DropColumn&lt;/li&gt;
&lt;li&gt;CreateTable&lt;/li&gt;
&lt;li&gt;CreateIndex&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✔ Add column 'Code' to 'Loans'
✔ Create index 'IX_Loans_Code'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvwu8h7bvb7vwass23a7d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvwu8h7bvb7vwass23a7d.png" alt=" " width="800" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Improving CLI UX
&lt;/h2&gt;

&lt;p&gt;I didn't want ugly terminal output.&lt;/p&gt;

&lt;p&gt;So I used Spectre.Console to improve the experience.&lt;/p&gt;

&lt;p&gt;That gave me:&lt;/p&gt;

&lt;p&gt;cleaner headers&lt;br&gt;
colored output&lt;br&gt;
migration tables&lt;br&gt;
applied/pending summaries&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✔ Applied: 5
⏳ Pending: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqacg7g40bivsyzb2ga97.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqacg7g40bivsyzb2ga97.png" alt=" " width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Refactoring and testing
&lt;/h2&gt;

&lt;p&gt;At some point I realized I had built a lot of features quickly...&lt;/p&gt;

&lt;p&gt;but needed better architecture.&lt;/p&gt;

&lt;p&gt;I refactored:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;command abstractions&lt;/li&gt;
&lt;li&gt;dependency injection&lt;/li&gt;
&lt;li&gt;output handling&lt;/li&gt;
&lt;li&gt;test coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was probably less exciting than the feature work...&lt;/p&gt;

&lt;p&gt;but arguably more important.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;A few things surprised me while building this:&lt;/p&gt;

&lt;p&gt;Developer tooling UX matters a lot&lt;/p&gt;

&lt;p&gt;Small frustrations repeated every day are worth solving.&lt;/p&gt;

&lt;p&gt;Real-world architecture creates tooling gaps&lt;/p&gt;

&lt;p&gt;Framework tools often work perfectly for simple examples.&lt;/p&gt;

&lt;p&gt;Real architectures expose friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side projects don't need to be massive
&lt;/h2&gt;

&lt;p&gt;This started as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm tired of typing long EF commands"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And turned into a real CLI product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;EfPilot started as a personal productivity tool.&lt;/p&gt;

&lt;p&gt;But I ended up building something I'd happily use in any serious .NET project.&lt;/p&gt;

&lt;p&gt;If you're dealing with complex EF Core migrations workflows, I'd love your feedback.&lt;/p&gt;

&lt;p&gt;GitHub repo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/adrianmenegatti/efpilot" rel="noopener noreferrer"&gt;https://github.com/adrianmenegatti/efpilot&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Possible future improvements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;NuGet global tool packaging&lt;/li&gt;
&lt;li&gt;Interactive mode&lt;/li&gt;
&lt;li&gt;Better migration diff engine&lt;/li&gt;
&lt;li&gt;Migration visualization&lt;/li&gt;
&lt;li&gt;CI integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This project reminded me that some of the best products come from solving your own recurring frustrations.&lt;/p&gt;

&lt;p&gt;And sometimes... those frustrations start with a very long dotnet ef command 😄&lt;/p&gt;

</description>
      <category>cli</category>
      <category>dotnet</category>
      <category>tooling</category>
      <category>entityframework</category>
    </item>
  </channel>
</rss>
