Decorating system services
The decorator pattern allows you to add new functionality to an existing object without altering its structure. There are two main ways the decorator pattern can be implemented in Xperience.
Decorator pattern considerations
This section provides general guidelines and recommendations for when to use the decorator pattern.
Consider using the decorator pattern when:
- extending an existing implementation with new logic while maintaining the original behavior
- replacing the implementation of specific members, leaving others unchanged
Avoid using the decorator pattern when:
- completely replacing the implementation of a service class – there is no point in decorating a class if you don’t plan on reusing any of the logic
Decorate services via dependency injection
Xperience service classes registered within the ASP.NET Core application’s Inversion of Control (IoC) container can be decorated by registering a new implementation of the same service (interface) with a constructor dependency on itself. The service container is capable of resolving the previous implementation of the service from within the new one, allowing you to add custom logic to the service’s members.
The benefits of this approach are:
- the service maintains its behavior, but with added custom logic (e.g., additional logging)
- the new implementation gets used in place of the old one automatically across the system
- to revert back to the previous implementation, you only need to stop registering the new implementation into the container
- opens
sealed
andinternal
service implementations for modification - the IoC container always resolves the previous implementation of a service (last registered relative to the implementation being resolved), allowing you to chain multiple decorators
Registering multiple decorators for a service from the same assembly
Service registrations from within the same assembly are nondeterministic – the order of registration can be different every time the application starts. If you need to chain multiple decorators from within a single assembly, you can do so via a custom code-only module class. Override the module’s OnPreInit
method, and call Service.Use<TService, TImplementation>()
for each of your implementation in the order of dependency. This ensures a deterministic order for the IoC container.
Example
The following example demonstrates decoration via dependency injection:
Create a new implementation of the desired service. This example modifies
IEventLogService
to add more information to the application’s event logging.- For best practices about integrating custom code, see Adding custom assemblies.
Inject the same service via a constructor dependency. When instantiating your service, the container resolves its previous implementation.
using CMS.Core; public class EventLogServiceCustomized : IEventLogService { private readonly IEventLogService eventLogService; // Resolves to the previous implementation of the service public EventLogServiceCustomized(IEventLogService eventLogService) { this.eventLogService = eventLogService; } }
Implement the methods prescribed by the interface. To keep the original behavior, call the equivalent methods from the injected service within the corresponding method implementations. Add custom logic as required.
using CMS.Core; using CMS.Helpers; public class EventLogServiceCustomized : IEventLogService { private readonly IEventLogService eventLogService; public EventLogServiceCustomized(IEventLogService eventLogService ) { this.eventLogService = eventLogService; } public void LogEvent(EventLogData eventLogData) { // Added custom logic that modifies the logged event data eventLogData.EventDescription += $" Action was performed from {RequestContext.UserHostAddress}"; // Call to the previous implementation of the method to do the actual logging eventLogService.LogEvent(eventLogData); } }
using Microsoft.AspNetCore.Http; using CMS.Core; public class EventLogServiceCustomized : IEventLogService { private readonly IEventLogService eventLogService; private readonly IHttpContextAccessor httpContextAccessor; public EventLogServiceCustomized(IEventLogService eventLogService, IHttpContextAccessor httpContextAccessor) { this.eventLogService = eventLogService; this.httpContextAccessor = httpContextAccessor; } public void LogEvent(EventLogData eventLogData) { // Added custom logic that modifies the logged event data eventLogData.EventDescription += $" Action was performed from {httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString()}"; // Call to the previous implementation of the method to do the actual logging eventLogService.LogEvent(eventLogData); } }
Register the service implementation within the application’s IoC container via the
RegisterImplementation
attribute.using CMS; [assembly: RegisterImplementation(typeof(IEventLogService), typeof(EventLogServiceCustomized))] public class EventLogServiceCustomized : IEventLogService
The system now uses your service implementation in place of the previous one. The service’s core behavior remains unchanged, but it also executes the additional logic when used.
Decorate services via inheritance
Xperience services with public implementations can be decorated via inheritance. This form of customization is only possible for services that
- expose their implementation
- contain virtual members (overridable from derived classes)
Example
The following example demonstrates decoration via inheritance:
Create a new implementation that inherits from the default implementation of the desired service. This example modifies
EventLogService
(the implementation ofIEventLogService
) to add more information to the application’s event logging.- For best practices about integrating custom code, see Adding custom assemblies.
using CMS.EventLog; public class EventLogServiceCustomized : EventLogService { }
Override the virtual members that you wish to decorate. To keep the original behavior, call their base implementation from within the overridden member. Add custom logic as required.
using CMS.EventLog; using CMS.Helpers; public class EventLogServiceCustomized : EventLogService { public override void LogEvent(EventLogData eventLogData) { // Added custom logic that modifies the logged event data eventLogData.EventDescription += $" Action was performed from {RequestContext.UserHostAddress}"; // Call to the default implementation of the method to do the actual logging base.LogEvent(eventLogData); } }
using CMS.EventLog; public class EventLogServiceCustomized : EventLogService { private readonly IHttpContextAccessor httpContextAccessor; public EventLogServiceCustomized(IHttpContextAccessor httpContextAccessor) { this.httpContextAccessor = httpContextAccessor; } public override void LogEvent(EventLogData eventLogData) { // Added custom logic that modifies the logged event data eventLogData.EventDescription += $" Action was performed from {httpContextAccessor.HttpContext.Connection.RemoteIpAddress}"; // Call to the default implementation of the method to do the actual logging base.LogEvent(eventLogData); } }
Register the new implementation using the
RegisterImplementation
assembly attribute.using CMS; [assembly: RegisterImplementation(typeof(IEventLogService), typeof(EventLogServiceCutomized))] public class EventLogServiceCustomized : EventLogService
The system now uses your service implementation in place of the default one. The service’s core behavior remains unchanged, but it also executes the additional logic when used.