Invoking Activity Validation
Activity validation provides a method to identify and report errors in any activity's configuration prior to its execution. Validation occurs when a workflow is modified in the workflow designer and any validation errors or warnings are displayed in the workflow designer. Validation also occurs at run time when a workflow is invoked and if any validation errors occur, an InvalidWorkflowException is thrown by the default validation logic. Windows Workflow Foundation (WF) provides the ActivityValidationServices class that can be used by workflow application and tooling developers to explicitly validate an activity. This topic describes how to use ActivityValidationServices to perform activity validation.
Using ActivityValidationServices
ActivityValidationServices has two Validate overloads that are used to invoke an activity's validation logic. The first overload takes the root activity to be validated and returns a collection of validation errors and warnings. In the following example, a custom Add
activity is used that has two required arguments.
public sealed class Add : CodeActivity<int>
{
[RequiredArgument]
public InArgument<int> Operand1 { get; set; }
[RequiredArgument]
public InArgument<int> Operand2 { get; set; }
protected override int Execute(CodeActivityContext context)
{
return Operand1.Get(context) + Operand2.Get(context);
}
}
The Add
activity is used inside a Sequence, but its two required arguments are not bound, as shown in the following example.
Variable<int> Operand1 = new Variable<int>{ Default = 10 };
Variable<int> Operand2 = new Variable<int>{ Default = 15 };
Variable<int> Result = new Variable<int>();
Activity wf = new Sequence
{
Variables = { Operand1, Operand2, Result },
Activities =
{
new Add(),
new WriteLine
{
Text = new InArgument<string>(env => "The result is " + Result.Get(env))
}
}
};
This workflow can be validated by calling Validate. Validate returns a collection of any validation errors or warnings contained by the activity and any children, as shown in the following example.
ValidationResults results = ActivityValidationServices.Validate(wf);
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: {0}", error.Message);
}
foreach (ValidationError warning in results.Warnings)
{
Console.WriteLine("Warning: {0}", warning.Message);
}
}
When Validate is called on this sample workflow, two validation errors are returned.
Error: Value for a required activity argument 'Operand2' was not supplied.
Error: Value for a required activity argument 'Operand1' was not supplied. If this workflow was invoked, an InvalidWorkflowException would be thrown, as shown in the following example.
try
{
WorkflowInvoker.Invoke(wf);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
System.Activities.InvalidWorkflowException:
The following errors were encountered while processing the workflow tree:
'Add': Value for a required activity argument 'Operand2' was not supplied.
'Add': Value for a required activity argument 'Operand1' was not supplied. For this example workflow to be valid, the two required arguments of the Add
activity must be bound. In the following example, the two required arguments are bound to workflow variables along with the result value. In this example the Result argument is bound along with the two required arguments. The Result argument is not required to be bound and does not cause a validation error if it is not. It is the responsibility of the workflow author to bind Result if its value is used elsewhere in the workflow.
new Add
{
Operand1 = Operand1,
Operand2 = Operand2,
Result = Result
}
Validating Required Arguments on the Root Activity
If the root activity of a workflow has arguments, these are not bound until the workflow is invoked and parameters are passed to the workflow. The following workflow passes validation, but an exception is thrown if the workflow is invoked without passing in the required arguments, as shown in the following example.
Activity wf = new Add();
ValidationResults results = ActivityValidationServices.Validate(wf);
// results has no errors or warnings, but when the workflow
// is invoked, an InvalidWorkflowException is thrown.
try
{
WorkflowInvoker.Invoke(wf);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
System.ArgumentException: The root activity's argument settings are incorrect.
Either fix the workflow definition or supply input values to fix these errors:
'Add': Value for a required activity argument 'Operand2' was not supplied.
'Add': Value for a required activity argument 'Operand1' was not supplied. After the correct arguments are passed, the workflow completes successfully, as shown in the following example.
Add wf = new Add();
ValidationResults results = ActivityValidationServices.Validate(wf);
// results has no errors or warnings, and the workflow completes
// successfully because the required arguments were passed.
try
{
Dictionary<string, object> wfparams = new Dictionary<string, object>
{
{ "Operand1", 10 },
{ "Operand2", 15 }
};
int result = WorkflowInvoker.Invoke(wf, wfparams);
Console.WriteLine("Result: {0}", result);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
Note
In this example, the root activity was declared as Add
instead of Activity
as in the previous example. This allows the WorkflowInvoker.Invoke
method to return a single integer that represents the results of the Add
activity instead of a dictionary of out
arguments. The variable wf
could also have been declared as Activity<int>
.
When validating root arguments, it is the responsibility of the host application to ensure that all required arguments are passed when the workflow is invoked.
Invoking Imperative Code-Based Validation
Imperative code-based validation provides a simple way for an activity to provide validation about itself, and is available for activities that derive from CodeActivity, AsyncCodeActivity, and NativeActivity. Validation code that determines any validation errors or warnings is added to the activity. When validation is invoked on the activity, these warnings or errors are contained in the collection returned by the call to Validate. In the following example, a CreateProduct
activity is defined. If the Cost
is greater than the Price
, a validation error is added to the metadata in the CacheMetadata override.
public sealed class CreateProduct : CodeActivity
{
public double Price { get; set; }
public double Cost { get; set; }
// [RequiredArgument] attribute will generate a validation error
// if the Description argument is not set.
[RequiredArgument]
public InArgument<string> Description { get; set; }
protected override void CacheMetadata(CodeActivityMetadata metadata)
{
base.CacheMetadata(metadata);
// Determine when the activity has been configured in an invalid way.
if (this.Cost > this.Price)
{
// Add a validation error with a custom message.
metadata.AddValidationError("The Cost must be less than or equal to the Price.");
}
}
protected override void Execute(CodeActivityContext context)
{
// Not needed for the sample.
}
}
In this example, a workflow is configured using the CreateProduct
activity. In this workflow, the Cost
is greater than the Price
, and the required Description
argument is not set. When validation is invoked, the following errors are returned.
Activity wf = new Sequence
{
Activities =
{
new CreateProduct
{
Cost = 75.00,
Price = 55.00
// Cost > Price and required Description argument not set.
},
new WriteLine
{
Text = "Product added."
}
}
};
ValidationResults results = ActivityValidationServices.Validate(wf);
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: {0}", error.Message);
}
foreach (ValidationError warning in results.Warnings)
{
Console.WriteLine("Warning: {0}", warning.Message);
}
}
Error: The Cost must be less than or equal to the Price.
Error: Value for a required activity argument 'Description' was not supplied.
Note
Custom activity authors can provide validation logic in an activity's CacheMetadata override. Any exceptions that are thrown from CacheMetadata are not treated as validation errors. These exceptions will escape from the call to Validate and must be handled by the caller.
Using ValidationSettings
By default, all activities in the activity tree are evaluated when validation is invoked by ActivityValidationServices. ValidationSettings allows the validation to be customized in several different ways by configuring its three properties. SingleLevel specifies whether the validator should walk the entire activity tree or only apply validation logic to the supplied activity. The default for this value is false
. AdditionalConstraints specifies additional constraint mapping from a type to a list of constraints. For the base type of each activity in the activity tree being validated there is a lookup into AdditionalConstraints. If a matching constraint list is found, all constraints in the list are evaluated for the activity. OnlyUseAdditionalConstraints specifies whether the validator should evaluate all constraints or only those specified in AdditionalConstraints. The default value is false
. AdditionalConstraints and OnlyUseAdditionalConstraints are useful for workflow host authors to add additional validation for workflows, such as policy constraints for tools such as FxCop. For more information about constraints, see Declarative Constraints.
To use ValidationSettings, configure the desired properties, and then pass it in the call to Validate. In this example, a workflow that consists of a Sequence with a custom Add
activity is validated. The Add
activity has two required arguments.
public sealed class Add : CodeActivity<int>
{
[RequiredArgument]
public InArgument<int> Operand1 { get; set; }
[RequiredArgument]
public InArgument<int> Operand2 { get; set; }
protected override int Execute(CodeActivityContext context)
{
return Operand1.Get(context) + Operand2.Get(context);
}
}
The following Add
activity is used in a Sequence, but its two required arguments are not bound.
Variable<int> Operand1 = new Variable<int> { Default = 10 };
Variable<int> Operand2 = new Variable<int> { Default = 15 };
Variable<int> Result = new Variable<int>();
Activity wf = new Sequence
{
Variables = { Operand1, Operand2, Result },
Activities =
{
new Add(),
new WriteLine
{
Text = new InArgument<string>(env => "The result is " + Result.Get(env))
}
}
};
For the following example, validation is performed with SingleLevel set to true
, so only the root Sequence activity is validated.
ValidationSettings settings = new ValidationSettings
{
SingleLevel = true
};
ValidationResults results = ActivityValidationServices.Validate(wf, settings);
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: {0}", error.Message);
}
foreach (ValidationError warning in results.Warnings)
{
Console.WriteLine("Warning: {0}", warning.Message);
}
}
This code displays the following output:
No warnings or errors Even though the Add
activity has required arguments that are not bound, validation is successful because only the root activity is evaluated. This type of validation is useful for validating only specific elements in an activity tree, such as validation of a property change of a single activity in a designer. Note that if this workflow is invoked, the full validation configured in the workflow is evaluated and an InvalidWorkflowException would be thrown. ActivityValidationServices and ValidationSettings configure only validation explicitly invoked by the host, and not the validation that occurs when a workflow is invoked.