HomeDev GuideAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubNuGetDev CommunitySubmit a ticketLog In
GitHubNuGetDev CommunitySubmit a ticket

Moderation workflows

Explains how to work with the Optimizely Community API's moderation workflows.

In the Optimizely Community API, your moderation strategy is represented as a workflow. The platform provides a simple set of tools for defining these workflows.

A workflow is comprised of:

  • A set of states – For example,Pending, In Review, Rejected, Published.
  • Actions – For example Accept, Ignore, Reject, Publish.
  • Transitions – The combination of two states (origin and destination) and an action causes the transition. For example, if an item's state is Pending (origin state), a reviewer accepts the request (action), changing its state to In Review (destination state).

Define a workflow

To define a workflow in the Optimizely Community API, use the Workflow class.

An instance of the Workflow class is constructed with a name, an initial state (represented by the WorkflowState class), and a collection of transitions (represented by the WorkflowTransition class). Together, they represent a complete workflow.

Consider the example workflow in the diagram above. An instance of the Workflow class can be constructed to represent it as follows:

var workflow = new EPiServer.Social.Moderation.Workflow(
  "Example Workflow",
  new List<WorkflowTransition> {
    new WorkflowTransition(new WorkflowState("Pending"), new WorkflowState("In Review"), new WorkflowAction("Accept")),
    new WorkflowTransition(new WorkflowState("Pending"), new WorkflowState("Rejected"), new WorkflowAction("Ignore")),
    new WorkflowTransition(new WorkflowState("In Review"), new WorkflowState("Published"), new WorkflowAction("Publish")),
    new WorkflowTransition(new WorkflowState("In Review"), new WorkflowState("Rejected"), new WorkflowAction("Reject"))
  },
  new WorkflowState("Pending"));

The Workflow class exposes several methods to support the further development of moderation features. The methods let you enforce the workflow process. They also help you construct the user experiences involved in workflow management.

  • ActionsFor – For any state, this method lets you discover available actions governed by the workflow.
var availableActions = workflow.ActionsFor(new WorkflowState("Pending"));

In this method, you provide a state (Pending in the example above) that returns available actions according to the workflow's definition. Continuing the above example, when requesting actions for an entity in the Pending state, the method returns the two available actions: Accept and Ignore.

You might use these actions to create a UI with buttons that illustrate actions that may be taken on an entity under moderation. For example, you are moderating a group membership request whose state is In Review. To draw a UI that shows possible actions for In Review requests, leverage the ActionsFor method to discover available actions.

  • Transition – For an action occurring on an entity in a particular state (origin), this method identifies the resulting state (destination).
var destinationState = workflow.Transition(new WorkflowState("Pending"), new WorkflowAction("Accept"));

In this method, you provide an origin state (WorkflowState) and an action (WorkflowAction). The method returns the destination state (WorkflowState). Continuing the above example, given an origin state of Pending and an action of Accept, a destination state of In Review is returned.

📘

Note

The method simply identifies the resulting state. No state change is persisted as a result of invoking this method.

If an action is specified which is not available in the given state, an InvalidActionOnWorkflowItemException is thrown. To discover the list of available actions, leverage the ActionsFor method.

This method is leveraged in developing features managing the transition of entities through a moderation workflow.

  • HasState – Indicates whether or not the specified state exists within this workflow.
var hasState = workflow.HasState(new WorkflowState("Pending"));

In this method, you provide a state (Pending in the example above), and it returns a boolean indicating whether that state exists in this workflow.

Manage workflows

In the Optimizely Community API, workflows are managed through a service implementing the IWorkflowService interface. The workflow service provides the ability to persist and retrieve workflows you define.

  • Accessing the workflow service
  • Adding a workflow
  • Retrieving a workflow
  • Removing a workflow
  • Transition sessions

This service lets you persist, retrieve, and remove workflows you define.

Access the workflow service

If the Moderation feature is installed on an Optimizely CMS site with the site integration package, get an instance of this service from the inversion of control (IoC) container.

Example:

var workflowService = EPiServer.ServiceLocation.ServiceLocator.Current.GetInstance<IWorkflowService>();

If the feature is installed on a non-Optimizely CMS site, get an instance of this service from the default factory class provided in the package.

Example:

var factory = new EPiServer.Social.Moderation.Factories.DefaultWorkflowServiceFactory();
var workflowService = factory.Create();

Add a workflow

You can save a workflow that you defined with the Add(Workflow) method of IWorkflowService. This method accepts an instance of the Workflow class and returns a reference to an instance of this class, populated with additional, system-generated data (for example, a unique ID).

IWorkflowService workflowService;
Workflow workflow;

// ...
var addedWorkflow = workflowService.Add(workflow);

To add a workflow, the transitions cannot contain duplicate entries with identical combinations of From and To states. Also, the initial state of the added workflow must match the From or the To state of at least one of the workflow transitions.

The above example invokes the request to add a synchronous workflow. Below is an example of adding an asynchronous workflow using the asynchronous overload with C#'s async and await keywords.

private async Task<Workflow> AddWorkflowAsync(IWorkflowService workflowService) {
  Workflow workflow;

  // ...
  var addWorkflowTask = workflowService.AddAsync(workflow);

  //Do other application specific work in parallel while the task executes.
  //....

  //Wait until the task runs to completion.
  var addedWorkflow = await addWorkflowTask;
  return addedWorkflow;
}

Retrieve a workflow

To retrieve a specific instance of a Workflow that was previously added through the platform, use the Get(WorkflowId) method. This method accepts an instance of the WorkflowId class, which identifies the particular Workflow to be retrieved. It returns the instance of the Workflow class corresponding to that identifier.

IWorkflowService workflowService;
            
//...
            
// Construct a WorkflowId corresponding to the desired workflow
var workflowId = WorkflowId.Create("...");
var workflow = workflowService.Get(workflowId);

If the requested workflow cannot be found, a WorkflowDoesNotExistException is thrown.

The above example invokes the request to retrieve a workflow synchronously. An example of retrieving a workflow asynchronously using the asynchronous overload with C#'s async and await keywords is described below.

private async Task<Workflow> GetWorkflowAsync(IWorkflowService workflowService) {
  //...

  // Construct a WorkflowId corresponding to the desired workflow
  var workflowId = WorkflowId.Create("...");
  var getWorkflowTask = workflowService.GetAsync(workflowId);

  //Do other application specific work in parallel while the task executes.
  //....

  //Wait until the task runs to completion.
  var workflow = await getWorkflowTask;
  return workflow;
}

You can retrieve a collection of workflows that were previously added through the platform with the Get(Criteria<WorkflowFilter>) method. This method accepts an instance of Criteria, which contains specifications necessary to retrieve the desired workflows.

The Filter property of the Criteria<WorkflowFilter> class accepts an instance of the WorkflowFilter class. This class contains specifications that let you refine the result set of workflows you want to retrieve.

IWorkflowService workflowService;

// ...

var criteria = new Criteria<WorkflowFilter> {
  Filter = new WorkflowFilter {
    Name = "My Workflow"
  }
};
ResultPage<Workflow> pageOfWorkflows = workflowService.Get(criteria);

In the example above, Criteria is defined as requesting a result page of workflows that share the name "My Workflow." The criteria are provided to the workflow service and a ResultPage is returned with a collection of workflows that match the criteria.

In the above examples, the request to retrieve workflows is invoked synchronously. An example of retrieving workflows asynchronously using the asynchronous overload with C#'s async and await keywords is described below.

private async Task<ResultPage<Workflow>> GetWorkflowsAsync(IWorkflowService workflowService) {
  // ...
  var criteria = new Criteria<WorkflowFilter> {
    Filter = new WorkflowFilter {
      Name = "My Workflow"
    }
  };
  var getWorkflowTask = workflowService.GetAsync(criteria);

  //Do other application specific work in parallel while the task executes.
  //....

  //Wait until the task runs to completion.
  var pageOfWorkflows = await getWorkflowTask;
  return pageOfWorkflows;
}

See Criteria in Discover the platform.

Remove a workflow

To remove a specific instance of workflow previously added through the platform, use the Remove(WorkflowId) method. This method accepts an instance of the WorkflowId class, which identifies the workflow to be removed. The result is the deletion of the workflow corresponding to that ID.

IWorkflowService workflowService;
            
//...
            
// Construct a WorkflowId corresponding to the desired workflow
var workflowId = WorkflowId.Create("...");
workflowService.Remove(workflowId);

You cannot remove a workflow if related WorkflowItems exist. So, remove WorkflowItems related to a workflow before attempting to remove it.

The above example invokes the request to remove a workflow synchronously. Below is an example of removing a workflow asynchronously using the asynchronous overload with C#'s async and await keywords.

private async Task RemoveWorkflowAsync(IWorkflowService workflowService) {
  //...

  // Construct a WorkflowId corresponding to the desired workflow
  var workflowId = WorkflowId.Create("...");
  var removeWorkflowTask = workflowService.RemoveAsync(workflowId);

  //Do other application specific work in parallel while the task executes.
  //....

  //Wait until the task runs to completion.
  await removeWorkflowTask;
}

Transition sessions

The moderation schemes of applications vary greatly. Some are simple, with few operations to transition their targets from state to state. Others are more complex, involving many operations and custom business logic. Regardless of the complexity of your moderation scheme, its integrity is important. Your application may receive requests concurrently, whether on the same server or distributed across multiple servers.

For example, consider a scenario where two moderators within your application attempt to simultaneously moderate a request to join a group. They see the same request in the same state. One approves the request, while the other rejects it. If the application allows both moderators to succeed, the request will likely end up in an unexpected state.

Your application needs to ensure that moderation occurs in a controlled manner. To prevent situations such as the one described above, the application needs the opportunity to obtain exclusive access to a target of moderation. The application can perform the operations, queries, and evaluations necessary to transition that target through a workflow. The Optimizely Community API lets an application request exclusive access to a target of moderation within a particular workflow such that the application can execute its logic without jeopardizing the integrity of the target's state.

Begin a transition session

To request exclusive access to a target of moderation, use the BeginTransitionSession(WorkflowId, Reference) method of IWorkflowService. This method accepts:

  • An instance of the WorkflowId class, which identifies the Workflow being used for moderation.
  • An instance of the Reference class, which identifies the target under moderation. This is the target to which exclusive access is desired.

If exclusive access is acquired, a TransitionSessionToken is returned. This token is used to identify your session when you add a WorkflowItem for the target.

If exclusive access is not acquired, a TransitionSessionDenied exception is thrown. Access is denied if:

  • Exclusive access has already been granted for the identified target in the specified workflow.
  • The specified workflow does not exist.

The exception should be handled within the application if competing requests for access to the identified target are made.

📘

Note

A target need not have a pre-existing moderation history to request exclusive access to it. Exclusive access can also be granted to a target entering moderation for the first time.

📘

Note

While a transition session grants a client exclusive access to a target in a workflow, it is not a transaction. The operations performed in the course of a transition session are not atomic and are not reverted if an error occurs. It is important to account for the possibility that an error may occur within the application and handle it accordingly.

End a transition session

When the application completes its operation, it should relinquish its exclusive access to the target so that other clients may obtain it. To end a transition session, use the EndTransitionSession(TransitionSessionToken) or EndTransitionSession(WorkflowId, Reference) method of IWorkflowService.

The EndTransitionSession(TransitionSessionToken) method accepts:

  • An instance of TransitionSessionToken identifying the specific session to end. Provide the token that the application received when it called the BeginTransitionSession method.

The EndTransitionSession(WorkflowId, Reference) method accepts:

  • An instance of WorkflowId, which identifies the Workflow being used for moderation.
  • An instance of Reference, which identifies the target under moderation.
Example of a transition session

In the implementation of a moderation strategy for an application, a transition session typically involves these actions:

  1. Retrieve the workflow, under which the target is moderated.
  2. Begin a transition session for the target in that workflow.
  3. Retrieve the current state of that target in the workflow.
  4. Given the moderation action to be applied, use the workflow to determine the next state of that target in the workflow.
  5. Transition the target to a new state by adding a WorkflowItem.
  6. End the transition session.

The example below demonstrates the implementation of a moderation strategy, which leverages transition sessions to ensure that its targets are moderated in a protected manner.

public class MyModerationStrategy {
  private readonly IWorkflowService workflowService;
  private readonly IWorkflowItemService workflowItemService;
  private readonly Workflow moderationWorkflow;
  public MyModerationStrategy(IWorkflowService workflowService, IWorkflowItemService workflowItemService, Workflow moderationWorkflow) {
    this.workflowService = workflowService;
    this.workflowItemService = workflowItemService;
    this.moderationWorkflow = moderationWorkflow;
  }

  /// <summary>
  /// Moderates the specified target, transitioning it into a new
  /// workflow state by applying the specified action.
  /// </summary>
  /// <param name="targetToActUpon">Membership request item to act upon</param>
  /// <param name="intendedAction">Moderation action to be taken</param>
  public void Moderate(Reference targetToActUpon, WorkflowAction intendedAction) {
    TransitionSessionToken sessionToken = null;
    try {
      // Initiate an exclusive transition session for the request
      // which is being moderated.

      sessionToken = this.workflowService.BeginTransitionSession(moderationWorkflow.Id, targetToActUpon);

      // Determine the current state of the target by retrieving
      // its most recent moderation record.

      var currentModerationRecord = this.GetCurrentModerationState(targetToActUpon);

      // Determine the next state of the target, given the intended
      // action, and construct a corresponding moderation record.

      WorkflowItem nextModerationRecord = GetTransitionedModerationState(targetToActUpon, currentModerationRecord.State, intendedAction);

      // 

      this.workflowItemService.Add(nextModerationRecord, sessionToken);
    } catch (InvalidActionOnWorkflowItemException) {
      // The workflow has indicated that the intended action
      // is not possible given the target's current state. Handle
      // the exception to inform the user, etc.
    } catch (TransitionSessionDeniedException) {
      // Another client has already obtained exclusive
      // access to moderate the. Handle the exception 
      // to inform the user, etc.                
    } finally {
      // Ensure that exclusive access to the target is relinquished
      // when moderation is complete.
      if (sessionToken != null) {
        this.workflowService.EndTransitionSession(sessionToken);
      }
    }
  }

  private WorkflowItem GetTransitionedModerationState(Reference target, WorkflowState currentState, WorkflowAction intendedAction) {
    // Leverage the workflow to determine what the
    // resulting state of the item will be upon taking 
    // the specified action.

    // Example: Current State: "Pending", Action: "Approve" => Transitioned State: "Approved"

    var newState = this.moderationWorkflow.Transition(currentState, intendedAction);
    return new WorkflowItem(this.moderationWorkflow.Id, newState, target);
  }

  /// <summary>
  /// Retrieves a WorkflowItem describing the current moderation state of
  /// the identified target.
  /// </summary>
  /// <param name="target">Reference identifying the request under moderation</param>
  /// 
  /// <returns>WorkflowItem describing the current moderation state of a target</returns>
  private WorkflowItem GetCurrentModerationState(Reference target) {
    Criteria<WorkflowItemFilter> criteria = new Criteria<WorkflowItemFilter> {
      Filter = new WorkflowItemFilter {
        Workflow = this.moderationWorkflow.Id,
          Target = target,
          ExcludeHistoricalItems = true
      },
      PageInfo = new PageInfo {
        PageOffset = 0, PageSize = 1, CalculateTotalCount = false
      }
    };

    return this.workflowItemService.Get(criteria).Results.FirstOrDefault();
  }
}

In the above moderation example, the requests to begin and end the transition are invoked synchronously. An example of performing these activities asynchronously using the asynchronous overload with C#'s async and await keywords is described below.

/// <summary>
/// Moderates the specified target, transitioning it into a new
/// workflow state by applying the specified action.
/// </summary>
/// <param name="targetToActUpon">Membership request item to act upon</param>
/// <param name="intendedAction">Moderation action to be taken</param>
public async Task ModerateAsync(Reference targetToActUpon, WorkflowAction intendedAction) {
  try {
    // Initiate an exclusive transition session for the request
    // which is being moderated.
    sessionToken = await this.workflowService.BeginTransitionSessionAsync(moderationWorkflow.Id, targetToActUpon);

    // Determine the current state of the target by retrieving
    // its most recent moderation record.
    var currentModerationRecord = this.GetCurrentModerationState(targetToActUpon);

    // Determine the next state of the target, given the intended
    // action, and construct a corresponding moderation record.
    WorkflowItem nextModerationRecord = GetTransitionedModerationState(targetToActUpon, currentModerationRecord.State, intendedAction);
    var addWorkflowItemTask = this.workflowItemService.AddAsync(nextModerationRecord, sessionToken);

    //Do other application specific work in parallel while the task executes.
    //....

    //Wait until the addWorkflowItemTask runs to completion.
    await addWorkflowItemTask;
  } catch (InvalidActionOnWorkflowItemException) {
    // The workflow has indicated that the intended action
    // is not possible given the target's current state. Handle
    // the exception to inform the user, etc.
  } catch (TransitionSessionDeniedException) {
    // Another client has already obtained exclusive
    // access to moderate the. Handle the exception 
    // to inform the user, etc.    
  } finally {
    // Ensure that exclusive access to the target is relinquished
    // when moderation is complete.
    if (sessionToken != null) {
      // Wait until the end transition session task runs to completion.
      await this.workflowService.EndTransitionSessionAsync(sessionToken);
    }
  }
}

Extend workflows with composites

You may need to associate additional information with a workflow to support your application's use cases. For example, if a workflow is intended to support the moderation of a group, you might store the group's identifier to interpret their relationship dynamically.

Like other Optimizely Community API features, you can extend a workflow with your design data by creating a composite. See Composites in Discover the platform.

This section explains the workflow service's synchronous and asynchronous APIs that support extending workflow items with composites.

Add a composite workflow

To save a composite workflow that you defined, use the Add<TExtension>(Workflow,TExtension) method of IWorkflowService. This method accepts an instance of the Workflow class and an instance of TExtension. It returns an instance of Composite\<Workflow,TExtension>.

Consider the following class, which represents a sample of extension data:

public class MyWorkflowExtension {
  // ...
}

In the example below, a simple workflow is composed with an instance of this extension class:

IWorkflowService workflowService;

// ...
Workflow workflow = new Workflow("Simple Workflow", new List<WorkflowTransition> {
    new WorkflowTransition(new WorkflowState("Pending"), new WorkflowState("Approved"), new WorkflowAction("Approve")),
    new WorkflowTransition(new WorkflowState("Pending"), new WorkflowState("Rejected"), new WorkflowAction("Reject"))
  },
  new WorkflowState("Pending"));

MyWorkflowExtension extension = new MyWorkflowExtension {
  // ...
};

var compositeWorkflow = workflowService.Add(workflow, extension);

The above example invokes the request to add a synchronous workflow. Below is an example of adding an asynchronous workflow using the asynchronous overload with C#'s async and await keywords.

private async Task<Composite<Workflow, MyWorkflowExtension>> AddWorkflowAsync(IWorkflowService workflowService) {
  // ...
  Workflow workflow = new Workflow("Simple Workflow", new List<WorkflowTransition> {
      new WorkflowTransition(new WorkflowState("Pending"), new WorkflowState("Approved"), new WorkflowAction("Approve")),
      new WorkflowTransition(new WorkflowState("Pending"), new WorkflowState("Rejected"), new WorkflowAction("Reject"))
    },
    new WorkflowState("Pending"));

  MyWorkflowExtension extension = new MyWorkflowExtension {
    // ...
  };

  var addWorkflowTask = workflowService.AddAsync(workflow, extension);

  //Do other application specific work in parallel while the task executes.
  //....

  //Wait until the task runs to completion.
  var compositeWorkflow = await addWorkflowTask;
  return compositeWorkflow;
}

Retrieve a composite workflow

To retrieve an instance of a composite workflow that was previously added through the platform, use the Get<TExtension>(WorkflowId) method. This method accepts an instance of the WorkflowId class, which identifies the workflow to be retrieved. It returns the instance of the Composite\<Workflow,TExtension> class corresponding to that identifier.

IWorkflowService workflowService;
//...
            
// Construct a WorkflowId corresponding to the desired workflow
var workflowId = WorkflowId.Create("...");
var compositeWorkflow = workflowService.Get<MyWorkflowExtension>(workflowId);

If a composite workflow with the specified ID and extension type cannot be found, a WorkflowDoesNotExistException is thrown.

The above example invokes the request to retrieve a workflow synchronously. An example of retrieving a workflow asynchronously using the asynchronous overload with C#'s async and await keywords is described below.

private async Task<Composite<Workflow, MyWorkflowExtension>> GetWorkflowAsync(IWorkflowService workflowService) {
  // ...

  // Construct a WorkflowId corresponding to the desired workflow
  var workflowId = WorkflowId.Create("...");
  var getWorkflowTask = workflowService.GetAsync<MyWorkflowExtension>(workflowId);

  //Do other application specific work in parallel while the task executes.
  //....

  //Wait until the task runs to completion.
  var compositeWorkflow = await getWorkflowTask;
  return compositeWorkflow;
}

To retrieve a collection of composite workflows, which were previously added through the platform, use the Get<TExtension>(CompositeCriteria\<WorkflowFilter, TExtension>) method. This method accepts an instance of CompositeCriteria\<WorkflowFilter,TExtension>, which contains the specifications necessary to retrieve the desired workflows.

The Filter property of the CompositeCriteria\<WorkflowFilter,TExtension> class accepts an instance of the WorkflowFilter class. This class contains specifications that let you refine the result set of workflows you want to retrieve.

The ExtensionFilter property of the CompositeCriteria\<WorkflowFilter,TExtension> class accepts a FilterExpression that lets you specify a Boolean expression to refine the result set further by values represented within your extension data. See Composite Criteria and Filtering Composites in Discover the platform.

Consider the following class, which represents a sample of extension data:

public class MyWorkflowExtension {
  public string Department {
    get;
    set;
  }
}

In the example below, a page of workflows composed with MyWorkflowExtension is retrieved, where the Department property of that extension data has a value of Finance.

IWorkflowService workflowService;

// ...
var criteria = new CompositeCriteria<WorkflowFilter,
  MyWorkflowExtension> {
    ExtensionFilter = FilterExpressionBuilder<MyWorkflowExtension> .Field(e => e.Department).EqualTo("Finance")
  };
var resultPage = workflowService.Get(criteria);

In the above example, the request to retrieve workflows is invoked synchronously. An example of retrieving workflows asynchronously using the asynchronous overload with C#'s async and await keywords is described below.

private async Task<ResultPage<Composite<Workflow, MyWorkflowExtension>>> GetWorkflowsAsync(IWorkflowService workflowService) {
  // ...
  var criteria = new CompositeCriteria<WorkflowFilter,
    MyWorkflowExtension> {
      ExtensionFilter = FilterExpressionBuilder<MyWorkflowExtension> .Field(e => e.Department).EqualTo("Finance")
    };
  var getWorkflowsTask = workflowService.GetAsync(criteria);

  //Do other application specific work in parallel while the task executes.
  //....

  //Wait until the task runs to completion.
  var resultPage = await getWorkflowsTask;
  return resultPage;
}

Best practices for extending workflows in relationships

In scenarios where your application must dynamically interpret the relationship between a workflow and some other entity, leverage composites to persist the data necessary to resolve that association. For example, if a workflow is intended to moderate group membership, define extension data that captures the group's ID and stores that information with the workflow as a composite.

Best practices for acting on transitions

Often, your application must execute business logic due to a transition within a workflow. You might implement stateless strategy methods within the extension data that you associate with your workflow. This provides easy access to logic that can be dynamically executed upon committing a transition.