Declarative Constraints

Declarative constraints provide a powerful method of validation for an activity and its relationships with other activities. Constraints are configured for an activity during the authoring process, but additional constraints can also be specified by the workflow host. This topic provides an overview of using declarative constraints to provide activity validation.

Using Declarative Constraints

A constraint is an activity that contains validation logic. This constraint activity can be authored in code or in XAML. After a constraint activity is created, activity authors add this constraint to the Constraints property of the activity to validate, or they use the constraint to provide additional validation by using the AdditionalConstraints property of a ValidationSettings instance. The validation logic can consist of simple validations such as validating an activity’s metadata, but it can also perform validation that takes into account the relationship of the current activity to its parent, children, and sibling activities. Constraints are authored by using the Constraint<T> activity, and several additional validation activities are provided to assist with the creation of validation errors and warnings and to provide information about related activities in the workflow.

AssertValidation and AddValidationError

The AssertValidation activity evaluates the expression referenced by its Assertion property, and if the expression evaluates to false, a validation error or warning is added to the ValidationResults. The Message property describes the validation error and the IsWarning property indicates whether the validation failure is an error or a warning. The default value for IsWarning is false.

In the following example, a constraint is declared that returns a validation warning if the DisplayName of the activity being validated is two characters or less in length. The generic type parameter used for Constraint<T> specifies the type of activity that is validated by the constraint. This constraint uses Activity as the generic type and can be used to validate all types of activities.

public static Constraint ActivityDisplayNameIsNotSetWarning()  
{  
    DelegateInArgument<Activity> element = new DelegateInArgument<Activity>();  
  
    return new Constraint<Activity>  
    {  
        Body = new ActivityAction<Activity, ValidationContext>  
        {  
            Argument1 = element,  
            Handler = new AssertValidation  
            {  
                IsWarning = true,  
                Assertion = new InArgument<bool>(env => (element.Get(env).DisplayName.Length > 2)),  
                Message = new InArgument<string>("It is a best practice to have a DisplayName of more than 2 characters."),  
            }  
        }  
    };  
}  

To specify this constraint for an activity, it is added to the Constraints of the activity, as shown in the following example code.

public sealed class SampleActivity : CodeActivity  
{  
    public SampleActivity()  
    {  
        base.Constraints.Add(ActivityDisplayNameIsNotSetWarning());  
    }  
  
    // Activity implementation omitted.  
}  

The host could also specify this constraint for activities in a workflow by using AdditionalConstraints, which is covered in the next section.

The AddValidationError activity is used to generate a validation error or warning without requiring the evaluation of an expression. Its properties are similar to AssertValidation and it can be used in conjunction with flow control activities of a constraint such as the If activity.

Workflow Relationship Activities

Several validation activities are available that provide information about the other activities in the workflow in relation to the activity being validated. GetParentChain returns a collection of activities that contains all of the activities between the current activity and the root activity. GetChildSubtree provides a collection of activities that contains the child activities in a recursive pattern, and GetWorkflowTree gets all the activities in the workflow.

In the following example, a CreateState activity is defined. The CreateState activity must be contained within a CreateCountry activity, and the GetParent method returns a constraint that enforces this requirement. GetParent uses the GetParentChain activity in conjunction with a ForEach<T> activity to inspect the parent activities of the CreateState activity to determine if the requirement is met.

public sealed class CreateState : CodeActivity  
{  
    public CreateState()  
    {  
        base.Constraints.Add(CheckParent());  
        this.Cities = new List<Activity>();
    }  
  
    public List<Activity> Cities { get; set; }  
  
    public string Name { get; set; }
  
    static Constraint CheckParent()  
    {  
        DelegateInArgument<CreateState> element = new DelegateInArgument<CreateState>();  
        DelegateInArgument<ValidationContext> context = new DelegateInArgument<ValidationContext>();
        Variable<bool> result = new Variable<bool>();  
        DelegateInArgument<Activity> parent = new DelegateInArgument<Activity>();  
  
        return new Constraint<CreateState>  
        {
            Body = new ActivityAction<CreateState,ValidationContext>  
            {
                Argument1 = element,  
                Argument2 = context,  
                Handler = new Sequence  
                {  
                    Variables =  
                    {  
                        result
                    },  
                    Activities =  
                    {  
                        new ForEach<Activity>  
                        {
                            Values = new GetParentChain  
                            {  
                                ValidationContext = context
                            },  
                            Body = new ActivityAction<Activity>  
                            {
                                Argument = parent,
                                Handler = new If()  
                                {
                                    Condition = new InArgument<bool>((env) => object.Equals(parent.Get(env).GetType(),typeof(CreateCountry))),
                                    Then = new Assign<bool>  
                                    {  
                                        Value = true,  
                                        To = result  
                                    }  
                                }  
                            }
                        },  
                        new AssertValidation  
                        {  
                            Assertion = new InArgument<bool>(result),  
                            Message = new InArgument<string> ("CreateState has to be inside a CreateCountry activity"),
                        }  
                    }  
                }  
            }  
        };  
    }  
  
    protected override void Execute(CodeActivityContext context)  
    {  
        // not needed for the sample  
    }  
}  

Additional Constraints

Workflow host authors can specify additional validation constraints for activities in a workflow by creating constraints and adding them to the AdditionalConstraints dictionary of a ValidationSettings instance. Each item in AdditionalConstraints contains the type of activity for which the constraints apply and a list of the additional constraints for that type of activity. When validation is invoked for the workflow, each activity of the specified type, including derived classes, evaluates the constraints. In this example, the ActivityDisplayNameIsNotSetWarning constraint from the previous section is applied to all activities in a workflow.

Activity wf = new Sequence  
{  
    // Workflow Details Omitted.  
};  
  
ValidationSettings settings = new ValidationSettings()  
{  
  
    AdditionalConstraints =  
    {  
        {typeof(Activity), new List<Constraint> {ActivityDisplayNameIsNotSetWarning()}},
    }  
};  
  
// Validate the workflow.  
ValidationResults results = ActivityValidationServices.Validate(wf, settings);  
  
// Evaluate the results.  
if (results.Errors.Count == 0 && results.Warnings.Count == 0)  
{  
    Console.WriteLine("No warnings or errors");  
}  
else  
{  
    foreach (ValidationError error in results.Errors)  
    {  
        Console.WriteLine("Error in " + error.Source.DisplayName + ": " + error.Message);  
    }  
    foreach (ValidationError warning in results.Warnings)  
    {  
        Console.WriteLine("Warning in " + warning.Source.DisplayName + ": " + warning.Message);  
    }  
}  

If the OnlyUseAdditionalConstraints property of ValidationSettings is true, then only the specified additional constraints are evaluated when validation is invoked by calling Validate. This can be useful for inspecting workflows for specific validation configurations. Note however that when the workflow is invoked, the validation logic configured in the workflow is evaluated and must pass for the workflow to successfully begin. For more information about invoking validation, see Invoking Activity Validation.