Multi-site Episerver Solutions using MVC Areas: Dependency Injection
The nice part about building a multi-site solution in Episerver is the flexibility around how the project can be architected. I demonstrated this in my post "Architecting Multi-site Episerver Solutions" by providing multiple approaches in which this could be accomplished. While using the MVC Areas approach allows you to easily separate each site, it can pose a problem if each site needs to implement a specific service.
This post is the second in a series of posts related to architecting and developing multi-site Episerver solutions using MVC Areas. In this post, I'll cover how we handle multiple sites implementing the same service.
This post builds off the previous post in this series. If you haven't read that post yet, you should do so before continuing with this one.
The Setup
Before we dive in, let's first set the stage. It's fairly common in Episerver to create and register a service implementation for dependency injection. In a single-site solution, this is a fairly trivial task.
We have this interface:
public interface IService
{
string GetValue();
}
We implement the interface:
public class MyService : IService
{
public string GetValue()
{
return "The value";
}
}
We can register the implementation (usually in an IConfigurableModule
):
context.Services.AddSingleton<IService, MyService>();
We retrieve the service implementation (in different ways, but for example using ServiceLocator
):
var service = ServiceLocator.Current.GetInstance<IService>();
The Problem
Now, in a multi-site solution, you may need every site to implement the same service interface:
public class SiteAService : IService
{
public string GetValue()
{
return "Site A value";
}
}
public class SiteBService : IService
{
public string GetValue()
{
return "Site B value";
}
}
And then we register it for dependency injection (likely in multiple initialization modules):
context.Services.AddSingleton<IService, SiteAService>();
context.Services.AddSingleton<IService, SiteBService>();
Can you see the problem? When we call for the implementation of IService
, we'll only get the implementation for "Site B". If we are in the context of "Site A", how do we get the service implementation for "Site A"?
The Solution
The way that we solve this problem is by using named instances. This is certainly something we can do in Episerver, but we have to start by using Episerver's StructureMap abstraction.
As I mentioned before, this post builds off my previous post, so you'll see the same namespaces and class names defined in that post.
Registering
To register the service implementation, we'll use an IConfigurableModule
:
namespace MySolution.Areas.SiteA.Business
{
[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class SiteAInitialization : IConfigurableModule
{
public void ConfigureContainer(ServiceConfigurationContext context)
{
context.ConfigurationComplete += (o, e) =>
{
// If multiple sites implement the same service, you MUST name your service registration like this:
context.StructureMap()
.Configure(config => config
.For<IService>()
.Use<SiteAService>()
.Named(SiteAConstants.AreaName));
};
}
public void Initialize(InitializationEngine context) { }
public void Uninitialize(InitializationEngine context) { }
}
}
As you see, we're reusing the AreaName
from our SiteAConstants
class.
Retrieving
Now it gets a little more fun. When we are getting the implementation of the service, we'll very likely not know which AreaName
we're working with. Luckily though, in the majority of cases, we'll want the service implementation that's specific to the current site that is being viewed. This means we can rely on getting the StartPage of the current site. Therefore, we'll need to get the AreaName
from the StartPage.
We'll do this by first creating an interface that each site's StartPage implements:
namespace MySolution.Business.Models
{
public interface IAreaStartPage : IContentData
{
string AreaName { get; }
}
}
Then we can easily implement the interface on the each site's StartPage:
namespace MySolution.Areas.SiteA.Models.Pages
{
[ContentType(DisplayName = "Site A Start Page", GUID = "00000000-0000-0000-0000-000000000000",
GroupName = SiteAConstants.AreaName)]
public class StartPage : SiteABasePageData, IAreaStartPage
{
[Ignore]
public string AreaName => SiteAConstants.AreaName;
}
}
We add the [Ignore]
attribute to the AreaName
property because Episerver doesn't really need to know about this property.
Then from here, as long as each site's StartPage implements that IAreaStartPage
interface, we can retrieve the AreaName
like so:
var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
var areaName = contentLoader.Get<IAreaStartPage>(SiteDefinition.Current.StartPage).AreaName;
Finally, we can get the site's specific service implementation like this:
var service = ServiceLocator.Current.GetInstance<IService>(areaName);
What's Next?
Knowing we can get a service implementation for a specific site, this helps us further build out the architecture of a multi-site solution in Episerver using MVC Areas. We can use this concept for additional features, which we'll see in future posts related to this topic.
More posts in this series:
- Multi-site Episerver Solutions using MVC Areas
- Multi-site Episerver Solutions using MVC Areas: Dependency Injection (You are here)
- Multi-site Episerver Solutions using MVC Areas: Block Controllers
- Multi-site Episerver Solutions using MVC Areas: Block Preview
- Multi-site Episerver Solutions using MVC Areas: Restricting Content Types