e79aa3c0ed
Former-commit-id: a2155e9bd80020e49e72e86c44da02a8ac0e57a4
452 lines
20 KiB
C#
452 lines
20 KiB
C#
// ---------------------------------------------------------------------------
|
|
// Copyright (C) 2005 Microsoft Corporation All Rights Reserved
|
|
// ---------------------------------------------------------------------------
|
|
|
|
using System;
|
|
using System.CodeDom;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Workflow.ComponentModel;
|
|
using System.Workflow.ComponentModel.Compiler;
|
|
using System.Reflection;
|
|
using System.Workflow.Activities.Common;
|
|
|
|
|
|
namespace System.Workflow.Activities.Rules
|
|
{
|
|
public enum RuleAttributeTarget
|
|
{
|
|
Parameter,
|
|
This
|
|
}
|
|
|
|
public abstract class RuleAttribute : Attribute
|
|
{
|
|
internal abstract bool Validate(RuleValidation validation, MemberInfo member, Type contextType, ParameterInfo[] parameters);
|
|
internal abstract void Analyze(RuleAnalysis analysis, MemberInfo member, CodeExpression targetExpression, RulePathQualifier targetQualifier, CodeExpressionCollection argumentExpressions, ParameterInfo[] parameters, List<CodeExpression> attributedExpressions);
|
|
}
|
|
|
|
public abstract class RuleReadWriteAttribute : RuleAttribute
|
|
{
|
|
private RuleAttributeTarget attributeTarget;
|
|
private string attributePath;
|
|
|
|
protected RuleReadWriteAttribute(string path, RuleAttributeTarget target)
|
|
{
|
|
this.attributeTarget = target;
|
|
this.attributePath = path;
|
|
}
|
|
|
|
public string Path
|
|
{
|
|
get { return attributePath; }
|
|
}
|
|
|
|
public RuleAttributeTarget Target
|
|
{
|
|
get { return attributeTarget; }
|
|
}
|
|
|
|
internal override bool Validate(RuleValidation validation, MemberInfo member, Type contextType, ParameterInfo[] parameters)
|
|
{
|
|
ValidationError error = null;
|
|
string message = null;
|
|
|
|
if (string.IsNullOrEmpty(attributePath))
|
|
{
|
|
// It is allowed to pass null or the empty string to [RuleRead] or [RuleWrite]. This
|
|
// is how you indicate that a method or property has no dependencies or side effects.
|
|
return true;
|
|
}
|
|
|
|
bool valid = true;
|
|
|
|
string[] parts = attributePath.Split('/');
|
|
|
|
// Check the first part.
|
|
|
|
string firstPart = parts[0];
|
|
int startOfRelativePortion = 0;
|
|
if (attributeTarget == RuleAttributeTarget.This)
|
|
{
|
|
// When target is "This", the path is allowed to start with the token "this". It is
|
|
// then skipped for the rest of the validation, and the contextType remains what it
|
|
// was when passed in.
|
|
if (firstPart == "this")
|
|
++startOfRelativePortion;
|
|
}
|
|
else
|
|
{
|
|
// When target is "Parameter", the path must start with the name of a parameter.
|
|
bool found = false;
|
|
for (int p = 0; p < parameters.Length; ++p)
|
|
{
|
|
ParameterInfo param = parameters[p];
|
|
if (param.Name == firstPart)
|
|
{
|
|
found = true;
|
|
|
|
// The context type is the parameter type.
|
|
contextType = param.ParameterType;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
message = string.Format(CultureInfo.CurrentCulture, Messages.InvalidRuleAttributeParameter, firstPart, member.Name);
|
|
error = new ValidationError(message, ErrorNumbers.Error_InvalidRuleAttributeParameter);
|
|
error.UserData[RuleUserDataKeys.ErrorObject] = this;
|
|
validation.AddError(error);
|
|
return false;
|
|
}
|
|
|
|
++startOfRelativePortion;
|
|
}
|
|
|
|
int numParts = parts.Length;
|
|
|
|
// Check the last part. The last part is allowed to be empty, or "*".
|
|
|
|
string lastPart = parts[numParts - 1];
|
|
if (string.IsNullOrEmpty(lastPart) || lastPart == "*")
|
|
numParts -= 1;
|
|
|
|
// Check the rest of the parts.
|
|
|
|
Type currentType = contextType;
|
|
for (int i = startOfRelativePortion; i < numParts; ++i)
|
|
{
|
|
// Can't have embedded "*" wildcards.
|
|
if (parts[i] == "*")
|
|
{
|
|
// The "*" occurred in the middle of the path, which is a no-no.
|
|
error = new ValidationError(Messages.InvalidWildCardInPathQualifier, ErrorNumbers.Error_InvalidWildCardInPathQualifier);
|
|
error.UserData[RuleUserDataKeys.ErrorObject] = this;
|
|
validation.AddError(error);
|
|
valid = false;
|
|
break;
|
|
}
|
|
|
|
// Skip array types.
|
|
while (currentType.IsArray)
|
|
currentType = currentType.GetElementType();
|
|
|
|
// Make sure the member exists in the current type.
|
|
BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy;
|
|
if (validation.AllowInternalMembers(currentType))
|
|
bindingFlags |= BindingFlags.NonPublic;
|
|
|
|
FieldInfo field = currentType.GetField(parts[i], bindingFlags);
|
|
if (field != null)
|
|
{
|
|
currentType = field.FieldType;
|
|
}
|
|
else
|
|
{
|
|
PropertyInfo property = currentType.GetProperty(parts[i], bindingFlags);
|
|
if (property != null)
|
|
{
|
|
currentType = property.PropertyType;
|
|
}
|
|
else
|
|
{
|
|
message = string.Format(CultureInfo.CurrentCulture, Messages.UpdateUnknownFieldOrProperty, parts[i]);
|
|
error = new ValidationError(message, ErrorNumbers.Error_UnknownFieldOrProperty);
|
|
error.UserData[RuleUserDataKeys.ErrorObject] = this;
|
|
validation.AddError(error);
|
|
valid = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
internal void AnalyzeReadWrite(RuleAnalysis analysis, CodeExpression targetExpression, RulePathQualifier targetQualifier, CodeExpressionCollection argumentExpressions, ParameterInfo[] parameters, List<CodeExpression> attributedExpressions)
|
|
{
|
|
if (string.IsNullOrEmpty(attributePath))
|
|
{
|
|
// If the suffix is null or empty, this means the RuleAttributeTarget has no dependencies.
|
|
if (attributeTarget == RuleAttributeTarget.This)
|
|
{
|
|
// The target object has no dependencies.
|
|
attributedExpressions.Add(targetExpression);
|
|
}
|
|
else if (attributeTarget == RuleAttributeTarget.Parameter)
|
|
{
|
|
// ALL arguments have no dependencies.
|
|
for (int i = 0; i < argumentExpressions.Count; ++i)
|
|
attributedExpressions.Add(argumentExpressions[i]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string suffix = attributePath;
|
|
|
|
bool isRead = !analysis.ForWrites;
|
|
bool isWrite = analysis.ForWrites;
|
|
|
|
if (attributeTarget == RuleAttributeTarget.This)
|
|
{
|
|
// Target is "This", so perform the analysis on the target expression.
|
|
|
|
// Remove the optional "this/" token if present.
|
|
string optionalPrefix = "this/";
|
|
if (suffix.StartsWith(optionalPrefix, StringComparison.Ordinal))
|
|
suffix = suffix.Substring(optionalPrefix.Length);
|
|
|
|
RuleExpressionWalker.AnalyzeUsage(analysis, targetExpression, isRead, isWrite, new RulePathQualifier(suffix, targetQualifier));
|
|
attributedExpressions.Add(targetExpression);
|
|
}
|
|
else if (attributeTarget == RuleAttributeTarget.Parameter)
|
|
{
|
|
string paramName = null;
|
|
|
|
int firstSlash = suffix.IndexOf('/');
|
|
if (firstSlash >= 0)
|
|
{
|
|
paramName = suffix.Substring(0, firstSlash);
|
|
suffix = suffix.Substring(firstSlash + 1);
|
|
}
|
|
else
|
|
{
|
|
paramName = suffix;
|
|
suffix = null;
|
|
}
|
|
|
|
// Find the ParameterInfo that corresponds to this attribute path.
|
|
ParameterInfo param = Array.Find<ParameterInfo>(parameters,
|
|
delegate(ParameterInfo p) { return p.Name == paramName; });
|
|
if (param != null)
|
|
{
|
|
RulePathQualifier qualifier = string.IsNullOrEmpty(suffix) ? null : new RulePathQualifier(suffix, null);
|
|
|
|
// 99.9% of the time, the parameter usage attribute only applies to one argument. However,
|
|
// if this attribute corresponds to the last parameter, then just assume that all the trailing
|
|
// arguments correspond. (In other words, if the caller passed more arguments then there
|
|
// are parameters, we assume it was a params array.)
|
|
//
|
|
// Usually this loop will only execute once.
|
|
int end = param.Position + 1;
|
|
if (param.Position == parameters.Length - 1)
|
|
end = argumentExpressions.Count;
|
|
|
|
for (int i = param.Position; i < end; ++i)
|
|
{
|
|
CodeExpression argExpr = argumentExpressions[i];
|
|
RuleExpressionWalker.AnalyzeUsage(analysis, argExpr, isRead, isWrite, qualifier);
|
|
attributedExpressions.Add(argExpr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = true)]
|
|
public sealed class RuleReadAttribute : RuleReadWriteAttribute
|
|
{
|
|
public RuleReadAttribute(string path, RuleAttributeTarget target)
|
|
: base(path, target)
|
|
{
|
|
}
|
|
|
|
public RuleReadAttribute(string path)
|
|
: base(path, RuleAttributeTarget.This)
|
|
{
|
|
}
|
|
|
|
internal override void Analyze(RuleAnalysis analysis, MemberInfo member, CodeExpression targetExpression, RulePathQualifier targetQualifier, CodeExpressionCollection argumentExpressions, ParameterInfo[] parameters, List<CodeExpression> attributedExpressions)
|
|
{
|
|
// A RuleRead attribute is only applicable if we're analyzing for reads.
|
|
if (analysis.ForWrites)
|
|
return;
|
|
|
|
base.AnalyzeReadWrite(analysis, targetExpression, targetQualifier, argumentExpressions, parameters, attributedExpressions);
|
|
}
|
|
}
|
|
|
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = true)]
|
|
public sealed class RuleWriteAttribute : RuleReadWriteAttribute
|
|
{
|
|
public RuleWriteAttribute(string path, RuleAttributeTarget target)
|
|
: base(path, target)
|
|
{
|
|
}
|
|
|
|
public RuleWriteAttribute(string path)
|
|
: base(path, RuleAttributeTarget.This)
|
|
{
|
|
}
|
|
|
|
internal override void Analyze(RuleAnalysis analysis, MemberInfo member, CodeExpression targetExpression, RulePathQualifier targetQualifier, CodeExpressionCollection argumentExpressions, ParameterInfo[] parameters, List<CodeExpression> attributedExpressions)
|
|
{
|
|
// A RuleWrite attribute is only applicable if we're analyzing for writes.
|
|
if (!analysis.ForWrites)
|
|
return;
|
|
|
|
base.AnalyzeReadWrite(analysis, targetExpression, targetQualifier, argumentExpressions, parameters, attributedExpressions);
|
|
}
|
|
}
|
|
|
|
|
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = true)]
|
|
public sealed class RuleInvokeAttribute : RuleAttribute
|
|
{
|
|
private string methodInvoked;
|
|
|
|
public RuleInvokeAttribute(string methodInvoked)
|
|
{
|
|
this.methodInvoked = methodInvoked;
|
|
}
|
|
|
|
public string MethodInvoked
|
|
{
|
|
get { return methodInvoked; }
|
|
}
|
|
|
|
internal override bool Validate(RuleValidation validation, MemberInfo member, Type contextType, ParameterInfo[] parameters)
|
|
{
|
|
Stack<MemberInfo> methodStack = new Stack<MemberInfo>();
|
|
methodStack.Push(member);
|
|
|
|
bool result = ValidateInvokeAttribute(validation, member, contextType, methodStack);
|
|
|
|
methodStack.Pop();
|
|
|
|
return result;
|
|
}
|
|
|
|
private bool ValidateInvokeAttribute(RuleValidation validation, MemberInfo member, Type contextType, Stack<MemberInfo> methodStack)
|
|
{
|
|
string message;
|
|
ValidationError error;
|
|
|
|
if (string.IsNullOrEmpty(methodInvoked))
|
|
{
|
|
// Invoked method or property name was null or empty.
|
|
message = string.Format(CultureInfo.CurrentCulture, Messages.AttributeMethodNotFound, member.Name, this.GetType().Name, Messages.NullValue);
|
|
error = new ValidationError(message, ErrorNumbers.Warning_RuleAttributeNoMatch, true);
|
|
error.UserData[RuleUserDataKeys.ErrorObject] = this;
|
|
validation.AddError(error);
|
|
return false;
|
|
}
|
|
|
|
bool valid = true;
|
|
|
|
// Go through all the methods and properties on the target context,
|
|
// looking for all the ones that match the name on the attribute.
|
|
MemberInfo[] members = contextType.GetMember(methodInvoked, MemberTypes.Method | MemberTypes.Property, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
|
|
|
|
if (members == null || members.Length == 0)
|
|
{
|
|
// Invoked method or property didn't exist.
|
|
message = string.Format(CultureInfo.CurrentCulture, Messages.AttributeMethodNotFound, member.Name, this.GetType().Name, methodInvoked);
|
|
error = new ValidationError(message, ErrorNumbers.Warning_RuleAttributeNoMatch, true);
|
|
error.UserData[RuleUserDataKeys.ErrorObject] = this;
|
|
validation.AddError(error);
|
|
valid = false;
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < members.Length; ++i)
|
|
{
|
|
MemberInfo mi = members[i];
|
|
if (!methodStack.Contains(mi)) // Prevent recursion
|
|
{
|
|
methodStack.Push(mi);
|
|
|
|
object[] attrs = mi.GetCustomAttributes(typeof(RuleAttribute), true);
|
|
if (attrs != null && attrs.Length != 0)
|
|
{
|
|
foreach (RuleAttribute invokedRuleAttr in attrs)
|
|
{
|
|
RuleReadWriteAttribute readWriteAttr = invokedRuleAttr as RuleReadWriteAttribute;
|
|
if (readWriteAttr != null)
|
|
{
|
|
// This read/write attribute may not specify a target of "Parameter", since
|
|
// we can't map from the invoker's parameters to the invokee's parameters.
|
|
if (readWriteAttr.Target == RuleAttributeTarget.Parameter)
|
|
{
|
|
message = string.Format(CultureInfo.CurrentCulture, Messages.InvokeAttrRefersToParameterAttribute, mi.Name);
|
|
error = new ValidationError(message, ErrorNumbers.Error_InvokeAttrRefersToParameterAttribute, true);
|
|
error.UserData[RuleUserDataKeys.ErrorObject] = this;
|
|
validation.AddError(error);
|
|
valid = false;
|
|
}
|
|
else
|
|
{
|
|
// Validate the read/write attribute normally.
|
|
readWriteAttr.Validate(validation, mi, contextType, null);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RuleInvokeAttribute invokeAttr = (RuleInvokeAttribute)invokedRuleAttr;
|
|
invokeAttr.ValidateInvokeAttribute(validation, mi, contextType, methodStack);
|
|
}
|
|
}
|
|
}
|
|
|
|
methodStack.Pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
internal override void Analyze(RuleAnalysis analysis, MemberInfo member, CodeExpression targetExpression, RulePathQualifier targetQualifier, CodeExpressionCollection argumentExpressions, ParameterInfo[] parameters, List<CodeExpression> attributedExpressions)
|
|
{
|
|
Stack<MemberInfo> methodStack = new Stack<MemberInfo>();
|
|
methodStack.Push(member);
|
|
|
|
AnalyzeInvokeAttribute(analysis, member.DeclaringType, methodStack, targetExpression, targetQualifier, argumentExpressions, parameters, attributedExpressions);
|
|
|
|
methodStack.Pop();
|
|
}
|
|
|
|
private void AnalyzeInvokeAttribute(RuleAnalysis analysis, Type contextType, Stack<MemberInfo> methodStack, CodeExpression targetExpression, RulePathQualifier targetQualifier, CodeExpressionCollection argumentExpressions, ParameterInfo[] parameters, List<CodeExpression> attributedExpressions)
|
|
{
|
|
// Go through all the methods and properties on the target context,
|
|
// looking for all the ones that match the name on the attribute.
|
|
MemberInfo[] members = contextType.GetMember(methodInvoked, MemberTypes.Method | MemberTypes.Property, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
|
|
|
|
for (int m = 0; m < members.Length; ++m)
|
|
{
|
|
MemberInfo mi = members[m];
|
|
if (!methodStack.Contains(mi)) // Prevent recursion
|
|
{
|
|
methodStack.Push(mi);
|
|
|
|
object[] attrs = mi.GetCustomAttributes(typeof(RuleAttribute), true);
|
|
if (attrs != null && attrs.Length != 0)
|
|
{
|
|
RuleAttribute[] ruleAttrs = (RuleAttribute[])attrs;
|
|
for (int i = 0; i < ruleAttrs.Length; ++i)
|
|
{
|
|
RuleAttribute ruleAttr = ruleAttrs[i];
|
|
|
|
RuleReadWriteAttribute readWriteAttr = ruleAttr as RuleReadWriteAttribute;
|
|
if (readWriteAttr != null)
|
|
{
|
|
// Just analyze the read/write attribute normally.
|
|
readWriteAttr.Analyze(analysis, mi, targetExpression, targetQualifier, argumentExpressions, parameters, attributedExpressions);
|
|
}
|
|
else
|
|
{
|
|
RuleInvokeAttribute invokeAttr = (RuleInvokeAttribute)ruleAttr;
|
|
invokeAttr.AnalyzeInvokeAttribute(analysis, contextType, methodStack, targetExpression, targetQualifier, argumentExpressions, parameters, attributedExpressions);
|
|
}
|
|
}
|
|
}
|
|
|
|
methodStack.Pop();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|