How to validate the object state in method or property entry using PostSharp?

243 Views Asked by At

How can I validate the object state (example: value of a bool field) in a method entry using PostSharp?

Is that also possible for a property getter or setter? Possible for auto-properties too?

I know how to validate method parameters via custom contracts and how to intercept a method via methodboundry but I don't know how to pass a state validation rule from aspect attribute to method entry body.

My use case:

I want all initialized checks in Method1, 2 and 3 to be handled with an aspect.

Without aspect:

class MyClass
{
   bool Initialized;

   void Init()
   {
       //do stuff;
       Initialized = true;
   }

   void Method1()
   {
       if (!Initialized) throw AwesomeException("Awesome text");

       //do stuff;
   }

   void Method2()
   {
       if (!Initialized) throw AwesomeException("Awesome text");

       //do stuff;
   }

   void Method3()
   {
       if (!Initialized) throw AwesomeException("Awesome text");

       //do stuff;
   }
}

with aspect:

   [Conditional( <<somehow define condition field here>> Initialized, "Awesome text"]
   void Method1()
   {
       //do stuff;
   }

   [Conditional( <<magic>> Initialized, "Awesome text"]
   void Method2()
   {
       //do stuff;
   }

   [Conditional( <<magic>> Initialized, "Awesome text"]
   void Method3()
   {
       //do stuff;
   }
1

There are 1 best solutions below

2
On BEST ANSWER

Solution requires several a little bit advanced PostSharp features.

You need to be able to access a "condition field" from your aspect and you need to be able to configure which field is the "condition field". Unfortunately C# allows you to specify constant expressions as attribute arguments. The only way (I know about) is using a string to specify the "condition field" name:

   [Conditional( "Initialized", "Awesome text"]
   void Method1()
   {
       //do stuff;
   }

The problem is that intellisense doesn't work with strings but you can validate existence of the field in your aspect.

You can import any field of a target class by using ImportLocationAdviceInstance and implementing IAdviceProvider:

[PSerializable]
public class ConditionalAttribute : InstanceLevelAspect, IAdviceProvider
{
    private string conditionFieldName;
    private string awesomeText;
    public ILocationBinding ConditionBindingField;

    public ConditionalAttribute(string conditionFieldName, string awesomeText)
    {
        this.conditionFieldName = conditionFieldName;
        this.awesomeText = awesomeText;
    }

    public IEnumerable<AdviceInstance> ProvideAdvices(object targetElement)
    {
        var targetType = (Type) targetElement;
        var bindingFieldInfo = this.GetType().GetField("ConditionBindingField", BindingFlags.Instance | BindingFlags.Public);

        foreach (
            var field in
            targetType.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public |
                                 BindingFlags.NonPublic))
        {
            if (field.Name == conditionFieldName)
            {
                if (field.FieldType.IsAssignableFrom(typeof(bool)))
                {
                    yield return new ImportLocationAdviceInstance(bindingFieldInfo, new LocationInfo(field));
                    yield break;
                }
                {
                    Message.Write(MessageLocation.Of(targetType), SeverityType.Error, "ERR002", $"{targetType.Name} contains {field.FieldType.Name} {conditionFieldName}. {conditionFieldName} has to be bool.");
                    yield break;
                }
            }
        }

        Message.Write(MessageLocation.Of(targetType), SeverityType.Error, "ERR001", $"{targetType.Name} doesn't contain {conditionFieldName}");
    }
}

Now, ConditionBindingField either contains binding to the "condition field", or PostSharp emits ERR001 or ERR002 if the "condition field" of given name doesn't exists or is declared with other type than bool.

The next step is intercept target class methods with validation code:

[OnMethodInvokeAdvice]
[MethodPointcut("SelectMethods")]
public void OnInvoke(MethodInterceptionArgs args)
{
    bool conditionFieldValue = (bool)ConditionBindingField.GetValue(args.Instance);
    if (!conditionFieldValue)
    {
        throw new InvalidOperationException(awesomeText);
    }

    args.Proceed(); // call original method body
}

PostSharp intercepts each method provided by SelectMethods method with the code in OnInvoke method. The problem is that you don't want to intercept Init methods with the "condition field" check, otherwise the call to this method would throw an exception with the "Awesome text" and it would not be possible to initialize a class. So you have to mark somehow methods that cannot be intercepted. You can either use convention and give the same name to all Init methods or you can mark Init method with an attribute:

[AttributeUsage(AttributeTargets.Method)]
public class InitAttribute : Attribute
{
}

You can use MethodPointcut to select public instance methods without Init attribute:

private IEnumerable<MethodInfo> SelectMethods(Type type)
{
    const BindingFlags bindingFlags = BindingFlags.Instance |
        BindingFlags.DeclaredOnly | BindingFlags.Public;

    return type.GetMethods(bindingFlags)
        .Where(m => !m.GetCustomAttributes(typeof(InitAttribute)).Any());
}

Conditional aspect usage example:

[Conditional("Initialized", "Awesome text")]
class MyClass
{
   bool Initialized;

   [Init]
   void Init()
   {
       Initialized = true;
   }

   void Method1()
   {
       //do stuff;
   }
}

EDIT: It is possible to mark "condition field" by an attribute instead of specifying its name as string:

[Conditional("Awesome text")]
class MyClass
{
   [ConditionField]
   bool Initialized;

   [Init]
   void Init()
   {
       Initialized = true;
   }

   void Method1()
   {
       //do stuff;
   }
}