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.
Create a class that inherits from the ScalarConstraint class in PostSharp.
using System; public class BusinessRulePatternValidation : ScalarConstraint { }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
BusinessRulePatternValidationis applied not only to the class that it annotates but also to all of its subclasses, even in other assemblies.[MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)] public class BusinessRulePatternValidation : ScalarConstraint { }Override the ValidateCode(object) method.
[MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)] public class BusinessRulePatternValidation : ScalarConstraint { public override void ValidateCode(object target) { } }Create a rule that checks that there is a nested type named
Factory. Thetargetparameter of the ValidateCode(object) method is of typeobject. The type of the value passed in this parameter depends on the target kind that you declare in the MulticastAttributeUsageAttribute attribute. ForMulticastTargets.Class, the value is aType. To use the target for validation, first cast it to that type.[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 ) { // warning } } }Note
Valid types for the
targetparameter of the ValidateCode(object) method includeAssembly,Type,MethodInfo,ConstructorInfo,PropertyInfo,EventInfo,FieldInfoandParameterInfo.Write a warning about the broken rule to the Output window in Visual Studio.
[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); } } }Attach the rule to the code that needs to be protected. In this example, add the rule to the
BusinessRuleclass.[BusinessRulePatternValidation] public class BusinessRule { // No nested Factory class: the constraint emits a warning for this class and for any subclass without one. }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.
Build the project. A warning appears in the Output window of Visual Studio.
Note
Due to a bug, the error code may not be shown in the Error list.
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.WritetoError.[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); } } }
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.
Create a class that inherits from the ReferentialConstraint class in PostSharp.
public class BusinessRuleUseValidation : ReferentialConstraint { }Declare that this aspect works only on types by adding the MulticastAttributeUsageAttribute attribute to the class. You will apply this class to
BusinessRuleto prevent all code in the project from usingBusinessRuleor its subclasses, unless the code is in theControllersnamespace.[MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)] public class BusinessRuleUseValidation : ReferentialConstraint { }Override the ValidateCode(object, Assembly) method. In this case, the target is the
BusinessRuleclass or one of its subclasses. The assembly is the assembly that is currently searched for violations.[MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)] public class BusinessRuleUseValidation : ReferentialConstraint { public override void ValidateCode(object target, Assembly assembly) { } }Create the rule that checks for the use of the default constructor of the
BusinessRuletype in all code.[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(new Type[0])); foreach (MethodUsageCodeReference methodUsageCodeReference in usages) { if (!methodUsageCodeReference.UsingMethod.DeclaringType.Namespace.Contains("Controllers")) { // warning } } } }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
assemblyparameter ofValidateCodeis not needed.Write a warning message to the Output window of Visual Studio.
[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(new Type[0])); foreach (MethodUsageCodeReference methodUsageCodeReference in usages) { if (!methodUsageCodeReference.UsingMethod.DeclaringType.Namespace.Contains("Controllers")) { Message.Write( targetType, SeverityType.Warning, "2002", "The {0} type contains a reference to '{1}' which can only be referenced from Controllers.", methodUsageCodeReference.UsingMethod.DeclaringType, target); } } } }Attach the referential constraint to the
BusinessRuleclass.[BusinessRuleUseValidation] public class BusinessRule { // Now, only code in the Controllers namespace can call this class's constructor. }Test the rule by adding nonconforming code to your project.
namespace PostSharp.Architecture.Repositories { public class AccountRepository { public void AddAccount(string name) { var businessRule = new BusinessRule(); } } }Build the project. A warning appears in the Output window in Visual Studio.
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:
Open the
BusinessRulePatternValidationclass that you created earlier.Override the ValidateConstraint(object) method.
Write the validation logic to ensure that this constraint is only applied to classes in the
Modelsnamespace.Note
When the ValidateConstraint(object) method returns
true, PostSharp applies the constraint to the target code element. When it returnsfalse, 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