Multi-site Episerver Solutions using MVC Areas: Dependency Injection

on January 09, 2021 in Episerver, Multi-site, Architecture, Development

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.