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.
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.Override the ValidateCode(object) method.
Create a rule that checks that there's a nested type called
Factory. You'll note that thetargetparameter for the ValidateCode(object) method is anobjecttype. Depending on which target type you declare in the MulticastAttributeUsageAttribute attribute, the value passed through this parameter will change. ForMulticastTargets.Classthe type passed isType. To make use of the target for validation you must cast to that type first. The rule skips abstract classes, such asBusinessRuleitself.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. 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 ); } } }Attach the rule to the code that needs to be protected. In this example, add the rule to the
BusinessRuleclass.[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.
Build the project. A warning appears in the Output window of Visual Studio. In the following code,
DiscountRuledoes not have a nestedFactoryclass: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.
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.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.
Create a class that inherits from the ReferentialConstraint class in PostSharp.
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.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.Create the rule that checks for the use of the default constructor of the
BusinessRuletype 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
assemblyparameter ofValidateCodeis not needed.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 ); } } } } }Attach the referential constraint to the
BusinessRuleclass.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." ); } } }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(); } } }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:
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