Now that Kentico has empowered its developers to build applications with ASP.NET MVC, we can leverage design patterns and best practices that are encouraged by the framework. ๐
As our libraries and code bases change to take advantage of these patterns and practices, can we use them in the CMS application which is built on ASP.NET Web Forms? If so, how do we accomplish this? ๐ค
With the pattern of Dependency Injection, the answer is Yes and Yes! ๐
Let's take a look!
The Pattern
One of the patterns that MVC encourages is Inversion of Control (IoC). This pattern can be implemented through Dependency Injection (DI) with both framework types and custom user defined types.
In application frameworks, the way IoC is usually accomplished is by allowing the framework to create and call specific entry point types, created by the developer โ from there our code executes until it returns.
IoC allows for us to use DI in an elegant manner because the framework is responsible for calling into our code (IoC) โ therefore it can also be responsible for creating the things our code depends on (DI).
DI in MVC
In an MVC application, one of those entry point types is the System.Web.Mvc.Controller
class, which all custom controllers inherit from.
Here is an example UserController
with (2) dependencies, IUserContext
and IEmailService
:
public class UserController : Controller
{
public UserController(IUserContext userContext, IEmailService emailService)
{
// ...
}
}
MVC needs to be able to construct instances of UserController
when it decides that an HTTP request should be handled by UserController
. MVC makes this decision based on URL or request body patterns, which map to the routing conventions or configuration in our app.
To see a routing configuration pattern that mixes convention and configuration read my previous post in this series. ๐ช
Kentico 12: Design Patterns Part 3 - Tips and Tricks, Application Structure
Sean G. Wright ใป Jun 1 '19 ใป 7 min read
#mvc #designpatterns #kentico #aspnet
If we are using DI through constructor injection, how is MVC able to figure out how to create the dependencies of our controller class? Or the dependencies of those classes?
This recursive problem is solved by an IoC Container. IoC Containers are configured to "know" how to construct an instance of any type used in an application that is considered a dependency. This configuration of types is often known as "registration".
So, at the point in our application where MVC needs to create an instance of our UserController
, it effectively tries to get one from the IoC Container.
The IoC Container figures out the entire chain of dependencies required to create a UserController
, creates one, and then returns it to the framework code.
When using IoC and DI, our code does not create its own dependencies.
Instead all dependencies must be provided to our code from somewhere higher up. This is the "inversion" of "control" over our own code's dependencies. ๐ฎ
DI In Web Forms
The ASP.NET Web Forms architecture is based around the Page
and UserControl
types.
When working with Kentico we use the
CMSPage
andCMSAbstractWebPart
classes, accordingly.
Both of these types require public parameter-less constructors.
What happens if we specify dependencies as constructor parameters? The Web Forms framework will fail to create an instance of the type and the request will fail with an exception! ๐คฆ๐ฝโโ๏ธ
ASP.NET Web Forms was not designed for the same patterns and practices that MVC encourages, but that doesn't mean we are at an impasse.
The Library - Autofac
With the design pattern established, let's dive into how we configure it, not for MVC, but for ASP.NET Web Forms, which is the framework that the Kentico content management application is built on.
We will be using Autofac as our IoC Container of choice.
There are many IoC Container libraries available in .NET. This benchmark blog by Daniel Palme contains a pretty thorough list of the most well known libraries.
In the past I've mostly used SimpleInjector because of its great performance, simple API, and container verification feature. It's a fantastic library and container verification is awesome.๐
That said, AutoFac provides some nice APIs for simplifying some problems .NET application frameworks require developers to face. It's also the IoC Container that Kentico uses in its example Dancing Goat site.
We will be using (2) NuGet packages (with versions specified):
Autofac (
v4.9.2
) - This is the main IoC Container library which is application framework agnostic.Autofac.Web (
v4.0.0
) - This is the ASP.NET Web Forms integration package for Autofac.
The Web Forms integration package allows us to perform DI in a couple of different ways.
Property Injection - Assigns instances resolved by the IoC Container to public properties of the Web Forms
Page
orUserControl
instance. โAttribute Property Injection - Same as the above, but only if a specific attribute is applied to those pages or controls. โ
Null Attribute / Property Injection - Using either approach above, this assigns a resolved instance only if the property is
null
at the time of assignment. โ
We are going to use the second option, you can explore the other two in Autofac's documentation.
Update (2019/07/09): Kentico has issues in certain areas of the CMS (Reports Module) when using the first option above. For the most part it works but not completely, and that's not good enough! So, while I initially recommended the first option, I'm now recommending the second.
Thanks to Tony at Luminary for discovering this problem!
Go ahead and install the NuGet packages into the CMSApp
project.
Managing NuGet packages in .NET Framework projects can be a lot easier if you use modern .NET features and tools. Check out my post to learn more. ๐ช
OK, now let's setup our application!
In .NET 4.7.2 a constructor based DI solution has been added to ASP.NET for Web Forms and this solution can be used as a hook point, with an adapter layer, to integrate an IoC Container.
Here is an example integrating SimpleInjector.
I'm going to continue using Property Injection since it's what Autofac supports without customization.
The Implementation - Kentico 12 CMS
The Autofac documentation has a great walkthrough of how to configure our application to start using the library, and I will summarize the steps below.
Configuring The Project
First, Add the following ASP.NET module entries to the application web.config
under the <configuration><system.webServer><modules>
node.
<add
name="ContainerDisposal"
type="Autofac.Integration.Web.ContainerDisposalModule, Autofac.Integration.Web"
preCondition="managedHandler"/>
<add
name="PropertyInjection"
type="Autofac.Integration.Web.Forms.AttributedInjectionModule, Autofac.Integration.Web"
preCondition="managedHandler"/>
Next, update our Global
class in Global.asax.cs
to support Autofac.
// Add the IContainerProviderAccessor interface
public class Global : CMSHttpApplication, IContainerProviderAccessor
{
static Global()
{
/// leave existing code
}
// Holds the application container
private static IContainerProvider containerProvider;
// Used by Autofac to resolve dependencies via IContainerProviderAccessor
public IContainerProvider ContainerProvider => containerProvider;
// Creates a container builder, builds it, and then sets the container
protected void Application_Start(object sender, EventArgs e)
{
var builder = new ContainerBuilder();
var container = builder.Build();
containerProvider = new ContainerProvider(container);
}
}
That's it! DI is now set up for our CMS application!
... except we haven't registered any types, so our IoC Container and its integration into Web Forms isn't very helpful at the moment. ๐
So, let's register some types with Autofac!
Registering Our Types
What kind of types do we want to register?
Why don't we use the ISiteContext
and KenticoSiteContext
types from previous posts in this series? ๐
First, create the ISiteContext
interface:
public interface ISiteContext
{
string SiteName { get; }
int SiteId { get; }
SiteInfo Site { get; }
}
And now the KenticoSiteContext
implementation:
public class KenticoSiteContext : ISiteContext
{
public string SiteName => SiteContext.CurrentSiteName;
public int SiteId => SiteContext.CurrentSiteID;
public SiteInfo Site => SiteContext.CurrentSite;
}
While there is nothing preventing us from adding all the Autofac type registrations to our Global.asax.cs
file, I'd recommend moving type registration to a different class.
I typically create a DependencyResolverConfig.cs
class (or something similar).
In this class we can register our types and then build the container:
public static class DependencyResolverConfig
{
public static IContainer BuildContainer()
{
var builder = new ContainerBuilder();
builder
.RegisterSource<KenticoRegistrationSource>();
builder
.RegisterType<KenticoSiteContext>()
.As<ISiteContext>();
var container = builder.Build();
return container;
}
}
As this file grows in registrations and complexity, I always move the registrations out into separate classes that each focus on a specific part of the application (ex: Application, Data Access, Authorization).
You can find an example in a gist here.
Notice the line builder.RegisterSource<KenticoRegistrationSource>();
.
This tells Autofac that when it doesn't know how to resolve a type, forward the request for an instance of that type on to Kentico. An example type that this could resolve for us would be CMS.Ecommerce.ICurrentShoppingCartService
.
The code for
KenticoRegistrationSource
is a bit long, so it can be found in this gist.
Now we need to update our Global
class in the Global.asax.cs
file to use the new DependencyResolverConfig
class. The only change that needs to be made is to replace the Application_Start
method with the code below.
protected void Application_Start(object sender, EventArgs e)
{
var container = DependencyResolverConfig.BuildContainer();
containerProvider = new ContainerProvider(container);
}
Resolving Our Types in a Page
Let's create a Web Form class and test resolving our types.
Create a new Web Form Page under CMSApp -> CMSPages
with the name DIPage
. Replace the contents of DIPage.aspx.cs
with the code below.
[InjectPropertiesAttribute]
public partial class DIPage : CMSPage
{
public ISiteContext SiteContext { get; set; }
public ICurrentShoppingCartService CurrentShoppingCartService { get; set; }
protected void Page_Load(object sender, EventArgs e)
{
var service = CurrentShoppingCartService;
var cart = service.GetCurrentShoppingCart(
CurrentUser,
SiteContext.SiteName);
}
}
We will set a breakpoint in Visual Studio on the opening brace of the Page_Load
method, and run the CMS project with debugging.
When we hit the breakpoint and step through the code, we will see the SiteContext
and CurrentShoppingCartService
properties are populated with the values we would expect.
Pretty nice! ๐โจ
Resolving Our Types Outside of Pages or Controls
What should we do when we want to resolve our types from within other parts of our Kentico application, like a scheduled task?
In this case we can't use DI because Kentico creates the instances of the custom scheduled task classes. Unlike with the Autofac Web Forms integration package, we don't have a way to tell Kentico to resolve constructor parameters or class property values from an IoC Container. ๐ค
Kentico folks: If there is a way to customize creation of these types, I'd love to know!
What we can do instead is use the Autofac container as a Service Locator.
Normally, service location would be considered an anti-pattern, but when you are only provided lemons you put on gloves, eye protection, and then start juicing! ๐ฅค
To demo this, let's create a custom scheduled task that returns the current requesting user's shopping cart Id.
public class GetCurrentUserShoppingCartIdTask : ITask
{
private readonly ISiteContext siteContext;
private readonly ICurrentShoppingCartService currentShoppingCartService;
public GetCurrentUserShoppingCartIdTask()
{
// Get the instance of our application
var application = HttpContext
.Current
.ApplicationInstance as IContainerProviderAccessor;
// Get the global container from the application
var container = application
.ContainerProvider
.ApplicationContainer;
// Resolve our dependencies
siteContext = container.Resolve<ISiteContext>();
currentShoppingCartService = container.Resolve<ICurrentShoppingCartService>();
}
public string Execute(TaskInfo task)
{
string siteName = siteContext.SiteName;
var cart = currentShoppingCartService.GetCurrentShoppingCart(
MembershipContext.AuthenticatedUser,
siteName);
return cart.ShoppingCartID.ToString();
}
}
We take dependencies on the two types we've already seen (ISiteContext
and ICurrentShoppingCartService
), but instead of getting them through the class constructor or public properties, we resolve them directly from the container.
The
IContainerProviderAccessor application
above is the instance of the application created from ourGlobal
class inGlobal.asax.cs
and theILifetimeScope container
is the same thing that Web Forms uses to get instances for property injection with our customPage
andUserControl
types.
So, we resolve our dependencies in the constructor of our class and then use them in the Execute()
method.
There's a lot of glue code in that constructor. Let's move some to an extension method.
public static class TaskExtensions
{
public static ILifetimeScope GetContainer(this ITask task, HttpContext context)
{
// Get the instance of our application
var application = context
.ApplicationInstance as IContainerProviderAccessor;
// Get the global container from the application
return application
.ContainerProvider
.ApplicationContainer;
}
}
Now our constructor looks as follows:
public GetCurrentUserShoppingCartIdTask()
{
var container = this.GetContainer(HttpContext.Current);
// Resolve our dependencies
siteContext = container.Resolve<ISiteContext>();
currentShoppingCartService = container.Resolve<ICurrentShoppingCartService>();
}
The
GetContainer()
extension method can only be called on anITask
which prevents the Service Locator pattern from leaking into the rest of our codebase! ๐
My final recommendation, which I won't show here, would be to move the business logic of the Execute()
method into another class that follows our patterns and practices from MVC โ DI and the Single Reponsibility Principle.
This new class will take ISiteContext
and ICurrentShoppingCartService
as constructor dependencies and have a simple API (only a couple of methods at most) to process the scheduled task.
We can then use the IoC Container to create an instance of it instead of its dependencies. Then we call its methods in the ITask.Execute()
method.
Wrap Up!
We reviewed what Inversion of Control is, how it enables Dependency Injection, and how ASP.NET MVC uses these concepts to help developers create an application. ๐ง
Although ASP.NET Web Forms doesn't support either of these concepts out of the box, we can enhance the framework with Autofac to allow for Property Injection of the Page
and UserControl
types. ๐
We also took a look at the scenario where we want to resolve our registered dependencies inside Kentico types that we cannot integrate with our IoC Container. ๐ค
Instead, we said YOLO!, and used a Service Locator pattern to resolve our types. ๐
I hope you found this walkthrough helpful and the explanations clear!
This is my first blog post since switching my blogging from Medium to DEV, and I'm looking forward to writing more in the future.
For more on Kentico Design Patterns, checkout my series or the Kentico tag here on DEV.
Top comments (1)
Hi Sean
Thank you very much for this article. Really appreciate it.