DI or Dependency Injection is a complex topic, not just in Optimizely CMS, but in general in .net core implementations. Some aspects we get, like the different ways dependencies can be injected and which is better over others, while some leave us scratching our heads. Often times we end up doing a lot of troubleshooting, digging into internal code and finding the right alternatives to DI, to get things to work.
I recently learned some interesting things myself, while troubleshooting a strange DI error. And I figured it called for a blogpost for others like me, that run into these almost every other day.
So, here’s to decoding DI in Optimizely CMS 12.
Understanding DI in Optimizely CMS context
To give a little introduction, DI is basically how we register our service (interface) implementations into the .net core service resolver engine. There are several ways to achieve it. Some people prefer to do it explicitly like registering services as :
Some prefer the implicit approach like :
[ServiceConfiguration(Lifecycle = ServiceInstanceScope.Transient, ServiceType = typeof (IService))] public class Service : IService
Check out more in-depth details on Optimizely documentation.
Order of Operations
In CMS implementations, we add Initialization and Configuration Modules to initialize/configure certain services and logic. In CMS 12 specifically, we add a lot of initialization and configuration logic in Startup file, either directly or as extension methods. Most of our DI sits in this initialization/configuration logic. Not just our custom code one, but Optimizely’s internal DI as well.
Choosing a Global Software Development Partner to Accelerate Your Digital Strategy
To be successful and outpace the competition, you need a software development partner that excels in exactly the type of digital projects you are now faced with accelerating, and in the most cost effective and optimized way possible.
Our Startup file has two methods :
- ConfigureServices(IServiceCollection services)
- Configure(IApplicationBuilder app)
So the order of operation goes something like this :
- When Startup first gets called, it looks for any/all Initialization/Configuration modules in code and executes those. Quite a bit of our DI logic gets executed at this step and our services get registered to this ServiceCollection object. You can find more detail on the initialization logic and the order in which the modules are executed here.
- It then executes the ConfigureServices method.
- After this, it executes all the internal Optimizely implicit service registrations (implicit DI).
- Finally, it runs the Configure method that executes application level logic like routing, middlewares, notfoundHandlers etc. But fair to say that by this step, we already have all ours and Optimizely’s services registered into the site’s DI engine.
Our specific issue?
So what helped us figure this order out. We wanted to run some custom code at startup that needed to access a specific service implementation from our code. At first glance, this sounds straightforward. If we have our service registered upfront, we should be able to access it from the DI engine and call our custom code in ConfigureServices.
Catch? – Our service internally was injecting quite a few other internal as well as Optimizely services via Constructor Injection. For those of you unfamiliar with Constructor Injection, here’s an interesting post explaining this and more around DI.
So what was the problem there?
One of the services injected via the constructor was IContentRepository. The default implementation of IContentRepository internally injected IContentProviderManager via its constructor. IContentProviderManager also has a default implementation within Optimizely called DefaultContentProviderManager, which is implicitly registered to DI :
Now, when I called my custom logic from inside ConfigureServices() method in Startup, I got a runtime error :
InvalidOperationException: No service for type 'EPiServer.Core.Internal.DefaultContentProviderManager' has been registered.
Why this error?
If we revisit the Order of Operations above, Optimizely’s implicit service registrations (which includes the one needed for DefaultContentProviderManager) happens at Step 3 and our code expects this at Step 2. And we could have solved for this by explicitly registering this service before our custom code, but unfortunately this is an “internal” class, so we can’t access it directly.
What is the solution?
Solution was simple. We moved our custom logic over to Step 4, into the Configure method instead, allowing for all explicit and implicit DI registrations to complete before requesting any.
I’ve personally encountered so many of these over the last 2 years since I started working on CMS 12, that I feel this is worth sharing. Also for reference, I used JetBrains dotPeek tool to dig into the internal implementations for Optimizely interfaces and classes to understand this setup.
For those of you on the upgrade path, upgrading from CMS 11 to CMS 12 and looking for a solve for all the StructureMap DI logic, here’s my other blogpost that gives plenty of details.
Happy coding (and decoding)!