Open sandboxFocusImprove this doc

Developing Custom Architectural Constraints

Applications often 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 line-by-line code reviews can miss violations of the pattern. PostSharp lets you create custom architectural constraints to detect them. The constraints that you write can verify anything that you can query using reflection.

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

A scalar constraint applied to a declaration is checked when that declaration is compiled, so in only one assembly. Scalar constraints typically validate a type, member or parameter.

A referential constraint applied to a declaration can be checked many times: once for the assembly where that declaration is, and once for each assembly that uses (references) that declaration. Referential constraints typically validate the usage of a type, member or parameter.

Creating a scalar constraint

Start with a scalar constraint that verifies the first condition of the BusinessRule design pattern: any class derived from BusinessRule must have a nested class named Factory. You can model this condition as a scalar constraint that applies to any class derived from BusinessRule. Therefore, you create a type-level scalar constraint, apply it to the BusinessRule class, and use attribute inheritance to apply the constraint automatically to all derived classes.

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

  2. Specify the kind of code element that this validation aspect targets by adding the MulticastAttributeUsageAttribute attribute. In this case, the validation must occur on types only, and inheritance must be enabled. Strict means that BusinessRulePatternValidation is applied not only to the class that it annotates but also to all of its subclasses, even in other assemblies.

  3. Override the ValidateCode(object) method.

  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.Class the type passed is Type. To make use of the target for validation you must cast to that type first. The rule skips abstract classes, such as BusinessRule itself.

    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 about the broken rule to the Output window in Visual Studio. The complete constraint is the following:

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

    [BusinessRulePatternValidation]
    public abstract class BusinessRule
    {
        // Every non-abstract class derived from BusinessRule must have a nested class named Factory.
    }
    
    Note

    This example applies the constraint to only one class. To apply a constraint to large portions of your codebase, see Adding Aspects to Multiple Declarations Using Attributes.

  7. Build the project. A warning appears in the Output window of Visual Studio. In the following code, DiscountRule does not have a nested Factory class:

    public class CreditLimitRule : BusinessRule
    {
        public class Factory
        {
        }
    }
    
    // Warning: this class does not have a nested class named Factory.
    public class DiscountRule : BusinessRule
    {
    }
    

    The build reports the following warning:

    warning BR001: BusinessRulePattern.cs(46,14): The BusinessRulePattern.DiscountRule type does not have a nested type named 'Factory'.
    
    Note

    Due to a bug, the error code may not be shown in the Error list.

  8. If a warning is not strict enough, you can change the rule so that it emits a build-time error instead. To do this, change the SeverityType in Message.Write to Error.

    Message.Write(
    targetType,
    SeverityType.Error,
    "BR001",
    "The {0} type does not have a nested type named 'Factory'.",
    targetType );
    

    The build now fails with the following error:

    error BR001: BusinessRulePatternError.cs(35,14): The BusinessRulePatternError.DiscountRule type does not have a nested type named 'Factory'.
    

With this technique, you can create rules or restrictions based on many different criteria and implement validation for several design patterns.

Projects must adhere to the principles of the project team. Manual verification fails at some point. As in other areas of the development process, you should automate the verification and enforcement of these principles. Custom architectural constraints provide the flexibility and verification that you need to achieve this goal.

Creating a referential constraint

Now create a referential constraint that verifies the second condition of the BusinessRule design pattern: the BusinessRule class can be used only in the Controllers namespace. You can model this condition as a referential constraint.

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

  2. Declare that this aspect works only on types by adding the MulticastAttributeUsageAttribute attribute to the class. You will apply this class to BusinessRule to prevent all code in the project from using BusinessRule or its subclasses, unless the code is in the Controllers namespace.

  3. Override the ValidateCode(object, Assembly) method. In this case, the target is the BusinessRule class or one of its subclasses. The assembly is the assembly that is currently searched for violations.

  4. Create the rule that checks for the use of the default constructor of the BusinessRule type in all code.

    Note

    The rule uses the ReflectionSearch helper class that PostSharp provides. This class, along with others, extends the built-in reflection functionality of .NET and can be used outside of aspects as well. The method GetMethodsUsingDeclaration(MemberInfo) already searches the current assembly, so the assembly parameter of ValidateCode is not needed.

  5. Write a warning message to the Output window of Visual Studio. The complete constraint is the following:

    namespace BusinessRuleUse
    {
        [MulticastAttributeUsage( MulticastTargets.Class, Inheritance = MulticastInheritance.Strict )]
        public class BusinessRuleUseValidation : ReferentialConstraint
        {
            public override void ValidateCode( object target, Assembly assembly )
            {
                var targetType = (Type) target;
                var usages = ReflectionSearch
                    .GetMethodsUsingDeclaration( targetType.GetConstructor( Type.EmptyTypes ) );
    
                foreach ( MethodUsageCodeReference methodUsageCodeReference in usages )
                {
                    var usingType = methodUsageCodeReference.UsingMethod.DeclaringType;
    
                    if ( usingType?.Namespace == null || !usingType.Namespace.Contains( "Controllers", StringComparison.Ordinal ) )
                    {
                        Message.Write(
                            methodUsageCodeReference.UsingMethod,
                            SeverityType.Warning,
                            "BR002",
                            "The {0} type contains a reference to '{1}' which can only be referenced from Controllers.",
                            usingType,
                            target );
                    }
                }
            }
        }
    }
    
  6. Attach the referential constraint to the BusinessRule class.

    namespace BusinessRuleUse
    {
        [BusinessRuleUseValidation]
        public class BusinessRule
        {
            // Now, only code in the Controllers namespace can call this class's constructor.
    
            public void Execute()
            {
                Console.WriteLine( "Executing the business rule." );
            }
        }
    }
    
  7. Test the rule by adding nonconforming code to your project.

    namespace BusinessRuleUse.Repositories
    {
        public class AccountRepository
        {
            public void AddAccount( string name )
            {
                // Warning: BusinessRule can only be instantiated in the Controllers namespace.
                var businessRule = new BusinessRule();
                businessRule.Execute();
            }
        }
    }
    
  8. Build the project. A warning appears in the Output window in Visual Studio.

    warning BR002: BusinessRuleUse.cs(61,21): The BusinessRuleUse.Repositories.AccountRepository type contains a reference to 'BusinessRuleUse.BusinessRule' which can only be referenced from Controllers.
    
    Note

    If a warning is not strict enough, you can change the SeverityType to Error. When the rule is broken, an error then appears in the Output window of Visual Studio and the build fails.

Caution

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

You can use custom attribute multicasting to apply a constraint to a large number of types, for instance all types of a namespace. But this results in one constraint instance for every type, method and field in this namespace. Although this has no impact on run time, it can 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 let you declare architectural design patterns in your code. These patterns are then documented in the codebase, which makes them easy for the development team to find, and they are continually verified.

Validating the constraint itself

You now have scalar and referential constraints that consistently enforce certain architectural rules in your codebase. One thing is missing.

So far, you can attach your architectural constraints to any code element in your projects. This may not be appropriate. For example, the scalar constraint BusinessRulePatternValidation may be valid only on classes in the Models namespace.

To ensure that this constraint is enforced only on classes 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

    When the ValidateConstraint(object) method returns true, PostSharp applies the constraint to the target code element. When it returns false, PostSharp does 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, no warning or error is added to the Visual Studio Output window.

When the attribute is applied to a class in the Models namespace and that class does not pass the rules of the constraint, the warning or error that indicates this architectural failure is still displayed.

See Also

Reference

MulticastAttributeUsageAttribute
ScalarConstraint
SuppressWarningAttribute
MessageId
Reason

Other Resources

Adding Aspects to Multiple Declarations Using Attributes
Understanding Aspect Inheritance