This is the online documentation for PostSharp 5.0.
Download PDF or CHM. Go to v4.3 or v5.0

Developing Custom Architectural Constraints

When you are creating your applications it is common to adopt custom design patterns that must be respected across all modules. Custom design patterns have the same benefits as standard ones, but they are specific to your application. For instance, the team could decide that every class derived from BusinessRule must have a nested class named Factory, derived from BusinessRulesFactory, with a public default constructor.

Even performing line-by-line code reviews can miss violations of the pattern. Is there a better way to ensure that this doesn't happen? PostSharp offers the ability create custom architectural constraints. The constraints that you write are able to verify anything that you can query using reflection.

There are two kinds of constraints: scalar constraints and referential constraints.

This topic contains the following sections:

Creating a scalar constraint

Scalar constraints typically validate an element of code, while referential constraints validate how an element of code is being used.

Let's start with a scalar constraint and create a constraint that verifies the first condition our BusinessRule design pattern: that any class derived from BusinessRule must have a nested class named Factory. We can model this condition as a scalar constraint that applies to any class derived from BusinessRule. Therefore, we will create a type-level scalar constraint, apply it to the BusinessRule class, and use attribute inheritance to have the constraint automatically applied to all derived classes.

  1. Create a class that inherits from the ScalarConstraint class in PostSharp.

    C#
    using System; 
    public class BusinessRulePatternValidation : ScalarConstraint 
    { 
    }
  2. Designate what code construct type this validation aspect should work for by adding the MulticastAttributeUsageAttribute attribute. In this case we want the validation to occur on types only, and we want to enable inheritance.

    C#
    [MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)] 
    public class BusinessRulePatternValidation : ScalarConstraint 
    { 
    }
  3. Override the ValidateCode(Object) method.

    C#
    [MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)] 
    public class BusinessRulePatternValidation : ScalarConstraint 
    { 
        public override void ValidateCode(object target) 
        { 
        } 
    }
  4. Create a rule that checks that there's a nested type called Factory. You'll note that the target parameter for the ValidateCode(Object) method is an object type. Depending on which target type you declare in the MulticastAttributeUsageAttribute attribute, the value passed through this parameter will change. For MulticastTargets.Type the type passed is Type. To make use of the target for validation you must cast to that type first.

    C#
    [MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)] 
    public class BusinessRulePatternValidation : ScalarConstraint 
    { 
        public override void ValidateCode(object target) 
        { 
            var targetType = (Type) target; 
    
            if ( targetType.GetNestedType("Factory") == null ) 
            { 
               // Error 
            } 
        } 
    }
    Note Note

    Valid types for the target parameter of the ValidateCode(Object) method include Assembly, Type, MethodInfo, ConstructorInfo, PropertyInfo, EventInfo, FieldInfo, and ParameterInfo.

  5. Write a warning that the rule being broken to the Output window in Visual Studio.

    C#
    [MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)] 
    public class BusinessRulePatternValidation : ScalarConstraint 
    { 
        public override void ValidateCode(object target) 
        { 
            var targetType = (Type)target; 
    
            if (targetType.GetNestedType("Factory") == null) 
            { 
                Message.Write( 
                    targetType, SeverityType.Warning, 
                    "2001", 
                    "The {0} type does not have a nested type named 'Factory'.", 
                    targetType.DeclaringType, 
                     targetType.Name); 
            } 
        } 
    }
  6. Attach the rule to the code that needs to be protected. For this example we want to add this rule to the BusinessRule class.

    C#
    [BusinessRulePatternValidation] 
    public class BusinessRule 
    { 
        // No Factory class here. 
    }
    Note Note

    This example shows applying the constraint to only one class. If you want to apply a constraint to large portions of your codebase, read the section on Adding Aspects to Multiple Declarations

  7. Now if you compile the project you will see an error in the Output window of Visual Studio when you run a build.

    Custom Arch Constraint warning
  8. In some circumstances you may determine that a warning isn't aggressive enough. We can alter the rule that you have created so that it outputs a compile time error instead. All that you need to do is change the SeverityType in the Message.Write to Error.

    C#
    [MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)] 
    public class BusinessRulePatternValidation : ScalarConstraint 
    { 
        public override void ValidateCode(object target) 
        { 
            var targetType = (Type)target; 
    
            if (targetType.GetNestedType("Factory") == null) 
            { 
                Message.Write( 
                targetType, SeverityType.Error, 
                "2001", 
                "The {0} type does not have a nested type named 'Factory'.", 
                targetType.DeclaringType, 
                targetType.Name); 
            } 
        } 
    }
    Custom Arch Constraint error

Using this technique it is possible to create rules or restrictions based on a number of different criteria and implement validation for several design patterns.

When you are working on projects you need to ensure that they adhere to the ideals and principles that our project teams hold dear. As with any process in software development, manual verification is guaranteed to fail at some point in time. As you do in other areas of the development process, you should look to automate the verification and enforcement of our ideals. The ability to create custom architectural constraints provides both the flexibility and verification that you need to achieve this goal.

Creating a referential constraint

Now let's create a referential constraint that verifies the second condition our BusinessRule design pattern: that the BusinessRule class can only be used in the Controllers namespace. You can model this condition as a referential constraint and apply the constraint to any class in your codebase. If you apply this constraint to the entirety of your codebase you will ensure that the BusinessRule design pattern is only referenced in the Controllers namespace.

  1. Create a class that inherits from the ReferentialConstraint class in PostSharp.

    C#
    public class BusinessRuleUseValidation : ReferentialConstraint 
    { 
    }
  2. Declare that this aspect should work only on types by adding the MulticastAttributeUsageAttribute attribute to the class.

    C#
    [MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)] 
    public class BusinessRuleUseValidation : ReferentialConstraint 
    { 
    }
  3. Override the ValidateCode(Object, Assembly) method.

    C#
    [MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)] 
    BusinessRuleUseValidation : ReferentialConstraint 
    { 
        public override void ValidateCode(object target, Assembly assembly) 
        { 
        } 
    }
  4. Create the rule that checks for the use of the BusinessRule type in the target code.

    C#
    [MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)] 
    public class BusinessRulePatternValidation : ScalarConstraint 
    { 
        public override void ValidateCode(object target, Assembly assembly) 
        { 
            var targetType = (Type) target; 
            var usages = ReflectionSearch 
                    .GetMethodsUsingDeclaration(typeof (BusinessRule)); 
    
            if (usages !=null) 
            { 
                // Warning 
            } 
        } 
    }
    Note Note

    The rule here makes use of the ReflectionSearch helper class that is provided by the PostSharp framework. This class, along with others, is an extension to the built in reflection functionality of .NET and can be used outside of aspects as well.

  5. Write a warning message to be included in the Output window of Visual Studio.

    C#
    [MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)] 
    public class BusinessRulePatternValidation : ScalarConstraint 
    { 
        public override void ValidateCode(object target, Assembly assembly) 
        { 
            var targetType = (Type) target; 
            var usages = ReflectionSearch 
            .GetMethodsUsingDeclaration(typeof (BusinessRule)); 
    
            if (usages !=null) 
            { 
                Message.Write( 
                    targetType, SeverityType.Warning,  
                    "2002", 
                    "The {0} type contains a reference to 'BusinessRule'" + 
                    "which should only be referenced from Controllers.", 
                    targetType.Name); 
            } 
        } 
    }
  6. Attach the referential constraint that you created to any code that needs to be checked. In this example, add an attribute to the AccountRepository class.

    C#
    namespace PostSharp.Architecture.Repositories 
    { 
        [BusinessRuleUseValidation] 
        public class AccountRepository 
        { 
            public void AddAccount(string name) 
            { 
                var businessRule = new BusinessRule(); 
                businessRule.DoStuff(); 
            } 
        } 
    }
    Note Note

    This example shows applying the constraint to only one class. If you want to apply this constraint to a larger portion of your codebase, read the section on Adding Aspects to Multiple Declarations.

  7. Now when you compile the project you will see a warning in the Output window in Visual Studio.

    Referential Constraint Warning
    Note Note

    If using a warning isn't aggressive enough you can change the SeverityType to Error. Now when the rule is broken an error will appear in the Output window of Visual Studio and the build will not be successful.

Caution note Caution

PostSharp constraints operate at the lowest level. For instance, checking relationships of a type with the rest of the code does not implicitly check the relationships of the methods of this type. Also, checking relationships of namespaces is not possible.

Custom attribute multicasting can be used to apply a constraint to a large number of types, for instance all types of a namespace. But this would result in one constraint instance for every type, method and field on this namespace. Altough this has no impact on run time, it could severely affect build time. For this reason, the current version of PostSharp Constraints is not suitable to check isolation (layering) of namespaces at large scale.

Referential constraints provide you with the ability to declare architectural design patterns right in your code. By documenting these patterns right in the codebase you are able to provide easy access for the development team as well as continual verification that your desired design patterns are being adhered to.

Validating the constraint itself

Now that you have created scalar and referential constraints you can be assured that certain architectural rules are being consistently implemented in your codebase. There is one thing that is missing though.

With what you have done thus far, it is possible to attach your architectural constraints to any code element in your projects. This may not be appropriate. For example, the scalar constraint that you created to perform the BusinessRulePatternValidation may be a valid constraint only on classes that exist in the Models namespace.

Let's look at how we can ensure that this constraint is only enforced on classes that exist in the Models namespace.

  1. Open the BusinessRulePatternValidation class that you created earlier.

  2. Override the ValidateConstraint(Object) method.

  3. Write the validation logic to ensure that this constraint is only applied to classes in the Models namespace.

    Note Note

    When the ValidateConstraint(Object) method returns true, it tells PostSharp that the constraint should be applied to that target code element. When the ValidateConstraint(Object) method returns false PostSharp will not apply the constraint to the target code element.

Now, when the BusinessRulePatternValidation attribute is applied to a class that is not in the Models namespace of your project, there will be no warning or error added to the Visual Studio Output window.

When the attribute is applied to a class in the Models namespace and that class doesn't pass the constraint's rules you will continue to see the warning or error indicating this architectural failure.

Ignoring warnings

There will be situations where a constraint is generating a warning that is of no concern. In these exceptional circumstances it is best if you remove the warning from the Visual Studio Output window.

To ignore these unnecessary warnings, find the target code that is responsible for generating the warning. Add the IgnoreWarningAttribute attribute to the target code entering the MessageId of the warning that you want to suppress.

The MessageId can be found in your constraint where you issue the Message.Write command. The Reason value performs no function during the suppression of the warning. It exists so that you can provide clear communication as to why the warning is being ignored.

Note Note

The IgnoreWarningAttribute attribute will only suppress the issuance of Message.Write statements that are assigned a SeverityType of Warning. If the SeverityType is set to Error the IgnoreWarningAttribute attribute will have no suppression effect on that statement.

See Also