Passing data between your own ContentSecuritySaving and ContentSecuritySaved event handlers in Episerver

If you want to pass information between a PublishingContent handler and a PublishedContent handler (EPiServer.Core.IContentEvents) it is quite easy as the internal code provides you with a Dictionary property on the ContentEventArgs object.

This may be useful if you need to maintain information about a property value through an entire publishing process. I.e. find out if the editor changed the value of the property.

public void My_ContentPublishingEvent(object sender, ContentEventArgs e)
{
  if (!(e.Content is IMyPage myPage))
  {
    return;
  }
  e.Items.Add("OldValue", myPage.MyBoolProperty);
}

public void My_ContentPublishedEvent(object sender, ContentEventArgs e)
{
  if (!(e.Content is IMyPage myPage))
  {
    return;
  }
  bool oldValue = (bool)e.Items["OldValue"];
  bool newValue = myPage.MyBoolProperty;

So, this is all good, but what about when someone changes access rights on one or more pages in the edit or admin modes? There is an interface with events for that as well (IContentSecurityRepository), but we cannot do the same thing between a ContentSecuritySaving handler and a ContentSecuritySaved handler as the ContentSecurityEventArg does not contain this Dictionary, nor anything like it.

Determining if security information changed on one or more Episerver pages

Until the same kind of behaviour is added to the ContentSecurityEventArg object as exists in the ContentEventArgs, we need to handle these changes differently. One solution is to wrap the Episerver ContentSecurityRepository in a decorator and pass our own argument object.

First we would extend the existing ContentSecurityCancellableEventArgs with a dictionary property in the same way that the ContentEventArgs do.

public class MyContentSecurityCancellableEventArgs : ContentSecurityCancellableEventArgs
{
  private readonly IDictionary<string, object> _items = new Dictionary<string, object>();

  public MyContentSecurityCancellableEventArgs(ContentReference contentLink, IContentSecurityDescriptor contentSecurityDescriptor, SecuritySaveType securitySaveType)
: base(contentLink, contentSecurityDescriptor, securitySaveType)
  {
  }

  public IDictionary<string, object> Items { get { return _items; } }
}

This new object may then be created and passed into the event handlers added to our decorator class below. As an example, in the Save method, we first create the new args object and then invoke decorator ContentSecuritySaving.

Episerver’s own (_inner) Save method will do everything it used to do, after which we trigger the decorator ContentSecuritySaved with our new args object.

public class ContentSecurityRepositoryDecorator : IContentSecurityRepository
{
  private readonly IContentSecurityRepository _inner;

  public event EventHandler<ContentSecurityCancellableEventArgs> ContentSecuritySaving;

  public event EventHandler<ContentSecurityEventArg> ContentSecuritySaved;

  public ContentSecurityRepositoryDecorator(IContentSecurityRepository inner)
  {
    _inner = inner ?? throw new ArgumentNullException(nameof(inner));
  }

  public virtual IContentSecurityDescriptor Get(ContentReference contentLink)
  {
    return _inner.Get(contentLink);
  }

  public virtual void Save(ContentReference contentLink, IContentSecurityDescriptor contentSecurityDescriptor, SecuritySaveType securitySaveType)
  {
    // Need to handle saving events for decorator class before invoking any Save functionality.
    ContentSecurityCancellableEventArgs myArgs = new MyContentSecurityCancellableEventArgs(contentLink, contentSecurityDescriptor, securitySaveType);
    this.ContentSecuritySaving?.Invoke(this, myArgs);
    if (myArgs.CancelAction)
    {
      throw new EPiServerCancelException(myArgs.CancelReason ?? "Saving of access rights was cancelled by an event handler");
    }

    // Episerver's own repository will handle _inner's saving and saved events in proper order, if any.
    _inner.Save(contentLink, contentSecurityDescriptor, securitySaveType);

    // Need to handle save events for decorator class after invoking all Save functionality.
    this.ContentSecuritySaved?.Invoke(this, myArgs);
  }

  public virtual void Delete(string userOrRoleName, SecurityEntityType entityUserRole)
  {
    _inner.Delete(userOrRoleName, entityUserRole);
  }
}

This will allow you to add your own handlers to the decorator, and pass information between them.

public void EasyEvents_ContentSecuritySavingEvent(object sender, ContentSecurityEventArg e)
{
  if (!(e is MyContentSecurityCancellableEventArgs args))
  {
    return;
  }
  args.Items.Add("Old...", ...);
}

public void EasyEvents_ContentSecuritySavedEvent(object sender, ContentSecurityEventArg e)
{
  if (!(e is MyContentSecurityCancellableEventArgs args))
  {
    return;
  }
  var oldStuff = args.Items["Old..."];
  // ...
}

Then it’s just a matter of letting the container know your intentions by registering it as a decorator.

[InitializableModule]
[ModuleDependency(typeof(ServiceContainerInitialization))]
public class MyInitializationModule : IConfigurableModule, IInitializableModule
{
  public void ConfigureContainer(ServiceConfigurationContext context)
  {
    context.Services.Intercept<IContentSecurityRepository>((locator, inner) => new ContentSecurityRepositoryDecorator(inner));
  }

Of course, if you do not have the need or want to bother with using events, you could just put your code inside the decorator methods instead, and skip passing information.