In this tutorial, you will learn about dependency injection within Episerver. You will learn about a technique that may help you write cleaner code. You will also helpfully learn a little bit about the CMS and how it has improved over the years.
Trying to get 100% test coverage in your project is a noble goal. In Episerver 6 trying to get good test coverage was difficult. The main way to access page data was via the DataFactory API. Trying to test code that used DataFactory
was difficult. The DataFactory
was implemented using a static helper that followed the singleton pattern. The DataFactory
was difficult to mock. The DataFactory
also relied on the HTTP context to exist. Without the correct context, the DataFactory
would throw a NullReference
whenever it was accessed within a test.
The key to writing tests is to inject all the dependencies required by a class instead of instantiating them directly. In simple terms, you can not use the new
keyword anywhere in your class. Instead, you will need to inject all your dependencies through the constructor as a parameter. This is why the way in which you access the APIs changed within Episerver 7. This change in API access makes for more flexible architectures. Two APIs that come with Episerver 7 are the IContentRepository
and the ILinkResolver
. You can access ILinkResolver
like this:
var pageRouteHelper = EPiServer.ServiceLocation.ServiceLocator.Current.GetInstance(); | |
var pageReference= pageRouteHelper.PageLink; |
This code uses a service locator to get access to the API. Service location is considered a bad technique. Instead, you should access an API using constructor injection, like so:
public class MyClass | |
{ | |
public MyClass(IPageRouteHelper pageRouteHelper) | |
{ | |
// We now have acess | |
} | |
} |
When you start injecting dependencies like this, sooner or later you will run into the problem of injecting too many items.
public MyClass(IDependency1 one, IDependency2 two, IDependency3 three, IDependency4 four) | |
{ | |
} |
One way to elevate this parameter overload is to group related dependencies using the facade pattern. In this example, I will create a facade called EpiserverDependencies
. My class will now look like this:
using EPiServer.Core; | |
namespace EpiserverDependencies | |
{ | |
public class EpiserverDependencies : IEpiserverDependencies | |
{ | |
private readonly Lazy<IContentRepository> _contentRepository; | |
private readonly Lazy<ILinkResolver> _linkResolver; | |
public BaseDependencies( | |
IContentRepositoryFactory contentRepositoryFactory, | |
ILinkResolverFactory linkResolverFactory) | |
{ | |
_contentRepositoryFactory = new Lazy(contentRepositoryFactory.GetContentRepository()); | |
_linkResolverFactory = new Lazy(linkResolverFactory.GetLinkResolver()); | |
} | |
public ILinkResolver LinkResolver | |
{ | |
get | |
{ | |
return _linkResolverFactory; | |
} | |
} | |
public IContentRepository ContentRepository | |
{ | |
get | |
{ | |
return _contentRepositoryFactory; | |
} | |
} | |
} | |
} |
What this code is doing is passing in an 'IContentRepositoryFactory' and 'ILinkResolverFactory'. These are two custom factories that I have created that will create the Episerver dependencies. I also use Lazy
to store the IContentRepository
and ILinkResolver
. If you have never come across Lazy
it allows you to only instantiate something the first time it is actually used. As this class will be passed into every base controller (read on for explanation), we do not want to instantiate everything on every request. The dependencies might not be used on each page request so delaying the load saves processing time and improves performance.
Now we have a class that will make it possible to unit test our code simpler. The next question is, how do we actually this class within a website? The first thing you need to do is inject your dependencies into your website. Episerver comes with Structure Map out-of-the-box. THe code to do this, would look something like this:
public static class IoC | |
{ | |
public static IContainer Initialize() | |
{ | |
ObjectFactory.Initialize(x => | |
{ | |
x.Scan(scan => | |
{ | |
scan.TheCallingAssembly(); | |
scan.WithDefaultConventions(); | |
}); | |
x.For<IEpiserverDependencies>().Use<EpiserverDependencies >(); | |
}); | |
return ObjectFactory.Container; | |
} | |
} |
Now we have set up Structure Map to inject our base dependencies whenever a call to IEpiserverDependencies
is made. Our next step is to actually start injecting this dependency into a class so we can use it. The starting point for all of your pages will be a controller. In Episerver, one way of structuring your website is to use a base page controller. This allows you to add some common features like your base dependencies and SSL. Below shows you an example of an Episerver base controller:
namespace BasePageController | |
{ | |
public class BasePageController<T> : PageController<T>, IEpiserverDependenciesResolver | |
where T : GlobalBasePage | |
{ | |
private Injected<EpiserverDependencies> epiServerDependencies Service; | |
protected EpiserverDependencies EpiserverDependencies | |
{ | |
get { return baseDependenciesService.Service; } | |
} | |
IEpiserverDependencies IEpiserverDependenciesResolver.EpiserverDependencies | |
{ | |
get { return EpiserverDependencies; } | |
} | |
} | |
} |
The next step in this process is to create a corresponding page controller for a page type. In this example, I am going to use an example page called Content
:
public class ContentPageController : BasePageController<ContentPage> | |
{ | |
public ActionResult Index(ContentPage currentPage) | |
{ | |
return View("Index", new ContentPageViewModel(currentPage, EpiserverDependencies)); | |
} | |
} |
In the controller, a view model is created and passed down into the view. Some people get confused around the use of a view model when they have the Episerver page object. In my simple opinion, a controller is a place that the routing engine looks for when it's trying to resolve a URL. The controller is responsible for getting the data required by the view, adding caching, authorization, handling query string. The Episerver page object should not contain code that renders presentation data. Data required in the view should be done in a view model. This creates a clean separation of concerns. Now we have that cleared up, let's see what the view model looks like:
namespace Example | |
{ | |
public class ContentPagePageViewModel : GlobalViewModel<ContentPage> | |
{ | |
private readonly IEpiserverDependencies _epiServerDependencies; | |
public BlogHomePageViewModel(ContentPage currentPage, IEpiserverDependencies epiServerDependencies) | |
: base(currentPage) | |
{ | |
_epiServerDependencies = epiServerDependencies; | |
} | |
} | |
} |
A lot of Episoerver sites use a base view model, where an instance of the current page and Episerver dependencies is passed into it to save on code duplication:
public class GlobalViewModel<T> : IPageViewModel<T> where T : PageData | |
{ | |
public GlobalViewModel(T currentPage) | |
{ | |
CurrentPage = currentPage; | |
} | |
public T CurrentPage { get; private set; } | |
public LayoutViewModel Layout { get; set; } | |
public IContent Section { get; set; } | |
} |
We have our dependencies injected into a view model, let us use them in a view:
GetContentPageCount(_epiServerDependencies.ContentRepository) | |
public static int GetContentPageCount(IContentRepository _repository) | |
{ | |
return _repository.GetChildren<ContentPage>(currentPage).Count(); | |
} |
This is a very small example, however, you can see how it has removed the dependency from Episerver in the controller. If you wanted to unit test this simple method you can now use something like MOQ to mock IContentRepository
.
That wraps up the first in this series of how to unit tests in Episerver and as of yet, we have yet to write a single test! We have written a mini-framework that will allow you to unit test anything Episerver API used in our code easily and consistently. This is a huge step forward. In the next chapter of this Epi saga, I will show you how you can write tests using fakes or the more popular mocks. The code for the whole of this series can be found on my #ILoveEpiserver project here enjoy :) Happy Coding 🤘