Refactoring Database Context Initialization
In a microservices architecture there are many (many!) worker and API services. Most of these persist state changes to a database - usually each having their own - and they perform the same initialization steps at startup: set database provider, configure database context, run database migrations. Seems like a perfect candidate for refactoring right? In this post we’ll walk through a pattern for services that use EF Core, but the pattern applies to most any runtime and most any database framework.
Let’s take a closer look at those steps. In the code snippets below “BoundedDbContext” represents any one of the bounded contexts in a context map. For example, “BoundedDbContext” could be “OrderContext”, or “FinancingContext”, or “OrganizationContext”, or any other bounded context in the context map.
Set Database Provider
Configure Database Context
Run Database Migrations
When I see this code repeated over and over again, I can’t help but worry. Without a consistent name for the connection string, will there eventually be a copy/paste error where we get the name wrong? What if someone inadvertently adds the context using a different lifetime? If scope is not used during run-migrations, we’ll have already-disposed issues when using in-memory provider in integration tests. These and other concerns led me to come up with a common pattern for the initialization steps.
Database Program
The first thing you will notice about DatabaseProgram<T> is the DbContext type parameter (T). A derived class (i.e., Order.Dispatcher.Program) specifies its specific database context in the type parameter (e.g., Program : DatabaseProgram<OrderContext>). This enables line 13 to add the specified database context to the dependency framework.
The next thing you will notice is that setting the database provider is set up as an abstract method (ConfigureEntityFramework) for the derived class to implement. The derived class class would override ConfigureEntityFramework and call something like AddEntityFrameworkNpgsql.
On line 12 we get the connection string from a well known connection string key name (“DatabaseConnection”). On line 13 we pass a builder lambda to AddDbContext so that the derived class database context will be created with a Scoped lifetime. Here’s where the derived class would override ConfigureDatabase - receiving the connection string - to initialize a connection to its database context. For example: UseNpgsql(connectionString).
The final initialization step of DatabaseProgram<T> is to run migrations. Nothing special there. The main thing is that it is implemented in a consistent manner. All derived classes know they can control this initialization step via the AppSettings:AutoMigrateDatabase setting.
Let’s take a look at what a derived class might look like. We’ll continue with the “Order Dispatcher” example.
Order Dispatcher
This should pull the three initialization steps together for you, but for completeness:
- Line 1 and 14 are used to configure the database context.
- Line 19 set the database provider.
- Line 14 is used by DatabaseProgram<T> to run migrations.
If you’re wondering what the heck is going on with line 9 and have not read Writing a Test-Oriented Program Main then go ahead and skim through that post, it’ll explain the reasoning behind that unusual approach to run-program.
If you’re interested in using DatabaseProgram<T> in your .NET Core project, add a NuGet reference to AppShapes.Core.Console, or fork appshapes-org/dotnet-core.
As always I hope this was useful for you