Multi-site Episerver Solutions using MVC Areas: Block Controllers

March 2022 Update: This post was originally written for EPiServer version 11. In 2021, Episerver (now known as Optimizely) released version 12. No content in this post has been changed for the current version.

As you are building out a multi-site solution with Episerver, you'll likely find some reuse between sites. You might be reusing page types and templates (in those cases where the sites have similar designs or wireframes) or you might be reusing block types and templates (as those smaller components don't generally need to be specific to a site). If you're not reusing content types, then maybe just the content type class names might be the same between sites.

In whichever case of reuse, getting the proper template for the content type is important. For page types, this is usually handled by a specific controller for the type, and that controller points directly to the view. But for blocks, there's a bit more flexibility about how this can be done.

This post is the third 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 block controllers.

This post builds off the previous posts in this series. If you haven't read those post yet, you should do so before continuing with this one.

Razor View Engines

Blocks in Episerver are just MVC partial views, which means that you don't always need to create a controller for a block type, especially if you're not performing additional logic needed for the block's partial view.

In fact, Episerver even suggests in their documentation to not create block controllers for performance reasons.

As long as the Razor View Engine can find the block's partial view, which is typically saved in the ~/Views/Shared/ folder, Episerver can directly render the block. This is great, as it means there's less code to write and maintain, and the site performs slightly better.

Let's take this block type for example:

namespace MySolution.Models.Blocks
{
    [ContentType(...)]
    public class HtmlBlock : BlockData
    {
        ...
    }
}

The Razor View Engine will search for this block in multiple locations, but usually it's commonly found here:

~/Views/Shared/HtmlBlock.cshtml

But, in a multi-site solution that uses MVC Areas, the potential locations include those Area folders as well:

~/Views/Shared/HtmlBlock.cshtml
~/Areas/SiteA/Views/Shared/HtmlBlock.cshtml
~/Areas/SiteB/Views/Shared/HtmlBlock.cshtml

This approach to use Razor View Engines is what we started with for our multi-site solution, but we ran into a bit of an issue. You can probably already see the issue.

Let's assume that each site in the multi-site solution is separate from each other, and there's no reuse of pages or blocks between the sites, but there is reuse of block type class names. Therefore, it's possible that each site might have their own version of the HtmlBlock. When the Razor View Engine goes to find that view, it's going to return the first one it finds, regardless of the site. This is not good, because it could return a block partial view with incorrect styling or a wrong model namespace.

We could solve this by simply renaming all of the block type class names, maybe to SiteAHtmlBlock and SiteBHtmlBlock, so there would be no confusion by the Razor View Engine on which partial view to return.

But, honestly, that's not very elegant.

Block Controllers

In the end, we decided to move away from having the Razor View Engine return the block partial views, and instead use block controllers. Sure, we might lose a bit of performance by going this way, but at least we can let our development team create the block types without fear of naming collisions.

How will performance be impacted? According to Episerver MVP Erik Henningson, he saw around 10% better performance when not using a controller. So will using block controllers severely impact your site's performance? Maybe not, but it's something to keep in mind.

As I mentioned before, this post builds off my previous posts, so you'll see the same namespaces and class names defined in those posts.

Default Block Controller

To prevent the need for creating a bunch of block controllers, we first added a DefaultBlockController, one for each site, to take care of those blocks that don't need additional logic for the block's partial view.

namespace MySolution.Areas.SiteA.Controllers
{
    [TemplateDescriptor(Inherited = true)]
    public class DefaultBlockController : SiteABaseBlockController<SiteABaseBlockData>
    {
        public override ActionResult Index(SiteABaseBlockData currentBlock)
        {
            return AreaPartialView(currentBlock.GetOriginalType().Name, currentBlock);
        }
    }
}

We're adding the TemplateDescriptor attribute here, with Inherited set to true, to note that this controller can be used for all block types that are inherited from SiteABaseBlockData.

You'll also notice there's a different ActionResult that's being returned. The AreaPartialView is found in the SiteABaseBlockController, the same base controller with the OnActionExecuting() action filter we previously created:

namespace MySolution.Areas.SiteA.Controllers.Base
{
    public abstract class SiteABaseBlockController<T> : GlobalBaseBlockController<T>
        where T : SiteABaseBlockData
    {
        protected override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            base.OnActionExecuting(filterContext);

            filterContext.RouteData.DataTokens["area"] = SiteAConstants.AreaName;
        }

        protected PartialViewResult AreaPartialView(string viewName, object model)
        {
            var viewLocation = string.Format(SiteAConstants.AreaBlocks, viewName);

            return PartialView(viewLocation, model);
        }
    }
}

The AreaPartialView method simply points to the partial view location of the block, as defined in the site's constants file:

namespace MySolution.Areas.SiteA.Business
{
    public static class SiteAConstants
    {
        public const string AreaName = "SiteA";
        public const string AreaBlocks = "~/Areas/SiteA/Views/Shared/Blocks/{0}.cshtml";
    }
}

Of course, there's some flexibility here if you don't want to store the block views in that particular folder.

Other Block Controllers

Since we updated our SiteABaseBlockController with the AreaPartialView method, we can use this for our block controllers that are needed for additional logic:

namespace MySolution.Areas.SiteA.Controllers
{
    public class HtmlBlockController : SiteABaseBlockController<HtmlBlock>
    {
        public override ActionResult Index(HtmlBlock currentBlock)
        {
            // Additional logic

            return AreaPartialView("HtmlBlock", model);
        }
    }
}

With this in place, we can now put all block partial views in one folder, regardless whether the block is being picked up by its own controller or the default block controller.

What's Next?

We now have a way which we can specify where the block partial views are located, and we can build those blocks without the fear of another site using the same block type class name.

From here, the next logical step would be to cover how we set up the block preview functionality for our multi-site solution, which I'll cover in the next post.

More posts in this series: