1 //----------------------------------------------------------------------------- 2 // Copyright (c) Microsoft Corporation. All rights reserved. 3 //----------------------------------------------------------------------------- 4 5 namespace System.Activities.Validation 6 { 7 using System; 8 using System.Collections.Generic; 9 using System.Collections.ObjectModel; 10 using System.Runtime; 11 using System.Text; 12 using System.Threading; 13 using System.Linq; 14 15 public static class ActivityValidationServices 16 { 17 internal static readonly ReadOnlyCollection<Activity> EmptyChildren = new ReadOnlyCollection<Activity>(new Activity[0]); 18 static ValidationSettings defaultSettings = new ValidationSettings(); 19 internal static ReadOnlyCollection<ValidationError> EmptyValidationErrors = new ReadOnlyCollection<ValidationError>(new List<ValidationError>(0)); 20 Validate(Activity toValidate)21 public static ValidationResults Validate(Activity toValidate) 22 { 23 return Validate(toValidate, defaultSettings); 24 } 25 Validate(Activity toValidate, ValidationSettings settings)26 public static ValidationResults Validate(Activity toValidate, ValidationSettings settings) 27 { 28 if (toValidate == null) 29 { 30 throw FxTrace.Exception.ArgumentNull("toValidate"); 31 } 32 33 if (settings == null) 34 { 35 throw FxTrace.Exception.ArgumentNull("settings"); 36 } 37 38 if (toValidate.HasBeenAssociatedWithAnInstance) 39 { 40 throw FxTrace.Exception.AsError(new InvalidOperationException(SR.RootActivityAlreadyAssociatedWithInstance(toValidate.DisplayName))); 41 } 42 43 if (settings.PrepareForRuntime && (settings.SingleLevel || settings.SkipValidatingRootConfiguration || settings.OnlyUseAdditionalConstraints)) 44 { 45 throw FxTrace.Exception.Argument("settings", SR.InvalidPrepareForRuntimeValidationSettings); 46 } 47 48 InternalActivityValidationServices validator = new InternalActivityValidationServices(settings, toValidate); 49 return validator.InternalValidate(); 50 } 51 Resolve(Activity root, string id)52 public static Activity Resolve(Activity root, string id) 53 { 54 return WorkflowInspectionServices.Resolve(root, id); 55 } 56 ThrowIfViolationsExist(IList<ValidationError> validationErrors, ExceptionReason reason = ExceptionReason.InvalidTree)57 internal static void ThrowIfViolationsExist(IList<ValidationError> validationErrors, ExceptionReason reason = ExceptionReason.InvalidTree) 58 { 59 Exception exception = CreateExceptionFromValidationErrors(validationErrors, reason); 60 61 if (exception != null) 62 { 63 throw FxTrace.Exception.AsError(exception); 64 } 65 } 66 CreateExceptionFromValidationErrors(IList<ValidationError> validationErrors, ExceptionReason reason)67 static Exception CreateExceptionFromValidationErrors(IList<ValidationError> validationErrors, ExceptionReason reason) 68 { 69 if (validationErrors != null && validationErrors.Count > 0) 70 { 71 string exceptionString = GenerateExceptionString(validationErrors, reason); 72 73 if (exceptionString != null) 74 { 75 return new InvalidWorkflowException(exceptionString); 76 } 77 else 78 { 79 return null; 80 } 81 } 82 else 83 { 84 return null; 85 } 86 } 87 GetChildren(ActivityUtilities.ChildActivity root, ActivityUtilities.ActivityCallStack parentChain, ProcessActivityTreeOptions options)88 internal static List<Activity> GetChildren(ActivityUtilities.ChildActivity root, ActivityUtilities.ActivityCallStack parentChain, ProcessActivityTreeOptions options) 89 { 90 ActivityUtilities.FinishCachingSubtree(root, parentChain, options); 91 92 List<Activity> listOfChildren = new List<Activity>(); 93 94 foreach (Activity activity in WorkflowInspectionServices.GetActivities(root.Activity)) 95 { 96 listOfChildren.Add(activity); 97 } 98 99 int toProcessIndex = 0; 100 101 while (toProcessIndex < listOfChildren.Count) 102 { 103 foreach (Activity activity in WorkflowInspectionServices.GetActivities(listOfChildren[toProcessIndex])) 104 { 105 listOfChildren.Add(activity); 106 } 107 108 toProcessIndex++; 109 } 110 111 return listOfChildren; 112 } 113 ValidateRootInputs(Activity rootActivity, IDictionary<string, object> inputs)114 internal static void ValidateRootInputs(Activity rootActivity, IDictionary<string, object> inputs) 115 { 116 IList<ValidationError> validationErrors = null; 117 ValidationHelper.ValidateArguments(rootActivity, rootActivity.EquivalenceInfo, rootActivity.OverloadGroups, rootActivity.RequiredArgumentsNotInOverloadGroups, inputs, ref validationErrors); 118 119 // Validate if there are any extra arguments passed in the input dictionary 120 if (inputs != null) 121 { 122 List<string> unusedArguments = null; 123 IEnumerable<RuntimeArgument> arguments = rootActivity.RuntimeArguments.Where((a) => ArgumentDirectionHelper.IsIn(a.Direction)); 124 125 foreach (string key in inputs.Keys) 126 { 127 bool found = false; 128 foreach (RuntimeArgument argument in arguments) 129 { 130 if (argument.Name == key) 131 { 132 found = true; 133 134 // Validate if the input argument type matches the expected argument type. 135 object inputArgumentValue = null; 136 if (inputs.TryGetValue(key, out inputArgumentValue)) 137 { 138 if (!TypeHelper.AreTypesCompatible(inputArgumentValue, argument.Type)) 139 { 140 ActivityUtilities.Add(ref validationErrors, new ValidationError(SR.InputParametersTypeMismatch(argument.Type, argument.Name), rootActivity)); 141 } 142 } 143 // The ValidateArguments will validate Required in-args and hence not duplicating that validation if the key is not found. 144 145 break; 146 } 147 } 148 149 if (!found) 150 { 151 if (unusedArguments == null) 152 { 153 unusedArguments = new List<string>(); 154 } 155 unusedArguments.Add(key); 156 } 157 } 158 if (unusedArguments != null) 159 { 160 ActivityUtilities.Add(ref validationErrors, new ValidationError(SR.UnusedInputArguments(unusedArguments.AsCommaSeparatedValues()), rootActivity)); 161 } 162 } 163 164 if (validationErrors != null && validationErrors.Count > 0) 165 { 166 string parameterName = "rootArgumentValues"; 167 ExceptionReason reason = ExceptionReason.InvalidNonNullInputs; 168 169 if (inputs == null) 170 { 171 parameterName = "program"; 172 reason = ExceptionReason.InvalidNullInputs; 173 } 174 175 string exceptionString = GenerateExceptionString(validationErrors, reason); 176 177 if (exceptionString != null) 178 { 179 throw FxTrace.Exception.Argument(parameterName, exceptionString); 180 } 181 } 182 } 183 ValidateArguments(Activity activity, bool isRoot, ref IList<ValidationError> validationErrors)184 internal static void ValidateArguments(Activity activity, bool isRoot, ref IList<ValidationError> validationErrors) 185 { 186 Fx.Assert(activity != null, "Activity to validate should not be null."); 187 188 Dictionary<string, List<RuntimeArgument>> overloadGroups; 189 List<RuntimeArgument> requiredArgumentsNotInOverloadGroups; 190 ValidationHelper.OverloadGroupEquivalenceInfo equivalenceInfo; 191 if (ValidationHelper.GatherAndValidateOverloads(activity, out overloadGroups, out requiredArgumentsNotInOverloadGroups, out equivalenceInfo, ref validationErrors)) 192 { 193 // If we're not the root and the overload groups are valid 194 // then we validate the arguments 195 if (!isRoot) 196 { 197 ValidationHelper.ValidateArguments(activity, equivalenceInfo, overloadGroups, requiredArgumentsNotInOverloadGroups, null, ref validationErrors); 198 } 199 } 200 201 // If we are the root, regardless of whether the groups are 202 // valid or not, we cache the group information 203 if (isRoot) 204 { 205 activity.OverloadGroups = overloadGroups; 206 activity.RequiredArgumentsNotInOverloadGroups = requiredArgumentsNotInOverloadGroups; 207 activity.EquivalenceInfo = equivalenceInfo; 208 } 209 } 210 GenerateExceptionString(IList<ValidationError> validationErrors, ExceptionReason reason)211 static string GenerateExceptionString(IList<ValidationError> validationErrors, ExceptionReason reason) 212 { 213 // 4096 is an arbitrary constant. Currently clipped by character count (not bytes). 214 const int maxExceptionStringSize = 4096; 215 216 StringBuilder exceptionMessageBuilder = null; 217 218 for (int i = 0; i < validationErrors.Count; i++) 219 { 220 ValidationError validationError = validationErrors[i]; 221 222 if (!validationError.IsWarning) 223 { 224 // create the common exception string 225 if (exceptionMessageBuilder == null) 226 { 227 exceptionMessageBuilder = new StringBuilder(); 228 229 switch (reason) 230 { 231 case ExceptionReason.InvalidTree: 232 exceptionMessageBuilder.Append(SR.ErrorsEncounteredWhileProcessingTree); 233 break; 234 case ExceptionReason.InvalidNonNullInputs: 235 exceptionMessageBuilder.Append(SR.RootArgumentViolationsFound); 236 break; 237 case ExceptionReason.InvalidNullInputs: 238 exceptionMessageBuilder.Append(SR.RootArgumentViolationsFoundNoInputs); 239 break; 240 } 241 } 242 243 string activityName = null; 244 245 if (validationError.Source != null) 246 { 247 activityName = validationError.Source.DisplayName; 248 } 249 else 250 { 251 activityName = "<UnknownActivity>"; 252 } 253 254 exceptionMessageBuilder.AppendLine(); 255 exceptionMessageBuilder.Append(string.Format(SR.Culture, "'{0}': {1}", activityName, validationError.Message)); 256 257 if (exceptionMessageBuilder.Length > maxExceptionStringSize) 258 { 259 break; 260 } 261 } 262 } 263 264 string exceptionString = null; 265 266 if (exceptionMessageBuilder != null) 267 { 268 exceptionString = exceptionMessageBuilder.ToString(); 269 270 if (exceptionString.Length > maxExceptionStringSize) 271 { 272 string snipNotification = SR.TooManyViolationsForExceptionMessage; 273 274 exceptionString = exceptionString.Substring(0, maxExceptionStringSize - snipNotification.Length); 275 exceptionString += snipNotification; 276 } 277 } 278 279 return exceptionString; 280 } 281 GenerateValidationErrorPrefix(Activity toValidate, ActivityUtilities.ActivityCallStack parentChain, ProcessActivityTreeOptions options, out Activity source)282 static internal string GenerateValidationErrorPrefix(Activity toValidate, ActivityUtilities.ActivityCallStack parentChain, ProcessActivityTreeOptions options, out Activity source) 283 { 284 bool parentVisible = true; 285 string prefix = ""; 286 source = toValidate; 287 288 // Processing for implementation of activity 289 // during build time 290 if (options.SkipRootConfigurationValidation) 291 { 292 // Check if the activity is a implementation child 293 if (toValidate.MemberOf.Parent != null) 294 { 295 // Check if activity is an immediate implementation child 296 // of x:class activity. This means that the activity is 297 // being designed and hence we do not want to add the 298 // prefix at build time 299 if (toValidate.MemberOf.Parent.Parent == null) 300 { 301 prefix = ""; 302 source = toValidate; 303 } 304 else 305 { 306 // This means the activity is a child of immediate implementation child 307 // of x:class activity which means the activity is not visible. 308 // The source points to the first visible parent activity in the 309 // parent chain. 310 while (source.MemberOf.Parent.Parent != null) 311 { 312 source = source.Parent; 313 } 314 prefix = SR.ValidationErrorPrefixForHiddenActivity(source); 315 } 316 return prefix; 317 } 318 } 319 320 // Find out if any of the parents of the activity are not publicly visible 321 for (int i = 0; i < parentChain.Count; i++) 322 { 323 if (parentChain[i].Activity.MemberOf.Parent != null) 324 { 325 parentVisible = false; 326 break; 327 } 328 } 329 330 // Figure out the source of validation error: 331 // - For hidden activity - source will be closest visible public parent 332 // - For visible activity - source will be the activity itself 333 // In current design an activity is visible only if it is in the root id space. 334 // In future, if we provide a knob for the user to specify the 335 // id spaces that are visible, then this check needs to be changed 336 // to iterate over the parentChain and find the closest parent activity that 337 // is in the visible id spaces. 338 while (source.MemberOf.Parent != null) 339 { 340 source = source.Parent; 341 } 342 343 if (toValidate.MemberOf.Parent != null) 344 { 345 // Activity itself is hidden 346 prefix = SR.ValidationErrorPrefixForHiddenActivity(source); 347 } 348 else 349 { 350 if (!parentVisible) 351 { 352 // Activity itself is public but has a private parent 353 prefix = SR.ValidationErrorPrefixForPublicActivityWithHiddenParent(source.Parent, source); 354 } 355 } 356 return prefix; 357 } 358 RunConstraints(ActivityUtilities.ChildActivity childActivity, ActivityUtilities.ActivityCallStack parentChain, IList<Constraint> constraints, ProcessActivityTreeOptions options, bool suppressGetChildrenViolations, ref IList<ValidationError> validationErrors)359 internal static void RunConstraints(ActivityUtilities.ChildActivity childActivity, ActivityUtilities.ActivityCallStack parentChain, IList<Constraint> constraints, ProcessActivityTreeOptions options, bool suppressGetChildrenViolations, ref IList<ValidationError> validationErrors) 360 { 361 if (constraints != null) 362 { 363 Activity toValidate = childActivity.Activity; 364 365 LocationReferenceEnvironment environment = toValidate.GetParentEnvironment(); 366 367 Dictionary<string, object> inputDictionary = new Dictionary<string, object>(2); 368 369 for (int constraintIndex = 0; constraintIndex < constraints.Count; constraintIndex++) 370 { 371 Constraint constraint = constraints[constraintIndex]; 372 373 // there may be null entries here 374 if (constraint == null) 375 { 376 continue; 377 } 378 379 inputDictionary[Constraint.ToValidateArgumentName] = toValidate; 380 ValidationContext validationContext = new ValidationContext(childActivity, parentChain, options, environment); 381 inputDictionary[Constraint.ToValidateContextArgumentName] = validationContext; 382 IDictionary<string, object> results = null; 383 384 try 385 { 386 results = WorkflowInvoker.Invoke(constraint, inputDictionary); 387 } 388 catch (Exception e) 389 { 390 if (Fx.IsFatal(e)) 391 { 392 throw; 393 } 394 395 ValidationError constraintExceptionValidationError = new ValidationError(SR.InternalConstraintException(constraint.DisplayName, toValidate.GetType().FullName, toValidate.DisplayName, e.ToString()), false) 396 { 397 Source = toValidate, 398 Id = toValidate.Id 399 }; 400 401 ActivityUtilities.Add(ref validationErrors, constraintExceptionValidationError); 402 } 403 404 if (results != null) 405 { 406 object resultValidationErrors; 407 if (results.TryGetValue(Constraint.ValidationErrorListArgumentName, out resultValidationErrors)) 408 { 409 IList<ValidationError> validationErrorList = (IList<ValidationError>)resultValidationErrors; 410 411 if (validationErrorList.Count > 0) 412 { 413 if (validationErrors == null) 414 { 415 validationErrors = new List<ValidationError>(); 416 } 417 418 Activity source; 419 string prefix = ActivityValidationServices.GenerateValidationErrorPrefix(childActivity.Activity, parentChain, options, out source); 420 421 for (int validationErrorIndex = 0; validationErrorIndex < validationErrorList.Count; validationErrorIndex++) 422 { 423 ValidationError validationError = validationErrorList[validationErrorIndex]; 424 425 validationError.Source = source; 426 validationError.Id = source.Id; 427 if (!string.IsNullOrEmpty(prefix)) 428 { 429 validationError.Message = prefix + validationError.Message; 430 } 431 validationErrors.Add(validationError); 432 } 433 } 434 } 435 } 436 437 if (!suppressGetChildrenViolations) 438 { 439 validationContext.AddGetChildrenErrors(ref validationErrors); 440 } 441 } 442 } 443 } 444 HasErrors(IList<ValidationError> validationErrors)445 internal static bool HasErrors(IList<ValidationError> validationErrors) 446 { 447 if (validationErrors != null && validationErrors.Count > 0) 448 { 449 for (int i = 0; i < validationErrors.Count; i++) 450 { 451 if (!validationErrors[i].IsWarning) 452 { 453 return true; 454 } 455 } 456 } 457 458 return false; 459 } 460 461 class InternalActivityValidationServices 462 { 463 ValidationSettings settings; 464 Activity rootToValidate; 465 IList<ValidationError> errors; 466 ProcessActivityTreeOptions options; 467 Activity expressionRoot; 468 LocationReferenceEnvironment environment; 469 InternalActivityValidationServices(ValidationSettings settings, Activity toValidate)470 internal InternalActivityValidationServices(ValidationSettings settings, Activity toValidate) 471 { 472 this.settings = settings; 473 this.rootToValidate = toValidate; 474 this.environment = settings.Environment; 475 } 476 InternalValidate()477 internal ValidationResults InternalValidate() 478 { 479 this.options = ProcessActivityTreeOptions.GetValidationOptions(this.settings); 480 481 if (this.settings.OnlyUseAdditionalConstraints) 482 { 483 // We don't want the errors from CacheMetadata so we send those to a "dummy" list. 484 IList<ValidationError> suppressedErrors = null; 485 ActivityUtilities.CacheRootMetadata(this.rootToValidate, this.environment, this.options, new ActivityUtilities.ProcessActivityCallback(ValidateElement), ref suppressedErrors); 486 } 487 else 488 { 489 // We want to add the CacheMetadata errors to our errors collection 490 ActivityUtilities.CacheRootMetadata(this.rootToValidate, this.environment, this.options, new ActivityUtilities.ProcessActivityCallback(ValidateElement), ref this.errors); 491 } 492 493 return new ValidationResults(this.errors); 494 } 495 ValidateElement(ActivityUtilities.ChildActivity childActivity, ActivityUtilities.ActivityCallStack parentChain)496 void ValidateElement(ActivityUtilities.ChildActivity childActivity, ActivityUtilities.ActivityCallStack parentChain) 497 { 498 Activity toValidate = childActivity.Activity; 499 500 if (!this.settings.SingleLevel || object.ReferenceEquals(toValidate, this.rootToValidate)) 501 { 502 // 0. Open time violations are captured by the CacheMetadata walk. 503 504 // 1. Argument validations are done by the CacheMetadata walk. 505 506 // 2. Build constraints are done by the CacheMetadata walk. 507 508 // 3. Then do policy constraints 509 if (this.settings.HasAdditionalConstraints && childActivity.CanBeExecuted && parentChain.WillExecute) 510 { 511 bool suppressGetChildrenViolations = this.settings.OnlyUseAdditionalConstraints || this.settings.SingleLevel; 512 513 Type currentType = toValidate.GetType(); 514 515 while (currentType != null) 516 { 517 IList<Constraint> policyConstraints; 518 if (this.settings.AdditionalConstraints.TryGetValue(currentType, out policyConstraints)) 519 { 520 RunConstraints(childActivity, parentChain, policyConstraints, this.options, suppressGetChildrenViolations, ref this.errors); 521 } 522 523 if (currentType.IsGenericType) 524 { 525 Type genericDefinitionType = currentType.GetGenericTypeDefinition(); 526 if (genericDefinitionType != null) 527 { 528 IList<Constraint> genericTypePolicyConstraints; 529 if (this.settings.AdditionalConstraints.TryGetValue(genericDefinitionType, out genericTypePolicyConstraints)) 530 { 531 RunConstraints(childActivity, parentChain, genericTypePolicyConstraints, this.options, suppressGetChildrenViolations, ref this.errors); 532 } 533 } 534 } 535 currentType = currentType.BaseType; 536 } 537 } 538 539 //4. Validate if the argument expression subtree contains an activity that can induce idle. 540 if (childActivity.Activity.IsExpressionRoot) 541 { 542 if (childActivity.Activity.HasNonEmptySubtree) 543 { 544 this.expressionRoot = childActivity.Activity; 545 // Back-compat: In Dev10 we always used ProcessActivityTreeOptions.FullCachingOptions here, and ignored this.options. 546 // So we need to continue to do that, unless the new Dev11 flag SkipRootConfigurationValidation is passed. 547 ProcessActivityTreeOptions options = this.options.SkipRootConfigurationValidation ? this.options : ProcessActivityTreeOptions.FullCachingOptions; 548 ActivityUtilities.FinishCachingSubtree(childActivity, parentChain, options, ValidateExpressionSubtree); 549 this.expressionRoot = null; 550 } 551 else if (childActivity.Activity.InternalCanInduceIdle) 552 { 553 Activity activity = childActivity.Activity; 554 RuntimeArgument runtimeArgument = GetBoundRuntimeArgument(activity); 555 ValidationError error = new ValidationError(SR.CanInduceIdleActivityInArgumentExpression(runtimeArgument.Name, activity.Parent.DisplayName, activity.DisplayName), true, runtimeArgument.Name, activity.Parent); 556 ActivityUtilities.Add(ref this.errors, error); 557 } 558 559 } 560 } 561 } 562 ValidateExpressionSubtree(ActivityUtilities.ChildActivity childActivity, ActivityUtilities.ActivityCallStack parentChain)563 void ValidateExpressionSubtree(ActivityUtilities.ChildActivity childActivity, ActivityUtilities.ActivityCallStack parentChain) 564 { 565 Fx.Assert(this.expressionRoot != null, "This callback should be called activities in the expression subtree only."); 566 567 if (childActivity.Activity.InternalCanInduceIdle) 568 { 569 Activity activity = childActivity.Activity; 570 Activity expressionRoot = this.expressionRoot; 571 572 RuntimeArgument runtimeArgument = GetBoundRuntimeArgument(expressionRoot); 573 ValidationError error = new ValidationError(SR.CanInduceIdleActivityInArgumentExpression(runtimeArgument.Name, expressionRoot.Parent.DisplayName, activity.DisplayName), true, runtimeArgument.Name, expressionRoot.Parent); 574 ActivityUtilities.Add(ref this.errors, error); 575 } 576 } 577 } 578 579 // Iterate through all runtime arguments on the configured activity 580 // and find the one that binds to expressionActivity. GetBoundRuntimeArgument(Activity expressionActivity)581 static RuntimeArgument GetBoundRuntimeArgument(Activity expressionActivity) 582 { 583 Activity configuredActivity = expressionActivity.Parent; 584 Fx.Assert(configuredActivity != null, "Configured activity should not be null."); 585 586 RuntimeArgument boundRuntimeArgument = null; 587 for (int i = 0; i < configuredActivity.RuntimeArguments.Count; i++) 588 { 589 boundRuntimeArgument = configuredActivity.RuntimeArguments[i]; 590 if (object.ReferenceEquals(boundRuntimeArgument.BoundArgument.Expression, expressionActivity)) 591 { 592 break; 593 } 594 } 595 Fx.Assert(boundRuntimeArgument != null, "We should always be able to find the runtime argument!"); 596 return boundRuntimeArgument; 597 } 598 599 // This method checks for duplicate evaluation order entries in a collection that is 600 // sorted in ascendng order of evaluation order values. ValidateEvaluationOrder(IList<RuntimeArgument> runtimeArguments, Activity referenceActivity, ref IList<ValidationError> validationErrors)601 internal static void ValidateEvaluationOrder(IList<RuntimeArgument> runtimeArguments, Activity referenceActivity, ref IList<ValidationError> validationErrors) 602 { 603 for (int i = 0; i < runtimeArguments.Count - 1; i++) 604 { 605 RuntimeArgument argument = runtimeArguments[i]; 606 RuntimeArgument nextArgument = runtimeArguments[i + 1]; 607 if (argument.IsEvaluationOrderSpecified && nextArgument.IsEvaluationOrderSpecified) 608 { 609 if (argument.BoundArgument.EvaluationOrder == nextArgument.BoundArgument.EvaluationOrder) 610 { 611 ActivityUtilities.Add(ref validationErrors, new ValidationError(SR.DuplicateEvaluationOrderValues(referenceActivity.DisplayName, argument.BoundArgument.EvaluationOrder), false, argument.Name, referenceActivity)); 612 } 613 } 614 } 615 } 616 617 internal enum ExceptionReason 618 { 619 InvalidTree, 620 InvalidNullInputs, 621 InvalidNonNullInputs, 622 } 623 624 } 625 } 626