This is Part 2 in a 3 part series:
- Add .NET Core DI and Config Goodness to AWS Lambda Functions
- IDesignTimeDbContextFactory and Dependency Injection: A Love Story (this post)
- Use EF Core with AWS Lambda Functions
Whenever I set out to create an application or service, I might start out with everything in a single project, but before long I find myself chopping things up into multiple projects. This is in line with the Single Responsibility and Interface Segregation principles of SOLID software development. A corollary of this approach is separating out development-time code from runtime code. You can see an example of this in the NPM world with separate dev dependencies in a package.json file. Similarly, .NET Core has adopted this concept with .NET Core CLI tools, which can also be installed globally.
Note: You can download or clone the code for this post here: https://github.com/tonysneed/ef-design-di
Entity Framework Core provides the
IDesignTimeDbContextFactory interface so that you can separate the EF code needed for generating database tables at design-time (what is commonly referred to as a code-first approach) from EF code used by your application at runtime. A typical implementation of
IDesignTimeDbContextFactory might look like this. Note that using the
MigrationAssembly method is also required for generating code-first migrations.
The code smell that stands out here is that the connection string is hard-coded. To remedy this you can build an
IConfiguration in which you set the base path to the main project directory.
While this is better than including the hard-coded connection string, we can still do better. For example, we might want to select a different appsettings.*.json file depending on the environment we’re in (Development, Staging, Production, etc). In ASP.NET Core, this is determined by an special environment variable,
ASPNETCORE_ENVIRONMENT. We’re also going to want to plug in environment variables, so that the connection string and other settings can be overriden when the application is deployed.
See It In Action
The beauty of adding config to your design-time
DbContext factory is that it will pick up the connection string from the configuration system, selecting the appropriate connection string for the environment you specify. How do you specify an environment (Development, Staging, Production, etc)? Simply by setting that special
ASPNETCORE_ENVIRONMENT environment variable.
If you’re on Windows, you can set and view it like so:
If you’re on Mac, here’s how to do it:
With the environment set, you can switch to the directory where the
DbContext factory is located and run commands to add EF code migrations and apply them to the database specified in the appsettings.*.json file for your selected environment.
Show Me Some DI Love
This solution works, but further improvements are possible. One problem is that it violates the Dependency Inversion principle of SOLID design, because we are newing up the
DbContext in the design-time factory. It might be cleaner to use DI to resolve dependencies and provide the
To remedy this we can factor out the configuration bits into an
IConfigurationService that builds an
IConfiguration, and this service will depend on an
IEnvironmentService to supply the environment name.
The implementations for these interfaces can go into a .NET Standard class library that exists to support configuration.
Now we just need to create
DependencyResolver class that uses an
IServiceCollection to register dependencies.
This class exposes an
IServiceProvider that we can use to get an instance that has been created by the DI container. This allows us to refactor the
ProductsDbContextFactory class to use
DependencyResolver to create the
Pick Your Potion
Adding DI to the mix may feel like overkill, because the
DbContext factory is only being used at design-time by EF Core tooling. In that case, it would be more straightforward to stick with building an
IConfiguration right within the
CreateDbContext of your factory class, as shown in the ProductsDbContextFactory3 code snippet.
However, there is a case where the DI-based approach would be worth the effort, which is when you need to set up DI for the application entry point, for example, when using EF Core with AWS Lamda Functions. More on that in my next blog post. 🤓 Enjoy!