Some new services were introduced in Glass Mapper 5 to make mapping Sitecore items easier than ever. In this post, I'll cover how to register all of these new services with the Sitecore dependency injection container.
The Configurator
Use this beastly configurator to register all Glass Mapper services with the Sitecore container.
public class GlassMapperConfigurator : IServicesConfigurator
{
public void Configure(IServiceCollection serviceCollection)
{
// For getting a SitecoreService for any database
serviceCollection.AddSingleton<Func<Database, ISitecoreService>>(_ => CreateSitecoreService);
// For injecting into Controllers and Web Forms
serviceCollection.AddScoped(_ => CreateSitecoreContextService());
serviceCollection.AddScoped(_ => CreateRequestContext());
serviceCollection.AddScoped(_ => CreateGlassHtml());
serviceCollection.AddScoped(_ => CreateMvcContext());
serviceCollection.AddScoped(_ => CreateWebFormsContext());
// For injecting into Configuration Factory types like pipeline processors
serviceCollection.AddSingleton<Func<ISitecoreService>>(_ => Get<ISitecoreService>);
serviceCollection.AddSingleton<Func<IRequestContext>>(_ => Get<IRequestContext>);
serviceCollection.AddSingleton<Func<IGlassHtml>>(_ => Get<IGlassHtml>);
serviceCollection.AddSingleton<Func<IMvcContext>>(_ => Get<IMvcContext>);
serviceCollection.AddSingleton<Func<IWebFormsContext>>(_ => Get<IWebFormsContext>);
}
private static ISitecoreService CreateSitecoreService(Database database)
{
return new SitecoreService(database);
}
private static ISitecoreService CreateSitecoreContextService()
{
var sitecoreServiceThunk = Get<Func<Database, ISitecoreService>>();
return sitecoreServiceThunk(Sitecore.Context.Database);
}
private static T Get<T>()
{
return ServiceLocator.ServiceProvider.GetService<T>();
}
private static IRequestContext CreateRequestContext()
{
return new RequestContext(Get<ISitecoreService>());
}
private static IGlassHtml CreateGlassHtml()
{
return new GlassHtml(Get<ISitecoreService>());
}
private static IMvcContext CreateMvcContext()
{
return new MvcContext(Get<ISitecoreService>(), Get<IGlassHtml>());
}
private static IWebFormsContext CreateWebFormsContext()
{
return new WebFormsContext(Get<ISitecoreService>(), Get<IGlassHtml>());
}
}
Check out my post on scoped services in Sitecore if you're curious why all of the services are being registered twice. While you're there, grab the AddScopedWithFuncFactory<T>
extension methods and save some code.
Patch it in like so:
<configuration>
<sitecore>
<services>
<configurator type="YourAssembly.GlassMapperConfigurator, YourAssembly" />
</services>
</sitecore>
</configuration>
If you're still on Glass Mapper 4, you can use this same configurator. Wire up ISitecoreContext
like IMvcContext
and then remove the IRequestContext
, IMvcContext
, and IWebFormsContext
registrations.
Using the Services
If you look at the configurator, you'll notice that I've registered ISitecoreService
, IRequestContext
, IMvcContext
, and IWebFormsContext
as scoped. The reasons for this are two fold:
- Each of the contexts are meant to wrap the context database. The context database is (generally) scoped to the request, so the wrapping contexts should be too. They definitely should not be singleton.
SitecoreService
implementsIDisposable
and all of the contexts depend onISitecoreService
. Sitecore automatically callsDispose
on all scoped services at the end of every request. If you use transient, you are responsible for callingDispose
at some point. Register as scoped and let Sitecore handle it for you.
Below I'll detail how to use these services in two different scenarios:
- In MVC (
Controller
s) and Web Forms (Page
s/UserControl
s). - In types instantiated by the Configuration Factory (e.g., pipeline processors).
Configuration Factory instantiated types need special attention, however. If you want to read details about why, check out my post on using scoped services with Sitecore.
ISitecoreService
ISitecoreService
is your most basic entry point into Sitecore through Glass Mapper. Usually you should be using one of the contexts to work with Glass Mapper, but if you only need the methods provided by ISitecoreService
, just use it.
The main reason I registered ISitecoreService
(and IGlassHtml
) with the container is so that the same instance of ISitecoreService
will be shared across IRequestContext
, IMvcContext
, and IWebFormsContext
if you require more than one of them in the same request.
Controller
public SampleController : Controller
{
private readonly ISitecoreService _sitecoreService;
public SampleController(ISitecoreService sitecoreService)
{
var _sitecoreService = sitecoreService ?? throw new ArgumentNullException(nameof(sitecoreService));
}
public ActionResult SampleAction()
{
// do something with _sitecoreService
return View(model);
}
}
Pretty standard--just inject ISitecoreService
.
Configuration Factory Types (e.g., Pipeline Processors)
Instead of injecting ISitecoreService
, you'll inject Func<ISitecoreService>
. ⚠️Never directly inject ISitecoreService
into a type instantiated by the Configuration Factory.⚠️ If you do inject ISitecoreService
directly, you may get exceptions like this:
Glass.Mapper.MapperException: 'Service has been disposed, cannot create object'
This also applies if you want to inject an ISitecoreService
into a service that you have registered as a singleton.
public SampleProcessor
{
private readonly Func<ISitecoreService> _sitecoreServiceThunk;
public SampleProcessor(Func<ISitecoreService> sitecoreServiceThunk)
{
_sitecoreServiceThunk = sitecoreServiceThunk ?? throw new ArgumentNullException(nameof(sitecoreServiceThunk));
}
public Process(PipelineArgs args)
{
var sitecoreService = _sitecoreServiceThunk();
// do something with sitecoreService
}
}
Here you're just injecting a method that will create the ISitecoreService
for you. You do not need to dispose of the _sitecoreServiceThunk
result, Sitecore will do that for you automatically at the end of the request.
ISitecoreService
for Any Database
Sometimes you want to use Glass Mapper to map an item from a database other than the context database, like core
. You'll do this the same way in Controller
s and types instantiated by the Configuration Factory. However, ⚠️you always need to dispose it when you're done with it⚠️; Sitecore won't handle that for you at the end of the request.
public SampleProcessor
{
private readonly BaseFactory _factory;
private readonly Func<Database, ISitecoreService> _sitecoreServiceThunk;
public SampleProcessor(BaseFactory factory, Func<Database, ISitecoreService> sitecoreServiceThunk)
{
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
_sitecoreServiceThunk = sitecoreServiceThunk ?? throw new ArgumentNullException(nameof(sitecoreServiceThunk));
}
public Process(PipelineArgs args)
{
var coreDatabase = _factory.GetDatabase("core");
using (var sitecoreService = _sitecoreServiceThunk(coreDatabase))
{
// do something with sitecoreService
}
}
}
The using
statement will automatically dispose of the ISitecoreService
for you.
Sure, you could just call new SitecoreService(coreDatabase)
on line 15. However, if Glass Mapper is updated in the future with a new way to create ISitecoreService
(e.g., through an ISitecoreServiceFactory
), it's not going to be very much fun for you to go and replace all new SitecoreService(someDatabase)
calls with SitecoreServiceFactory.Current.GetService(someDatabase)
. With the Func<Database, ISitecoreService>
injection you'll just need to update one line in the GlassMapperConfigurator
. Not to mention this is actually unit testable. Don't repeat yourself™.
If you like creating the ISitecoreService
by database name instead of Database
type, you can easily wire that up in your configurator too:
public void Configure(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<Func<string, ISitecoreService>>(_ => CreateSitecoreService));
}
private static ISitecoreService CreateSitecoreService(string databaseName)
{
return new SitecoreService(databaseName);
}
Use it in your processor like this:
public SampleProcessor
{
private readonly Func<string, ISitecoreService> _sitecoreServiceThunk;
public SampleProcessor(Func<string, ISitecoreService> sitecoreServiceThunk)
{
_sitecoreServiceThunk = sitecoreServiceThunk ?? throw new ArgumentNullException(nameof(sitecoreServiceThunk));
}
public Process(PipelineArgs args)
{
using (var sitecoreService = _sitecoreServiceThunk("core"))
{
// do something with sitecoreService
}
}
}
Then you don't need to inject BaseFactory
. Up to you.
Glass Mapper Contexts
Working with IRequestContext
, IMvcContext
, and IWebFormsContext
is really all the same. IRequestContext
you generally use when you're not inside of a Controller
or (shudder) Page
/UserControl
(i.e., Web Form)--this is what you'll most commonly be injecting into pipeline processors. IMvcContext
and IWebFormsContext
should only be injected into Controller
s or Page
/UserControl
s, respectively.
Controller
public SampleController : Controller
{
private readonly IMvcContext _mvcContext;
public SampleController(IMvcContext mvcContext)
{
var _mvcContext = mvcContext ?? throw new ArgumentNullException(nameof(mvcContext));
}
public ActionResult SampleAction()
{
// do something with _mvcContext
return View(model);
}
}
Again, in Controller
s you can just request IMvcContext
directly; no need for Func<IMvcContext>
.
Web Forms
I'm sorry if you're still using Web Forms. Hope you can move on to MVC soon. You should only use ISitecoreService
and IWebFormsContext
in Page
s and UserControl
s--don't use IMvcContext
.
To inject into a Page
:
[Sitecore.DependencyInjection.AllowDependencyInjection]
public partial class SamplePage : Page
{
private readonly IWebFormsContext _webFormsContext;
protected Sample()
{
}
public Sample(IWebFormsContext webFormsContext)
{
var _webFormsContext = webFormsContext ?? throw new ArgumentNullException(webFormsContext);
}
protected void Page_Load(object sender, EventArgs e)
{
// do something with _webFormsContext
}
}
To inject into a UserControl
:
[Sitecore.DependencyInjection.AllowDependencyInjection]
public partial class SampleControl : UserControl
{
protected SampleControl()
{
}
public SampleControl(IWebFormsContext webFormsContext)
{
var _webFormsContext = webFormsContext ?? throw new ArgumentNullException(webFormsContext);
}
protected void Page_Load(object sender, EventArgs e)
{
// do something with _webFormsContext
}
}
Note that in both instances you must have a default constructor. If you're curious why and how it works, check out this blog post. It's pretty clever.
If I recall correctly, services injected into Web Forms through controller injection are only available in Page_Load
and later. If you try to access in earlier page events, such as Page_Init
, you'll get a NullReferenceException
. Unfortunately, if you need access to a service in Page_Init
you'll just have to use service locator.
Configuration Factory Types (e.g., Pipeline Processors)
Notice that I inject Func<IRequestContext>
into the SampleProcessor
instead of IRequestContext
. ⚠️Never directly inject IRequestContext
, IMvcContext
, or IRequestContext
into a type instantiated by the Configuration Factory.⚠️
public SampleProcessor
{
private readonly Func<IRequestContext> _requestContextThunk;
public SampleProcessor(Func<IRequestContext> requestContextThunk)
{
_requestContextThunk = requestContextThunk ?? throw new ArgumentNullException(nameof(requestContextThunk));
}
public Process(PipelineArgs args)
{
var requestContext = _requestContextThunk();
// do something with requestContext
}
}
Don't forget to add the resolve="true"
attribute to your processor when you add it to your config patch.
Again, this also applies to any services you have registered as singletons that need to depend on Glass Mapper contexts.
Unit Testing
If you're using dependency injection, hopefully you're unit testing. Unit testing ISitecoreService
and the contexts is easy enough in Controller
s, so I won't cover that here. For the examples below, I'll use Moq to mock dependencies.
ISitecoreService
and Glass Mapper Contexts
Consider the following processor:
public SampleProcessor
{
private readonly Func<IRequestContext> _requestContextThunk;
public SampleProcessor(Func<IRequestContext> requestContextThunk)
{
_requestContextThunk = requestContextThunk ?? throw new ArgumentNullException(nameof(requestContextThunk));
}
public Process(PipelineArgs args)
{
var requestContext = _requestContextThunk();
// do something with requestContext
}
}
To set it up for unit testing:
public void Process_DoesSomeStuff_WhenCalled()
{
// Arrange
var mockRequestContext = new Mock<IRequestContext>();
// set up the mock RequestContext
var processor = new SampleProcessor(() => mockRequestContext.Object);
var args = new PipelineArgs();
// Act
processor.Process(args);
// Assert
// assert that stuff worked
}
Nice and easy.
ISitecoreService
with Any Database
Testing ISitecoreService
when you need to pass in any database is easy too. Consider the following processor:
public SampleProcessor
{
private readonly BaseFactory _factory;
private readonly Func<Database, ISitecoreService> _sitecoreServiceThunk;
public SampleProcessor(BaseFactory factory, Func<Database, ISitecoreService> sitecoreServiceThunk)
{
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
_sitecoreServiceThunk = sitecoreServiceThunk ?? throw new ArgumentNullException(nameof(sitecoreServiceThunk));
}
public Process(PipelineArgs args)
{
var coreDatabase = _factory.GetDatabase("core");
using (var sitecoreService = _sitecoreServiceThunk(coreDatabase))
{
// do something with sitecoreService
}
}
}
To unit test:
public void Process_DoesSomeStuff_WhenCalled()
{
// Arrange
var mockFactory = new Mock<BaseFactory>();
mockFactory.Setup(f => f.GetDatabase(It.IsAny<string>()));
var mockSitecoreService = new Mock<ISitecoreService>();
// set up the mock SitecoreService
var processor = new SampleProcessor(mockFactory, _ => mockSitecoreService.Object);
var args = new PipelineArgs();
// Act
processor.Process(args);
// Assert
// assert that stuff worked
}
Again, nice and easy.
Conclusion
You'll notice I didn't cover use cases of IGlassHtml
. I've seen it registered with dependency injection containers a lot before, but I've never actually seen a reason to use it. If you've got a good use case, let me know in the comments. Like with ISitecoreService
, my main reason to register it with the container is just to make sure that IMvcContext
and IWebFormsContext
share the same instance in case both contexts are instantiated in the same request.