We in the Microsoft .NET community are lucky: at critical moments, we are a few steps
behind the Java community. So we got C# and .NET after them, but we learned from
their mistakes, and got better language and platform. The same with AOP. AspectJ is widely used
over there, there is
plently of feedback, and PostSharp has been designed accordingly.
One of the questions engineers face when applying aspects to large software
is: how do you ensure aspects don't collide? This was a key design concern for
PostSharp 2.0; here I'll show how it's addressed. Be confident that this
feature puts PostSharp at the peek of all industrial aspect weavers, without
distinction of language.
Ordering Aspects
Let assume the following. We have two aspects: caching and authorization.
Obviously, we want authorization to be performed before caching.
I skip the code of the aspects. It's not the point here. But let see how we
define dependencies between aspects:
[Serializable]
[ProvideAspectRole(StandardRoles.Caching)]
public class CacheAttribute : OnMethodBoundaryAspect
{
// Implementation omitted.
}
[Serializable]
[ProvideAspectRole(StandardRoles.Security)]
[AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.Before, StandardRoles.Caching)]
public class AuthorizationAttribute : OnMethodBoundaryAspect
{
// Implementation omitted.
}
It's fairly simple. There are only two concepts:
- Aspects can provide a role. A role describes the
function of the aspect; represented as a unique string. The class
StandardRoles is a list of the most common aspect roles you would find
in business applications. To name a few of them: caching, security,
persistence, validation, observabiltity, exception handling, transaction
handling, and so on. We specify this using the custom attribute
ProvideAspectRole.
- Aspects can define relationships to other aspects.
Here, the custom attribute AspectRoleDependency specify a depedency
to other aspects that provide a given role. The custom attribute on
AuthorizationAspect just means: this aspect should be before any aspect
providing caching.
Now, if you apply two aspects on the same method, they will be ordered as you
expect:
[Authorization("SalesManager")]
[Cache]
public string GetQuoteDetailsHtml()
{
// Pretend we retrieve something large and confidential
// from database.
}
There are other ways than roles to match dependent aspects. For instance,
AspectTypeDependency allows you to specify an aspect type explicitely. All
dependency attributes are in namespace PostSharp.Aspects.Dependencies.
Expressing Aspect Conflicts
Let's take another scenario. I have a method, say ISecurable.IsUserInRole,
and I want to avoid at any price that this method gets cached.
Easy: we create an aspect NotCacheableAttribute with no logic at all
(the aspect does strictly nothing), and add a dependency:
[AspectRoleDependency(AspectDependencyAction.Conflict, StandardRoles.Caching)]
class NotCacheableAttribute : MethodLevelAspect
{
}
Now apply it to our interface method. We use aspect inheritance to ensure
than the aspect dependency applies to all methods implementing this interface
method:
public interface ISecurable
{
[NotCacheable(AttributeInheritance = MulticastInheritance.Strict)]
bool IsUserInRole( IPrincipal principal, string role );
}
Try to put a caching aspect on an implementation of this interface method:
class Organization : ISecurable
{
// Details omitted.
[Cache]
public bool IsUserInRole( IPrincipal principal, string role )
{
return accessList.IsUserInRole(principal, role);
}
}
Build the project; you'll get this error message:
Conflicting aspects on
"Dependencies.Entities.Organization/IsUserInRole([mscorlib]System.Security.Principal.IPrincipal
principal, string role) : bool": 'Dependencies.Aspects.NotCacheableAttribute'
cannot be used together with 'Before Dependencies.Aspects.CacheAttribute (3)'
because the second provides the role 'Caching'.
And this is exactly what we wanted!
Aspect Commutativity
You maybe remember commutativity from school. Multiplication is commutative
because 5*4 = 4*5; division is not. And you maybe even remember
function composition, which combines two functions in one: (f o g)(x) =
f(g(x)). Functions f and g are mutually commutative if
f o g = g o f. If you have done a little more maths, you know that
commutativity applies to differential operators, so we have (in good cases):
d/dx o d/dy = d/dy o d/dx. And if you have done much more maths, you are
even maybe an ace in Lie
algrebra, which I never understood.
Just as with functions and differential operators, if makes sense to talk of
commutativity with aspects.
Say we have two aspects on the same method: tracing and exception handling.
Does it matter if one aspect executes after the other? If you don't care, then
these aspects are said to commute (in other words, they are mutually
commutative). If not, you have to specify their ordering specifically.
If two aspects are not commutative but are not strongly ordered
(because you did not specify an ordering dependency), you will get a nice
warning at buid time:
Conflicting aspects on "Dependencies.Entities.Quote/GetQuoteDetailsHtml()
: string": transformations "Dependencies.Aspects.TraceAttribute" and
"Dependencies.Aspects.AuthorizationAttribute" are not commutative, but they are
not strongly ordered. Their order of execution is undeterministic.
You have two ways to get rid of this warning: either your order them strongly
by aspecifying an order dependency (see above), either you specify that both
aspects commute:
[AspectTypeDependency(AspectDependencyAction.Commute, typeof(CacheAttribute))]
class TraceAttribute : OnMethodBoundaryAspect
{
// Details omitted.
}
You may ask -- why to bother, anyway? Well, because every professional knows
how hard it is to troubleshoot non-deterministic issues, PostSharp 2.0 carefully
detects these situations and warns about them. You can specify that your aspect
has no effect at all (therefore you would lie, since it does not make sense to
write an aspect that has no effect) and, therefore, it's like the function
f(x)=x, it
commutes with any other aspect. But then you are supposed to have made an
educated choice (use the custom attribute WaiveAspectEffectAttribute
for this purpose).
Some Theory Behind
The theory behind shows that, while PostSharp Laos became popular quite
accidentally (PostSharp Core was well designed, but PostSharp Laos started as an
experiment), PostSharp 2.0 is really engineered to scale well in complexity and
to assume the burden of this popularity (be certain it is something we carry
both with pleasure and honor).
Before starting to execute transformations on a code element (say a method),
PostSharp gathers all transformations upfront. At this point, they form an
unstructured set of nodes. When this is done, PostSharp evaluates dependencies
between all transformations in the set. Ordering dependencies become edges in a
directed graph/a>
and form a partially
ordered set. Then, it performs a
topological sorting
on the graph to find out in which order transformations should be executed. A
part of this algorithm is to detect cycles, so you will get a build-time error
if two aspects have contradictory ordering requirements. Then, PostSharp checks
if it finds clusters of elements that are not strongly ordered. If it finds
some, it checks that all nodes in the cluster commute with all other nodes.
Finally, it checks if other dependency constraints (conflicts and requirements)
are fulfilled.
This complex algorithm is executed for every method, type, field, property,
or event that is the target of more than one aspect. Fortunately, the number of
aspects on a code element is typically small, so the performance cost is not
daunting in practice. Good news is that it scales in complexity.
Old Good Aspect Priority
"Where is my old AspectPriority property?" It's still there. You can
still specify a priority manually. The priority will be translated into a
dependency and will be merged with dependencies.
While using an aspect, you can always refind the dependencies provided by the
aspect developer. But you cannot break them. If you intend to do so, you will
introduce a circular reference in dependencies:
[Authorization("SalesManager", AspectPriority = 2)]
[Cache(AspectPriority = 1)]
Conficting aspects on "Dependencies.Entities.Quote/GetQuoteDetailsHtml()
: string": according to aspect dependencies, transformation "Before
Dependencies.Aspects.AuthorizationAttribute (4)" should be located both before
and after transformation "Before Dependencies.Aspects.CacheAttribute (3)".
Thanks to the topological sort algorithm, PostSharp is able to detect longer
dependency cycles and to enforce ordering even in complex situations.
Summary
I have shown how PostSharp 2.0 copes with mutliple aspects when they are
applied to the same code element. One word qualifies the approach: robustness.
Now you can safely use aspects in larger teams and projects. PostSharp is
engineered for.
Happy PostSharping!
-gael