// --------------------------------------------------------------------------- // Copyright (C) 2005 Microsoft Corporation All Rights Reserved // --------------------------------------------------------------------------- #pragma warning disable 1634, 1691 #define CODE_ANALYSIS using System.CodeDom; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using System.Workflow.ComponentModel; using System.Workflow.ComponentModel.Compiler; namespace System.Workflow.Activities.Rules { #region RuleExpressionResult class hierarchy public abstract class RuleExpressionResult { public abstract object Value { get; set; } } public class RuleLiteralResult : RuleExpressionResult { private object literal; public RuleLiteralResult(object literal) { this.literal = literal; } public override object Value { get { return literal; } set { throw new InvalidOperationException(Messages.CannotWriteToExpression); } } } internal class RuleFieldResult : RuleExpressionResult { private object targetObject; private FieldInfo fieldInfo; public RuleFieldResult(object targetObject, FieldInfo fieldInfo) { if (fieldInfo == null) throw new ArgumentNullException("fieldInfo"); this.targetObject = targetObject; this.fieldInfo = fieldInfo; } public override object Value { get { #pragma warning disable 56503 if (!fieldInfo.IsStatic && targetObject == null) { // Accessing a non-static field from null target. string message = string.Format(CultureInfo.CurrentCulture, Messages.TargetEvaluatedNullField, fieldInfo.Name); RuleEvaluationException exception = new RuleEvaluationException(message); exception.Data[RuleUserDataKeys.ErrorObject] = fieldInfo; throw exception; } return fieldInfo.GetValue(targetObject); #pragma warning restore 56503 } set { if (!fieldInfo.IsStatic && targetObject == null) { // Accessing a non-static field from null target. string message = string.Format(CultureInfo.CurrentCulture, Messages.TargetEvaluatedNullField, fieldInfo.Name); RuleEvaluationException exception = new RuleEvaluationException(message); exception.Data[RuleUserDataKeys.ErrorObject] = fieldInfo; throw exception; } fieldInfo.SetValue(targetObject, value); } } } internal class RulePropertyResult : RuleExpressionResult { private PropertyInfo propertyInfo; private object targetObject; private object[] indexerArguments; public RulePropertyResult(PropertyInfo propertyInfo, object targetObject, object[] indexerArguments) { if (propertyInfo == null) throw new ArgumentNullException("propertyInfo"); this.targetObject = targetObject; this.propertyInfo = propertyInfo; this.indexerArguments = indexerArguments; } public override object Value { get { #pragma warning disable 56503 if (!propertyInfo.GetGetMethod(true).IsStatic && targetObject == null) { string message = string.Format(CultureInfo.CurrentCulture, Messages.TargetEvaluatedNullProperty, propertyInfo.Name); RuleEvaluationException exception = new RuleEvaluationException(message); exception.Data[RuleUserDataKeys.ErrorObject] = propertyInfo; throw exception; } try { return propertyInfo.GetValue(targetObject, indexerArguments); } catch (TargetInvocationException e) { // if there is no inner exception, leave it untouched if (e.InnerException == null) throw; string message = string.Format(CultureInfo.CurrentCulture, Messages.Error_PropertyGet, RuleDecompiler.DecompileType(propertyInfo.ReflectedType), propertyInfo.Name, e.InnerException.Message); throw new TargetInvocationException(message, e.InnerException); } #pragma warning restore 56503 } set { if (!propertyInfo.GetSetMethod(true).IsStatic && targetObject == null) { string message = string.Format(CultureInfo.CurrentCulture, Messages.TargetEvaluatedNullProperty, propertyInfo.Name); RuleEvaluationException exception = new RuleEvaluationException(message); exception.Data[RuleUserDataKeys.ErrorObject] = propertyInfo; throw exception; } try { propertyInfo.SetValue(targetObject, value, indexerArguments); } catch (TargetInvocationException e) { // if there is no inner exception, leave it untouched if (e.InnerException == null) throw; string message = string.Format(CultureInfo.CurrentCulture, Messages.Error_PropertySet, RuleDecompiler.DecompileType(propertyInfo.ReflectedType), propertyInfo.Name, e.InnerException.Message); throw new TargetInvocationException(message, e.InnerException); } } } } internal class RuleArrayElementResult : RuleExpressionResult { private Array targetArray; private long[] indexerArguments; public RuleArrayElementResult(Array targetArray, long[] indexerArguments) { if (targetArray == null) throw new ArgumentNullException("targetArray"); if (indexerArguments == null) throw new ArgumentNullException("indexerArguments"); this.targetArray = targetArray; this.indexerArguments = indexerArguments; } public override object Value { get { return targetArray.GetValue(indexerArguments); } set { targetArray.SetValue(value, indexerArguments); } } } #endregion #region RuleExecution Class public class RuleExecution { private bool halted; // "Halt" was executed? private Activity activity; private object thisObject; private RuleValidation validation; private ActivityExecutionContext activityExecutionContext; private RuleLiteralResult thisLiteralResult; [SuppressMessage("Microsoft.Naming", "CA1720:AvoidTypeNamesInParameters", MessageId = "1#")] public RuleExecution(RuleValidation validation, object thisObject) { if (validation == null) throw new ArgumentNullException("validation"); if (thisObject == null) throw new ArgumentNullException("thisObject"); if (validation.ThisType != thisObject.GetType()) throw new InvalidOperationException( string.Format(CultureInfo.CurrentCulture, Messages.ValidationMismatch, RuleDecompiler.DecompileType(validation.ThisType), RuleDecompiler.DecompileType(thisObject.GetType()))); this.validation = validation; this.activity = thisObject as Activity; this.thisObject = thisObject; this.thisLiteralResult = new RuleLiteralResult(thisObject); } [SuppressMessage("Microsoft.Naming", "CA1720:AvoidTypeNamesInParameters", MessageId = "1#")] public RuleExecution(RuleValidation validation, object thisObject, ActivityExecutionContext activityExecutionContext) : this(validation, thisObject) { this.activityExecutionContext = activityExecutionContext; } public object ThisObject { get { return thisObject; } } public Activity Activity { get { #pragma warning disable 56503 if (activity == null) throw new InvalidOperationException(Messages.NoActivity); return activity; #pragma warning restore 56503 } } public RuleValidation Validation { get { return validation; } set { if (value == null) throw new ArgumentNullException("value"); validation = value; } } public bool Halted { get { return halted; } set { halted = value; } } public ActivityExecutionContext ActivityExecutionContext { get { return this.activityExecutionContext; } } internal RuleLiteralResult ThisLiteralResult { get { return this.thisLiteralResult; } } } #endregion #region RuleState internal class internal class RuleState : IComparable { internal Rule Rule; private ICollection thenActionsActiveRules; private ICollection elseActionsActiveRules; internal RuleState(Rule rule) { this.Rule = rule; } internal ICollection ThenActionsActiveRules { get { return thenActionsActiveRules; } set { thenActionsActiveRules = value; } } internal ICollection ElseActionsActiveRules { get { return elseActionsActiveRules; } set { elseActionsActiveRules = value; } } int IComparable.CompareTo(object obj) { RuleState other = obj as RuleState; int compare = other.Rule.Priority.CompareTo(Rule.Priority); if (compare == 0) // if the priorities are the same, compare names (in ascending order) compare = -other.Rule.Name.CompareTo(Rule.Name); return compare; } } #endregion #region Tracking Argument /// /// Contains the name and condition result of a rule that has caused one or more actions to execute. /// [Serializable] [Obsolete("The System.Workflow.* types are deprecated. Instead, please use the new types from System.Activities.*")] public class RuleActionTrackingEvent { private string ruleName; private bool conditionResult; internal RuleActionTrackingEvent(string ruleName, bool conditionResult) { this.ruleName = ruleName; this.conditionResult = conditionResult; } /// /// The name of the rule that has caused one or more actions to execute. /// public string RuleName { get { return ruleName; } } /// /// The rule's condition result: false means the "else" actions are executed; true means the "then" actions are executed. /// public bool ConditionResult { get { return conditionResult; } } } #endregion internal class Executor { #region Rule Set Executor internal static IList Preprocess(RuleChainingBehavior behavior, ICollection rules, RuleValidation validation, Tracer tracer) { // start by taking the active rules and make them into a list sorted by priority List orderedRules = new List(rules.Count); foreach (Rule r in rules) { if (r.Active) orderedRules.Add(new RuleState(r)); } orderedRules.Sort(); // Analyze the rules to match side-effects with dependencies. // Note that the RuleSet needs to have been validated prior to this. AnalyzeRules(behavior, orderedRules, validation, tracer); // return the sorted list of rules return orderedRules; } internal static void ExecuteRuleSet(IList orderedRules, RuleExecution ruleExecution, Tracer tracer, string trackingKey) { // keep track of rule execution long[] executionCount = new long[orderedRules.Count]; bool[] satisfied = new bool[orderedRules.Count]; // clear the halted flag ruleExecution.Halted = false; ActivityExecutionContext activityExecutionContext = ruleExecution.ActivityExecutionContext; // loop until we hit the end of the list int current = 0; while (current < orderedRules.Count) { RuleState currentRuleState = orderedRules[current]; // does this rule need to be evaluated? if (!satisfied[current]) { // yes, so evaluate it and determine the list of actions needed if (tracer != null) tracer.StartRule(currentRuleState.Rule.Name); satisfied[current] = true; bool result = currentRuleState.Rule.Condition.Evaluate(ruleExecution); if (tracer != null) tracer.RuleResult(currentRuleState.Rule.Name, result); if (activityExecutionContext != null && currentRuleState.Rule.Name != null) activityExecutionContext.TrackData(trackingKey, new RuleActionTrackingEvent(currentRuleState.Rule.Name, result)); ICollection actions = (result) ? currentRuleState.Rule.thenActions : currentRuleState.Rule.elseActions; ICollection activeRules = result ? currentRuleState.ThenActionsActiveRules : currentRuleState.ElseActionsActiveRules; // are there any actions to be performed? if ((actions != null) && (actions.Count > 0)) { ++executionCount[current]; string ruleName = currentRuleState.Rule.Name; if (tracer != null) tracer.StartActions(ruleName, result); // evaluate the actions foreach (RuleAction action in actions) { action.Execute(ruleExecution); // was Halt executed? if (ruleExecution.Halted) break; } // was Halt executed? if (ruleExecution.Halted) break; // any fields updated? if (activeRules != null) { foreach (int updatedRuleIndex in activeRules) { RuleState rs = orderedRules[updatedRuleIndex]; if (satisfied[updatedRuleIndex]) { // evaluate at least once, or repeatedly if appropriate if ((executionCount[updatedRuleIndex] == 0) || (rs.Rule.ReevaluationBehavior == RuleReevaluationBehavior.Always)) { if (tracer != null) tracer.TraceUpdate(ruleName, rs.Rule.Name); satisfied[updatedRuleIndex] = false; if (updatedRuleIndex < current) current = updatedRuleIndex; } } } } continue; } } ++current; } // no more rules to execute, so we are done } class RuleSymbolInfo { internal ICollection conditionDependencies; internal ICollection thenSideEffects; internal ICollection elseSideEffects; } private static void AnalyzeRules(RuleChainingBehavior behavior, List ruleStates, RuleValidation validation, Tracer tracer) { int i; int numRules = ruleStates.Count; // if no chaining is required, then nothing to do if (behavior == RuleChainingBehavior.None) return; // Analyze all the rules and collect all the dependencies & side-effects RuleSymbolInfo[] ruleSymbols = new RuleSymbolInfo[numRules]; for (i = 0; i < numRules; ++i) ruleSymbols[i] = AnalyzeRule(behavior, ruleStates[i].Rule, validation, tracer); for (i = 0; i < numRules; ++i) { RuleState currentRuleState = ruleStates[i]; if (ruleSymbols[i].thenSideEffects != null) { currentRuleState.ThenActionsActiveRules = AnalyzeSideEffects(ruleSymbols[i].thenSideEffects, ruleSymbols); if ((currentRuleState.ThenActionsActiveRules != null) && (tracer != null)) tracer.TraceThenTriggers(currentRuleState.Rule.Name, currentRuleState.ThenActionsActiveRules, ruleStates); } if (ruleSymbols[i].elseSideEffects != null) { currentRuleState.ElseActionsActiveRules = AnalyzeSideEffects(ruleSymbols[i].elseSideEffects, ruleSymbols); if ((currentRuleState.ElseActionsActiveRules != null) && (tracer != null)) tracer.TraceElseTriggers(currentRuleState.Rule.Name, currentRuleState.ElseActionsActiveRules, ruleStates); } } } private static ICollection AnalyzeSideEffects(ICollection sideEffects, RuleSymbolInfo[] ruleSymbols) { Dictionary affectedRules = new Dictionary(); for (int i = 0; i < ruleSymbols.Length; ++i) { ICollection dependencies = ruleSymbols[i].conditionDependencies; if (dependencies == null) { continue; } foreach (string sideEffect in sideEffects) { bool match = false; if (sideEffect.EndsWith("*", StringComparison.Ordinal)) { foreach (string dependency in dependencies) { if (dependency.EndsWith("*", StringComparison.Ordinal)) { // Strip the trailing "/*" from the dependency string stripDependency = dependency.Substring(0, dependency.Length - 2); // Strip the trailing "*" from the side-effect string stripSideEffect = sideEffect.Substring(0, sideEffect.Length - 1); string shortString; string longString; if (stripDependency.Length < stripSideEffect.Length) { shortString = stripDependency; longString = stripSideEffect; } else { shortString = stripSideEffect; longString = stripDependency; } // There's a match if the shorter string is a prefix of the longer string. if (longString.StartsWith(shortString, StringComparison.Ordinal)) { match = true; break; } } else { string stripSideEffect = sideEffect.Substring(0, sideEffect.Length - 1); string stripDependency = dependency; if (stripDependency.EndsWith("/", StringComparison.Ordinal)) stripDependency = stripDependency.Substring(0, stripDependency.Length - 1); if (stripDependency.StartsWith(stripSideEffect, StringComparison.Ordinal)) { match = true; break; } } } } else { // The side-effect did not end with a wildcard foreach (string dependency in dependencies) { if (dependency.EndsWith("*", StringComparison.Ordinal)) { // Strip the trailing "/*" string stripDependency = dependency.Substring(0, dependency.Length - 2); string shortString; string longString; if (stripDependency.Length < sideEffect.Length) { shortString = stripDependency; longString = sideEffect; } else { shortString = sideEffect; longString = stripDependency; } // There's a match if the shorter string is a prefix of the longer string. if (longString.StartsWith(shortString, StringComparison.Ordinal)) { match = true; break; } } else { // The side-effect must be a prefix of the dependency (or an exact match). if (dependency.StartsWith(sideEffect, StringComparison.Ordinal)) { match = true; break; } } } } if (match) { affectedRules[i] = null; break; } } } return affectedRules.Keys; } private static RuleSymbolInfo AnalyzeRule(RuleChainingBehavior behavior, Rule rule, RuleValidation validator, Tracer tracer) { RuleSymbolInfo rsi = new RuleSymbolInfo(); if (rule.Condition != null) { rsi.conditionDependencies = rule.Condition.GetDependencies(validator); if ((rsi.conditionDependencies != null) && (tracer != null)) tracer.TraceConditionSymbols(rule.Name, rsi.conditionDependencies); } if (rule.thenActions != null) { rsi.thenSideEffects = GetActionSideEffects(behavior, rule.thenActions, validator); if ((rsi.thenSideEffects != null) && (tracer != null)) tracer.TraceThenSymbols(rule.Name, rsi.thenSideEffects); } if (rule.elseActions != null) { rsi.elseSideEffects = GetActionSideEffects(behavior, rule.elseActions, validator); if ((rsi.elseSideEffects != null) && (tracer != null)) tracer.TraceElseSymbols(rule.Name, rsi.elseSideEffects); } return rsi; } private static ICollection GetActionSideEffects(RuleChainingBehavior behavior, IList actions, RuleValidation validation) { // Man, I wish there were a Set class... Dictionary symbols = new Dictionary(); foreach (RuleAction action in actions) { if ((behavior == RuleChainingBehavior.Full) || ((behavior == RuleChainingBehavior.UpdateOnly) && (action is RuleUpdateAction))) { ICollection sideEffects = action.GetSideEffects(validation); if (sideEffects != null) { foreach (string symbol in sideEffects) symbols[symbol] = null; } } } return symbols.Keys; } #endregion #region Condition Executors internal static bool EvaluateBool(CodeExpression expression, RuleExecution context) { object result = RuleExpressionWalker.Evaluate(context, expression).Value; if (result is bool) return (bool)result; Type expectedType = context.Validation.ExpressionInfo(expression).ExpressionType; if (expectedType == null) { // oops ... not a boolean, so error InvalidOperationException exception = new InvalidOperationException(Messages.ConditionMustBeBoolean); exception.Data[RuleUserDataKeys.ErrorObject] = expression; throw exception; } return (bool)AdjustType(expectedType, result, typeof(bool)); } internal static object AdjustType(Type operandType, object operandValue, Type toType) { // if no conversion required, we are done if (operandType == toType) return operandValue; object converted; if (AdjustValueStandard(operandType, operandValue, toType, out converted)) return converted; // not a standard conversion, see if it's an implicit user defined conversions ValidationError error; MethodInfo conversion = RuleValidation.FindImplicitConversion(operandType, toType, out error); if (conversion == null) { if (error != null) throw new RuleEvaluationException(error.ErrorText); throw new RuleEvaluationException( string.Format(CultureInfo.CurrentCulture, Messages.CastIncompatibleTypes, RuleDecompiler.DecompileType(operandType), RuleDecompiler.DecompileType(toType))); } // now we have a method, need to do the conversion S -> Sx -> Tx -> T Type sx = conversion.GetParameters()[0].ParameterType; Type tx = conversion.ReturnType; object intermediateResult1; if (AdjustValueStandard(operandType, operandValue, sx, out intermediateResult1)) { // we are happy with the first conversion, so call the user's static method object intermediateResult2 = conversion.Invoke(null, new object[] { intermediateResult1 }); object intermediateResult3; if (AdjustValueStandard(tx, intermediateResult2, toType, out intermediateResult3)) return intermediateResult3; } throw new RuleEvaluationException( string.Format(CultureInfo.CurrentCulture, Messages.CastIncompatibleTypes, RuleDecompiler.DecompileType(operandType), RuleDecompiler.DecompileType(toType))); } internal static object AdjustTypeWithCast(Type operandType, object operandValue, Type toType) { // if no conversion required, we are done if (operandType == toType) return operandValue; object converted; if (AdjustValueStandard(operandType, operandValue, toType, out converted)) return converted; // handle enumerations (done above?) // now it's time for implicit and explicit user defined conversions ValidationError error; MethodInfo conversion = RuleValidation.FindExplicitConversion(operandType, toType, out error); if (conversion == null) { if (error != null) throw new RuleEvaluationException(error.ErrorText); throw new RuleEvaluationException( string.Format(CultureInfo.CurrentCulture, Messages.CastIncompatibleTypes, RuleDecompiler.DecompileType(operandType), RuleDecompiler.DecompileType(toType))); } // now we have a method, need to do the conversion S -> Sx -> Tx -> T Type sx = conversion.GetParameters()[0].ParameterType; Type tx = conversion.ReturnType; object intermediateResult1; if (AdjustValueStandard(operandType, operandValue, sx, out intermediateResult1)) { // we are happy with the first conversion, so call the user's static method object intermediateResult2 = conversion.Invoke(null, new object[] { intermediateResult1 }); object intermediateResult3; if (AdjustValueStandard(tx, intermediateResult2, toType, out intermediateResult3)) return intermediateResult3; } throw new RuleEvaluationException( string.Format(CultureInfo.CurrentCulture, Messages.CastIncompatibleTypes, RuleDecompiler.DecompileType(operandType), RuleDecompiler.DecompileType(toType))); } [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] private static bool AdjustValueStandard(Type operandType, object operandValue, Type toType, out object converted) { // assume it's the same for now converted = operandValue; // check for null if (operandValue == null) { // are we converting to a value type? if (toType.IsValueType) { // is the conversion to nullable? if (!ConditionHelper.IsNullableValueType(toType)) { // value type and null, so no conversion possible string message = string.Format(CultureInfo.CurrentCulture, Messages.CannotCastNullToValueType, RuleDecompiler.DecompileType(toType)); throw new InvalidCastException(message); } // here we have a Nullable // however, we may need to call the implicit conversion operator if the types are not compatible converted = Activator.CreateInstance(toType); ValidationError error; return RuleValidation.StandardImplicitConversion(operandType, toType, null, out error); } // not a value type, so null is valid return true; } // check simple cases Type currentType = operandValue.GetType(); if (currentType == toType) return true; // now the fun begins // this should handle most class conversions if (toType.IsAssignableFrom(currentType)) return true; // handle the numerics (both implicit and explicit), along with nullable // note that if the value was null, it's already handled, so value cannot be nullable if ((currentType.IsValueType) && (toType.IsValueType)) { if (currentType.IsEnum) { // strip off the enum representation currentType = Enum.GetUnderlyingType(currentType); ArithmeticLiteral literal = ArithmeticLiteral.MakeLiteral(currentType, operandValue); operandValue = literal.Value; } bool resultNullable = ConditionHelper.IsNullableValueType(toType); Type resultType = (resultNullable) ? Nullable.GetUnderlyingType(toType) : toType; if (resultType.IsEnum) { // Enum.ToObject may throw if currentType is not type SByte, // Int16, Int32, Int64, Byte, UInt16, UInt32, or UInt64. // So we adjust currentValue to the underlying type (which may throw if out of range) Type underlyingType = Enum.GetUnderlyingType(resultType); object adjusted; if (AdjustValueStandard(currentType, operandValue, underlyingType, out adjusted)) { converted = Enum.ToObject(resultType, adjusted); if (resultNullable) converted = Activator.CreateInstance(toType, converted); return true; } } else if ((resultType.IsPrimitive) || (resultType == typeof(decimal))) { // resultType must be a primitive to continue (not a struct) // (enums and generics handled above) if (currentType == typeof(char)) { char c = (char)operandValue; if (resultType == typeof(float)) { converted = (float)c; } else if (resultType == typeof(double)) { converted = (double)c; } else if (resultType == typeof(decimal)) { converted = (decimal)c; } else { converted = ((IConvertible)c).ToType(resultType, CultureInfo.CurrentCulture); } if (resultNullable) converted = Activator.CreateInstance(toType, converted); return true; } else if (currentType == typeof(float)) { float f = (float)operandValue; if (resultType == typeof(char)) { converted = (char)f; } else { converted = ((IConvertible)f).ToType(resultType, CultureInfo.CurrentCulture); } if (resultNullable) converted = Activator.CreateInstance(toType, converted); return true; } else if (currentType == typeof(double)) { double d = (double)operandValue; if (resultType == typeof(char)) { converted = (char)d; } else { converted = ((IConvertible)d).ToType(resultType, CultureInfo.CurrentCulture); } if (resultNullable) converted = Activator.CreateInstance(toType, converted); return true; } else if (currentType == typeof(decimal)) { decimal d = (decimal)operandValue; if (resultType == typeof(char)) { converted = (char)d; } else { converted = ((IConvertible)d).ToType(resultType, CultureInfo.CurrentCulture); } if (resultNullable) converted = Activator.CreateInstance(toType, converted); return true; } else { IConvertible convert = operandValue as IConvertible; if (convert != null) { try { converted = convert.ToType(resultType, CultureInfo.CurrentCulture); if (resultNullable) converted = Activator.CreateInstance(toType, converted); return true; } catch (InvalidCastException) { // not IConvertable, so can't do it return false; } } } } } // no luck with standard conversions, so no conversion done return false; } #endregion } }