Loading...
Area: Episerver Community API
Applies to versions: 1 and higher

Moderation workflows

Recommendations [hide]

In the Episerver Community API, your moderation strategy is represented as a workflow. The platform provides a simple set of tools for defining these workflows. This topic explains how to work with the Episerver Community API's moderation 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, which causes the transition to occur. For example, an item's state is "Pending" (origin state), a reviewer accepts the request (action), changing its state to "In Review" (destination state).

Defining a workflow

To define a workflow in the Episerver 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 further support the 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, as governed by the workflow.
    var availableActions = workflow.ActionsFor(new WorkflowState("Pending"));

    In this method, you provide a state ("Pending" in the example above), and it returns available actions according to the workflow's definition. Continuing the above example, when requesting actions for an entity in "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.

    It is important to note that 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 the development of 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 or not that state exists in this workflow.

Managing workflows

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

This service provides the ability to persist, retrieve, and remove workflows you define.

Accessing the workflow service

If the Moderation feature is installed to an Episerver 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 to a non-Episerver 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();

Adding a workflow

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

IWorkflowService workflowService;
Workflow workflow;

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

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

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

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;
  }

Retrieving a workflow

To retrieve a specific instance of a Workflow which 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.

In the above example, the request to retrieve a workflow is invoked synchronously. An example of retrieving a worflow 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 via 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 allow you to refine the result set of workflows you wish 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 to request a result page of workflows that share the name "My Workflow". The criteria is 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;
  }

For more details on the use of criteria, including information on paging and sorting, see Criteria.

Removing 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 all WorkflowItems related to a workflow before attempting to remove it.

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

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, the integrity of that scheme is important. Your application may be receiving requests concurrently, whether it's on the same server or distributed across multiple servers.

As an example, consider a scenario where two moderators within your application attempt to moderate a request to join a group simultaneously. 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 very 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. In doing so, the application can perform the operations, queries, and evaluations necessary to transition that target through a workflow. The Episerver Community API allows an application to 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.

Moderating with transition sessions

Beginning 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 successfully 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 successfully 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, in the event that 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.

Ending 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 new 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);
              }
        }
    }

Extending 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 dynamically interpret their relationship.

Like other Episerver Community API features, you can extend a workflow with data of your design by creating a composite. (For an introduction to the concept, see Composites.)

This section explains, both, the synchronous and asynchronous APIs of the workflow service that provide support for extending workflow items with composites.

Adding a composite workflow

To save a composite workflow which 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 a new 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);

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

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;
  }

Retrieving a composite workflow

To retrieve an instance of a composite workflow which 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.

In the above example, the request to retrieve a workflow is invoked 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 further refine the result set by values represented within your extension data. (For more information on this type of filter, see Composite Criteria and Filtering Composites.)

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

Relationships

In scenarios where your application must dynamically interpret the relationship between a workflow and some other entity, leverage composites as a means of persisting 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.

Acting on transitions

Often, your application must execute business logic as a result of 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.

Do you find this information helpful? Please log in to provide feedback.

Last updated: Nov 01, 2016

Recommendations [hide]