//----------------------------------------------------------------------------- // Copyright (c) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- namespace System.Activities.Validation { using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Runtime; using System.Text; using System.Threading; using System.Linq; public static class ActivityValidationServices { internal static readonly ReadOnlyCollection EmptyChildren = new ReadOnlyCollection(new Activity[0]); static ValidationSettings defaultSettings = new ValidationSettings(); internal static ReadOnlyCollection EmptyValidationErrors = new ReadOnlyCollection(new List(0)); public static ValidationResults Validate(Activity toValidate) { return Validate(toValidate, defaultSettings); } public static ValidationResults Validate(Activity toValidate, ValidationSettings settings) { if (toValidate == null) { throw FxTrace.Exception.ArgumentNull("toValidate"); } if (settings == null) { throw FxTrace.Exception.ArgumentNull("settings"); } if (toValidate.HasBeenAssociatedWithAnInstance) { throw FxTrace.Exception.AsError(new InvalidOperationException(SR.RootActivityAlreadyAssociatedWithInstance(toValidate.DisplayName))); } if (settings.PrepareForRuntime && (settings.SingleLevel || settings.SkipValidatingRootConfiguration || settings.OnlyUseAdditionalConstraints)) { throw FxTrace.Exception.Argument("settings", SR.InvalidPrepareForRuntimeValidationSettings); } InternalActivityValidationServices validator = new InternalActivityValidationServices(settings, toValidate); return validator.InternalValidate(); } public static Activity Resolve(Activity root, string id) { return WorkflowInspectionServices.Resolve(root, id); } internal static void ThrowIfViolationsExist(IList validationErrors, ExceptionReason reason = ExceptionReason.InvalidTree) { Exception exception = CreateExceptionFromValidationErrors(validationErrors, reason); if (exception != null) { throw FxTrace.Exception.AsError(exception); } } static Exception CreateExceptionFromValidationErrors(IList validationErrors, ExceptionReason reason) { if (validationErrors != null && validationErrors.Count > 0) { string exceptionString = GenerateExceptionString(validationErrors, reason); if (exceptionString != null) { return new InvalidWorkflowException(exceptionString); } else { return null; } } else { return null; } } internal static List GetChildren(ActivityUtilities.ChildActivity root, ActivityUtilities.ActivityCallStack parentChain, ProcessActivityTreeOptions options) { ActivityUtilities.FinishCachingSubtree(root, parentChain, options); List listOfChildren = new List(); foreach (Activity activity in WorkflowInspectionServices.GetActivities(root.Activity)) { listOfChildren.Add(activity); } int toProcessIndex = 0; while (toProcessIndex < listOfChildren.Count) { foreach (Activity activity in WorkflowInspectionServices.GetActivities(listOfChildren[toProcessIndex])) { listOfChildren.Add(activity); } toProcessIndex++; } return listOfChildren; } internal static void ValidateRootInputs(Activity rootActivity, IDictionary inputs) { IList validationErrors = null; ValidationHelper.ValidateArguments(rootActivity, rootActivity.EquivalenceInfo, rootActivity.OverloadGroups, rootActivity.RequiredArgumentsNotInOverloadGroups, inputs, ref validationErrors); // Validate if there are any extra arguments passed in the input dictionary if (inputs != null) { List unusedArguments = null; IEnumerable arguments = rootActivity.RuntimeArguments.Where((a) => ArgumentDirectionHelper.IsIn(a.Direction)); foreach (string key in inputs.Keys) { bool found = false; foreach (RuntimeArgument argument in arguments) { if (argument.Name == key) { found = true; // Validate if the input argument type matches the expected argument type. object inputArgumentValue = null; if (inputs.TryGetValue(key, out inputArgumentValue)) { if (!TypeHelper.AreTypesCompatible(inputArgumentValue, argument.Type)) { ActivityUtilities.Add(ref validationErrors, new ValidationError(SR.InputParametersTypeMismatch(argument.Type, argument.Name), rootActivity)); } } // The ValidateArguments will validate Required in-args and hence not duplicating that validation if the key is not found. break; } } if (!found) { if (unusedArguments == null) { unusedArguments = new List(); } unusedArguments.Add(key); } } if (unusedArguments != null) { ActivityUtilities.Add(ref validationErrors, new ValidationError(SR.UnusedInputArguments(unusedArguments.AsCommaSeparatedValues()), rootActivity)); } } if (validationErrors != null && validationErrors.Count > 0) { string parameterName = "rootArgumentValues"; ExceptionReason reason = ExceptionReason.InvalidNonNullInputs; if (inputs == null) { parameterName = "program"; reason = ExceptionReason.InvalidNullInputs; } string exceptionString = GenerateExceptionString(validationErrors, reason); if (exceptionString != null) { throw FxTrace.Exception.Argument(parameterName, exceptionString); } } } internal static void ValidateArguments(Activity activity, bool isRoot, ref IList validationErrors) { Fx.Assert(activity != null, "Activity to validate should not be null."); Dictionary> overloadGroups; List requiredArgumentsNotInOverloadGroups; ValidationHelper.OverloadGroupEquivalenceInfo equivalenceInfo; if (ValidationHelper.GatherAndValidateOverloads(activity, out overloadGroups, out requiredArgumentsNotInOverloadGroups, out equivalenceInfo, ref validationErrors)) { // If we're not the root and the overload groups are valid // then we validate the arguments if (!isRoot) { ValidationHelper.ValidateArguments(activity, equivalenceInfo, overloadGroups, requiredArgumentsNotInOverloadGroups, null, ref validationErrors); } } // If we are the root, regardless of whether the groups are // valid or not, we cache the group information if (isRoot) { activity.OverloadGroups = overloadGroups; activity.RequiredArgumentsNotInOverloadGroups = requiredArgumentsNotInOverloadGroups; activity.EquivalenceInfo = equivalenceInfo; } } static string GenerateExceptionString(IList validationErrors, ExceptionReason reason) { // 4096 is an arbitrary constant. Currently clipped by character count (not bytes). const int maxExceptionStringSize = 4096; StringBuilder exceptionMessageBuilder = null; for (int i = 0; i < validationErrors.Count; i++) { ValidationError validationError = validationErrors[i]; if (!validationError.IsWarning) { // create the common exception string if (exceptionMessageBuilder == null) { exceptionMessageBuilder = new StringBuilder(); switch (reason) { case ExceptionReason.InvalidTree: exceptionMessageBuilder.Append(SR.ErrorsEncounteredWhileProcessingTree); break; case ExceptionReason.InvalidNonNullInputs: exceptionMessageBuilder.Append(SR.RootArgumentViolationsFound); break; case ExceptionReason.InvalidNullInputs: exceptionMessageBuilder.Append(SR.RootArgumentViolationsFoundNoInputs); break; } } string activityName = null; if (validationError.Source != null) { activityName = validationError.Source.DisplayName; } else { activityName = ""; } exceptionMessageBuilder.AppendLine(); exceptionMessageBuilder.Append(string.Format(SR.Culture, "'{0}': {1}", activityName, validationError.Message)); if (exceptionMessageBuilder.Length > maxExceptionStringSize) { break; } } } string exceptionString = null; if (exceptionMessageBuilder != null) { exceptionString = exceptionMessageBuilder.ToString(); if (exceptionString.Length > maxExceptionStringSize) { string snipNotification = SR.TooManyViolationsForExceptionMessage; exceptionString = exceptionString.Substring(0, maxExceptionStringSize - snipNotification.Length); exceptionString += snipNotification; } } return exceptionString; } static internal string GenerateValidationErrorPrefix(Activity toValidate, ActivityUtilities.ActivityCallStack parentChain, ProcessActivityTreeOptions options, out Activity source) { bool parentVisible = true; string prefix = ""; source = toValidate; // Processing for implementation of activity // during build time if (options.SkipRootConfigurationValidation) { // Check if the activity is a implementation child if (toValidate.MemberOf.Parent != null) { // Check if activity is an immediate implementation child // of x:class activity. This means that the activity is // being designed and hence we do not want to add the // prefix at build time if (toValidate.MemberOf.Parent.Parent == null) { prefix = ""; source = toValidate; } else { // This means the activity is a child of immediate implementation child // of x:class activity which means the activity is not visible. // The source points to the first visible parent activity in the // parent chain. while (source.MemberOf.Parent.Parent != null) { source = source.Parent; } prefix = SR.ValidationErrorPrefixForHiddenActivity(source); } return prefix; } } // Find out if any of the parents of the activity are not publicly visible for (int i = 0; i < parentChain.Count; i++) { if (parentChain[i].Activity.MemberOf.Parent != null) { parentVisible = false; break; } } // Figure out the source of validation error: // - For hidden activity - source will be closest visible public parent // - For visible activity - source will be the activity itself // In current design an activity is visible only if it is in the root id space. // In future, if we provide a knob for the user to specify the // id spaces that are visible, then this check needs to be changed // to iterate over the parentChain and find the closest parent activity that // is in the visible id spaces. while (source.MemberOf.Parent != null) { source = source.Parent; } if (toValidate.MemberOf.Parent != null) { // Activity itself is hidden prefix = SR.ValidationErrorPrefixForHiddenActivity(source); } else { if (!parentVisible) { // Activity itself is public but has a private parent prefix = SR.ValidationErrorPrefixForPublicActivityWithHiddenParent(source.Parent, source); } } return prefix; } internal static void RunConstraints(ActivityUtilities.ChildActivity childActivity, ActivityUtilities.ActivityCallStack parentChain, IList constraints, ProcessActivityTreeOptions options, bool suppressGetChildrenViolations, ref IList validationErrors) { if (constraints != null) { Activity toValidate = childActivity.Activity; LocationReferenceEnvironment environment = toValidate.GetParentEnvironment(); Dictionary inputDictionary = new Dictionary(2); for (int constraintIndex = 0; constraintIndex < constraints.Count; constraintIndex++) { Constraint constraint = constraints[constraintIndex]; // there may be null entries here if (constraint == null) { continue; } inputDictionary[Constraint.ToValidateArgumentName] = toValidate; ValidationContext validationContext = new ValidationContext(childActivity, parentChain, options, environment); inputDictionary[Constraint.ToValidateContextArgumentName] = validationContext; IDictionary results = null; try { results = WorkflowInvoker.Invoke(constraint, inputDictionary); } catch (Exception e) { if (Fx.IsFatal(e)) { throw; } ValidationError constraintExceptionValidationError = new ValidationError(SR.InternalConstraintException(constraint.DisplayName, toValidate.GetType().FullName, toValidate.DisplayName, e.ToString()), false) { Source = toValidate, Id = toValidate.Id }; ActivityUtilities.Add(ref validationErrors, constraintExceptionValidationError); } if (results != null) { object resultValidationErrors; if (results.TryGetValue(Constraint.ValidationErrorListArgumentName, out resultValidationErrors)) { IList validationErrorList = (IList)resultValidationErrors; if (validationErrorList.Count > 0) { if (validationErrors == null) { validationErrors = new List(); } Activity source; string prefix = ActivityValidationServices.GenerateValidationErrorPrefix(childActivity.Activity, parentChain, options, out source); for (int validationErrorIndex = 0; validationErrorIndex < validationErrorList.Count; validationErrorIndex++) { ValidationError validationError = validationErrorList[validationErrorIndex]; validationError.Source = source; validationError.Id = source.Id; if (!string.IsNullOrEmpty(prefix)) { validationError.Message = prefix + validationError.Message; } validationErrors.Add(validationError); } } } } if (!suppressGetChildrenViolations) { validationContext.AddGetChildrenErrors(ref validationErrors); } } } } internal static bool HasErrors(IList validationErrors) { if (validationErrors != null && validationErrors.Count > 0) { for (int i = 0; i < validationErrors.Count; i++) { if (!validationErrors[i].IsWarning) { return true; } } } return false; } class InternalActivityValidationServices { ValidationSettings settings; Activity rootToValidate; IList errors; ProcessActivityTreeOptions options; Activity expressionRoot; LocationReferenceEnvironment environment; internal InternalActivityValidationServices(ValidationSettings settings, Activity toValidate) { this.settings = settings; this.rootToValidate = toValidate; this.environment = settings.Environment; } internal ValidationResults InternalValidate() { this.options = ProcessActivityTreeOptions.GetValidationOptions(this.settings); if (this.settings.OnlyUseAdditionalConstraints) { // We don't want the errors from CacheMetadata so we send those to a "dummy" list. IList suppressedErrors = null; ActivityUtilities.CacheRootMetadata(this.rootToValidate, this.environment, this.options, new ActivityUtilities.ProcessActivityCallback(ValidateElement), ref suppressedErrors); } else { // We want to add the CacheMetadata errors to our errors collection ActivityUtilities.CacheRootMetadata(this.rootToValidate, this.environment, this.options, new ActivityUtilities.ProcessActivityCallback(ValidateElement), ref this.errors); } return new ValidationResults(this.errors); } void ValidateElement(ActivityUtilities.ChildActivity childActivity, ActivityUtilities.ActivityCallStack parentChain) { Activity toValidate = childActivity.Activity; if (!this.settings.SingleLevel || object.ReferenceEquals(toValidate, this.rootToValidate)) { // 0. Open time violations are captured by the CacheMetadata walk. // 1. Argument validations are done by the CacheMetadata walk. // 2. Build constraints are done by the CacheMetadata walk. // 3. Then do policy constraints if (this.settings.HasAdditionalConstraints && childActivity.CanBeExecuted && parentChain.WillExecute) { bool suppressGetChildrenViolations = this.settings.OnlyUseAdditionalConstraints || this.settings.SingleLevel; Type currentType = toValidate.GetType(); while (currentType != null) { IList policyConstraints; if (this.settings.AdditionalConstraints.TryGetValue(currentType, out policyConstraints)) { RunConstraints(childActivity, parentChain, policyConstraints, this.options, suppressGetChildrenViolations, ref this.errors); } if (currentType.IsGenericType) { Type genericDefinitionType = currentType.GetGenericTypeDefinition(); if (genericDefinitionType != null) { IList genericTypePolicyConstraints; if (this.settings.AdditionalConstraints.TryGetValue(genericDefinitionType, out genericTypePolicyConstraints)) { RunConstraints(childActivity, parentChain, genericTypePolicyConstraints, this.options, suppressGetChildrenViolations, ref this.errors); } } } currentType = currentType.BaseType; } } //4. Validate if the argument expression subtree contains an activity that can induce idle. if (childActivity.Activity.IsExpressionRoot) { if (childActivity.Activity.HasNonEmptySubtree) { this.expressionRoot = childActivity.Activity; // Back-compat: In Dev10 we always used ProcessActivityTreeOptions.FullCachingOptions here, and ignored this.options. // So we need to continue to do that, unless the new Dev11 flag SkipRootConfigurationValidation is passed. ProcessActivityTreeOptions options = this.options.SkipRootConfigurationValidation ? this.options : ProcessActivityTreeOptions.FullCachingOptions; ActivityUtilities.FinishCachingSubtree(childActivity, parentChain, options, ValidateExpressionSubtree); this.expressionRoot = null; } else if (childActivity.Activity.InternalCanInduceIdle) { Activity activity = childActivity.Activity; RuntimeArgument runtimeArgument = GetBoundRuntimeArgument(activity); ValidationError error = new ValidationError(SR.CanInduceIdleActivityInArgumentExpression(runtimeArgument.Name, activity.Parent.DisplayName, activity.DisplayName), true, runtimeArgument.Name, activity.Parent); ActivityUtilities.Add(ref this.errors, error); } } } } void ValidateExpressionSubtree(ActivityUtilities.ChildActivity childActivity, ActivityUtilities.ActivityCallStack parentChain) { Fx.Assert(this.expressionRoot != null, "This callback should be called activities in the expression subtree only."); if (childActivity.Activity.InternalCanInduceIdle) { Activity activity = childActivity.Activity; Activity expressionRoot = this.expressionRoot; RuntimeArgument runtimeArgument = GetBoundRuntimeArgument(expressionRoot); ValidationError error = new ValidationError(SR.CanInduceIdleActivityInArgumentExpression(runtimeArgument.Name, expressionRoot.Parent.DisplayName, activity.DisplayName), true, runtimeArgument.Name, expressionRoot.Parent); ActivityUtilities.Add(ref this.errors, error); } } } // Iterate through all runtime arguments on the configured activity // and find the one that binds to expressionActivity. static RuntimeArgument GetBoundRuntimeArgument(Activity expressionActivity) { Activity configuredActivity = expressionActivity.Parent; Fx.Assert(configuredActivity != null, "Configured activity should not be null."); RuntimeArgument boundRuntimeArgument = null; for (int i = 0; i < configuredActivity.RuntimeArguments.Count; i++) { boundRuntimeArgument = configuredActivity.RuntimeArguments[i]; if (object.ReferenceEquals(boundRuntimeArgument.BoundArgument.Expression, expressionActivity)) { break; } } Fx.Assert(boundRuntimeArgument != null, "We should always be able to find the runtime argument!"); return boundRuntimeArgument; } // This method checks for duplicate evaluation order entries in a collection that is // sorted in ascendng order of evaluation order values. internal static void ValidateEvaluationOrder(IList runtimeArguments, Activity referenceActivity, ref IList validationErrors) { for (int i = 0; i < runtimeArguments.Count - 1; i++) { RuntimeArgument argument = runtimeArguments[i]; RuntimeArgument nextArgument = runtimeArguments[i + 1]; if (argument.IsEvaluationOrderSpecified && nextArgument.IsEvaluationOrderSpecified) { if (argument.BoundArgument.EvaluationOrder == nextArgument.BoundArgument.EvaluationOrder) { ActivityUtilities.Add(ref validationErrors, new ValidationError(SR.DuplicateEvaluationOrderValues(referenceActivity.DisplayName, argument.BoundArgument.EvaluationOrder), false, argument.Name, referenceActivity)); } } } } internal enum ExceptionReason { InvalidTree, InvalidNullInputs, InvalidNonNullInputs, } } }