Changing registered services at runtime in EPiServer 7.5
The new Inversion of Control pattern introduced with EPiServer 7 is, in my opinion, one of the best new features
of EPiServer CMS. Anywhere in your application you can simply call the ServiceLocator and GetInstance <ISomeInterface>()
and
it will return the concrete implementation of your choosing of that interface.
This is of course nothing new, StructureMap has been around for years and there are dozens of other excellent frameworks for IoC.
However, in the EPiServer world this is still kind of new and I’ve seen several epi-projects where the concept of IoC is still largely misunderstood. But this is not a post about IoC as such, but if you’re interested here’s an introduction to StructureMap, it’s a pretty old post but still interesting.
I was working in a project recently where we created a couple of concrete implementations for cache handling. A CacheManager
and a NullCacheManager
, the CacheManager
utilized the HttpRuntime.Cache
in the background, the NullCacheManager
simply cached
nothing as one would expect. They both implement the ICacheManager
interface, shown highly simplified below.
public interface ICacheManager {
GetCachedItem<T>(string key, Func<T> uncachedMethod);
SetCachedItem<T>(string key, T item);
RemoveCachedItem(string key);
}
This was all good and we could now easily decouple our design using code looking something like this.
public void SomeMethod() {
var cacheManager = ServiceLocator.Current.GetInstance<ICacheManager>();
var cachedItem = cacheManager.GetCachedItem<SomeObject>("key", SomeMethodThatFetchesSomObjectUnCached);
}
This is great and gives us a very loosely coupled design. We could easily create a new concrete type of ICacheManager
such as
SqlCacheManager
or FileSystemCacheManager
and switch between the implementations using a ConfigureableModule, something like this:
public class ConfigureableModule : IConfigurableModule
{
public void ConfigureContainer(ServiceConfigurationContext context)
{
var container = context.Container;
container.Configure(c => c.For<ICacheManager>().Use<CacheManager>());
}
}
We could also have different concrete implementations for different environments by using profiles, like this:
public class ConfigureableModule : IConfigurableModule
{
public void ConfigureContainer(ServiceConfigurationContext context)
{
var container = context.Container;
container.Configure(c => {
c.Profile("debug", ctx => { ctx.For<ICacheManager>().Use<NullCacheManager>();
c.Profile("release", ctx => { ctx.For<ICacheManager>().Use<CacheManager>();
});
});
});
}
}
But wouldn’t it be pretty cool if we could switch the implementation at runtime? Turns out it’s not too difficult to achieve…
Lets create an admin plugin where an administrator can choose which implementation of ICacheManager
should currently be used by
the site.
We start out by creating a simple plugin:
[EPiServer.PlugIn.GuiPlugIn(Area = EPiServer.PlugIn.PlugInArea.AdminConfigMenu, Url = "/modules/samplesite/ChangeCacheManager/Index", DisplayName = "Cache management")]
[Authorize(Roles = "Administrators")]
public class ChangeCacheManagerController : Controller
{
public ActionResult Index() { return View(); }
}
Remember that we need to add this nonsense to the episerver.shell part of our web.config as well for our module to be picked up:
<episerver.shell>
<publicModules rootPath="~/modules/" autoDiscovery="Minimal">
<add name="samplesite">
<assemblies>
<add assembly="WhateverYouNameYourAssembly" />
</assemblies>
</add>
</publicModules>
</episerver.shell>
Also please note that I’ve added an authorization attribute to the controller to make sure noone but an administrator stumbles upon it.
Add an empty index.cshtml view for now and type something awesome in it. Compile and run and make sure your plugin appears in the admin config menu.
Allright, next lets create a model for our view.
public class CacheManagerViewModel {
public string SelectedManager { get; set; }
public List<SelectListItem> ConfiguredManagers { get; set; }
public CacheManagerViewModel() {
ConfiguredManagers = new List<SelectListItem>();
}
}
Simple, all our plugin will need is a list of the available cachemanagers and a string representing the currently selected one. Ok, lets flesh out our view next.
@model Web.AdminPlugins.Models.CacheManagerViewModel
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="/EPiServer/Shell/7.11.1.0/ClientResources/epi/themes/legacy/ShellCore.css">
<link rel="stylesheet" type="text/css" href="/EPiServer/Shell/7.11.1.0/ClientResources/epi/themes/legacy/ShellCoreLightTheme.css">
<link href="../../../App_Themes/Default/Styles/ToolButton.css" type="text/css" rel="stylesheet">
<title>Cache management</title>
</head>
<body>
<div class="epi-contentContainer epi-padding">
<h1>Select active cachemaanger</h1>
<p>Select which CacheManager implementation to use for this site.</p>
@using (Html.BeginForm())
{
<div class="epi-padding">
<p>Currently active cachemanager: <strong>@Model.SelectedManager</strong></p>
<div class="epi-size25">
<div>
<label>Select cachemanager: </label>
@Html.DropDownListFor(m => m.SelectedManager, Model.ConfiguredManagers, new { @class = "episize240" })
</div>
</div>
</div>
<div class="epi-buttonContainer">
<span class="epi-cmsButton">
<input class="epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Save" type="submit" value="Save" />
</span>
</div>
}
</div>
</body>
</html>
Now we have a model and a view, all that’s left is to add some code to our controller:
public ActionResult Index()
{
return View(GetViewModel());
}
[HttpPost]
public ActionResult Index(CacheManagerViewModel model)
{
var container = ServiceLocator.Current.GetInstance<IContainer>();
var selectedManager = Type.GetType(model.SelectedManager);
container.Model.EjectAndRemoveTypes(t => t == selectedManager);
var instance = (ICacheManager)container.GetInstance(selectedManager);
container.Configure(x => x.For<ICacheManager>().Use(instance));
return View(GetViewModel());
}
private CacheManagerViewModel GetViewModel()
{
var model = new CacheManagerViewModel();
var currentActiveManager = ServiceLocator.Current.GetInstance<ICacheManager>();
model.SelectedManager = currentActiveManager.GetType().Name;
foreach (var manager in ServiceLocator.Current.GetAllInstances<ICacheManager>())
{
model.ConfiguredManagers.Add(new SelectListItem()
{
Text = manager.GetType().Name,
Value = manager.GetType().AssemblyQualifiedName,
Selected = currentActiveManager.GetType() == manager.GetType()
});
}
return model;
}
There are a few interesting points in the code above. Lets first consider GetViewModel()
method. We simply get the currently
active concrete implementation and then loop through all instances of ICacheManager
that is currently registered in our IoC
container. This brings us to an important point, allimplementations of ICacheManager
must have been registered in our
container or they wont be returned by the GetAllInstances
method. This could be done in simple fashion by using the
AddAllTypesOf<ICacheManager>
when we configure our container. Something like this:
public void ConfigureContainer(ServiceConfigurationContext context) {
context.Container.Configure(c => c.Scan(s => s.AddAllTypesOf<ICacheManager>()));
}
Next lets have a look at the post method. This method looks kinda peculiar, what’s all this ejecting and removing stuff?
It turns out that we can’t really just change which configured cachemanager to use with For.Use
because that would add
another instance of the same type and not replace the existing one, and that’s why we first need to eject the model of the
selected type and then re-configure it.
And that should be it! We now have a working admin plugin that lets us change fundamental inner workings of our application
at runtime. We could substitute ICacheManager
for IContentRepository
and radically change how we fetch our content, we could
substitute it for IContentRenderer
and add functionality such a detailed logging to the rendering of our content… There’s no end
to the possibilities!
But, as we’ve learned from modern classics such as the bible and the amazing spiderman: