Anyone who has spent any time working with EntityFramework knows that the library has it’s share of warts. One of the biggest, in my opinion, is that the library is designed to position the data context class(es) right in the middle of the application. I mean that literally, by the way. As in, EntityFramework wants all the data context class(es) to physically live inside the application’s project. It also wants the main application to be the one to link to all the various EntityFramework nuget packages.
My main problem with that approach is, it tends to clutter everything up. More than that, I think it discourages the use of industry standard best practices that are designed to help developers create clean architecture. After all, it’s hard to avoid a cluttered mess when, thanks to Microsoft, you’ve started by placing your database access code in the same project with your business logic, your GRPC / REST interface(s), oh and your presentation code, as well!
It’s not your fault though. After all, every bit of Microsoft EntityFramework sample code I’ve ever seen depicts the data context class living wherever its used, often without any abstraction at all. What’s more, the Microsoft project wizards all deliberately place the EntityFramework code inside the main application. If you’ve been following Microsoft, and trusting them to do things correctly (as most .NET developers do), then you’ve simply been led astray.
So, the obvious solution is to move your EntityFramework code into a separate class library, right? Problem solved? Well, yes and no. The first issue you’ll notice is that the tooling, in Visual Studio, really, really, really wants the EntityFramework code to live inside your application. It will prompt you to add the Microsoft.EntityFrameworkCore.Design nuget package into you application – since EntityFramework is supposed to live in the application, right? You might have a similar issue with the Microsoft.EntityFramework.Tools nuget package. Referencing those (and other) Microsoft nuget packages from, say, a class library where you’ve moved your data context class(es) to won’t work. Visual Studio won’t be happy until you’ve moved at least those two nuget packages back into the application.
Maybe it’s me, but referencing EntityFramework nuget packages in my application, just to make the tooling in Visual Studio happy, doesn’t sound like a good solution.
After some fiddling, I came up with a solution where I move my data access code into a separate console application project, then reference that project, from my application, just like it was a class library. I know, that seems weird, at first, but it’s perfectly legal to do, in .NET. In fact, that little trick gets you about 80% of the way towards an application that’s blissfully unaware of EntityFramework. As long as you remember to set the console project as the “start project”, in Visual Studio, before you try to work with migrations, everyone is happy.
There is one issue left though. It’s an edge case, to be sure, but for me, it’s an important one. You see, I have several projects that need to be able to work with more than one type of database. For instance, I have microservices that usually work with SQLServer, but also need to support SQLite, or occasionally, even work with an in-memory database. For those projects, having all the migrations in a single project is still stupidly difficult. I knew I needed to put each set of migrations into a different console application, but, I didn’t want to duplicate any of the other code (data context, entities, etc), unless I absolutely had to.
Then it dawned on me that I could put all the data access code except the migrations into a single class library, and then put the migrations themselves into the console apps. That way, if I needed to support SQLServer, there would be a console app with my SQLServer migrations in it. Likewise, if I needed to support SQLite, there would be a console app with my SQLite migrations. Using this approach, the migration code is always in an application, so Visual Studio is happy. I’m happy because the core of my data access layer is inside a common class library with no duplicated code anywhere. It’s a win/win scenario.
While working through the details for this approach, I stood up a demo web project called
CG.Ivory. The code for that project is HERE.
CG.Ivory implements a simple TODO checklist. But, behind the simple UI is an architecture that would easily work for a much more complicated website. All the database code is in a single class library. All the migrations, for SQLServer, SQLite, and in-memory, are located in separate console applications that are then referenced by the Blazor website. Changing between databases is easy, done inside the appSettings.json file, and requires no recompile or down time.
I wrote a nuget package called
CG.EntityFrameworkCore that makes that database type switching possible. I’ll write about the
CG.EntityFrameworkCore package in a future blog entry.
I also wrote another nuget package called
CG.Seeding to perform quick and easy database seeding for the
CG.Ivory website. That way, there are always some TODO tasks when the website first starts. I’ll write about the
CG.Seeding nuget package in a future blog entry.
I may also write about the
CG.Ivory website in a future article. I haven’t decided about that yet.
So, the upshot of all this is, put your data access code in a class library, then put your migration code in a separate console application, then, finally, link to both of those projects from your main application project. I could almost start to like EntityFramework at this point …
The code for
CG.Seeding is HERE.
The code for
CG.EntityFrameworkCore is HERE.
The code for
CG.Ivory is HERE.
Edit: Oh by the way, placing your migrations into a console application project also gives you the perfect place to put any command-line driven data-access utilities. For instance, in some of my projects, I put a migrator there so I have the ability to tightly control which migrations get applied, and when.
Also, If you choose to live dangerously and run migrations in production, you can integrate your console application with your identity server and force the caller to authenticate with a password.