Decorate 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 and internal 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 deterministic ordering for the IoC container.

Example

The following example demonstrates decoration via dependency injection:

  1. Create a new implementation of the desired service. This example modifies IEventLogService to add more information to the application’s event logging.

  2. 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;
         }
     }
    
     
  3. 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 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);
         }
     }
    
     
  4. Register the service implementation within the application’s IoC container via the RegisterImplementation attribute.

    
    
     using Microsoft.AspNetCore.Http;
    
     using CMS;
     using CMS.Core;
    
     [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:

  1. Create a new implementation that inherits from the default implementation of the desired service. This example modifies EventLogService (the implementation of IEventLogService) to add more information to the application’s event logging.

    
    
     using CMS.EventLog;
    
     public class EventLogServiceCustomized : EventLogService
     {    
     }
    
     
  2. 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;
    
     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);
         }
     }
    
     
  3. Register the new implementation using the RegisterImplementation assembly attribute.

    
    
     using CMS;
     using CMS.EventLog;
    
     [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.