Archive

One of the use cases we wanted to optimize is to add logging to a whole solution as easily as possible. There are a few policies that you may want to add at solution level instead of at project level. Deadlock detection, which needs to be applied to all projects to be efficient, is another one.

In previous versions of PostSharp, you had to add these policies to each project individually. PostSharp 3.1 now makes it easier just to add them to the solution. However, this presented many challenges.

Improvements in PostSharp Tools

The first challenge was that PostSharp essentially needs to be installed to each project separately, because its file PostSharp.targets has to be included in every csproj or vbproj file. So, we still need to install the PostSharp NuGet package to all affected projects. This could already be done easily using the solution-level package management window of NuGet, but now the solution-level PostSharp wizard makes it even easier.

Remember however that you still need to install PostSharp to projects that you will later add to your solution.

Solution-Level Configuration File

If we want to add aspects to several projects from a single line, we also needed some solution-level configuration file for PostSharp. We already had a project-level file with extension psproj. We now have a solution-level file with extension pssln. The name of this file must be the same as the solution name, so if your solution is MySolution.sln, PostSharp is going to store its solution-level configuration into MySolution.pssln. Both psproj and pssln files share exactly the same syntax. Thanks to this new file, we now have somewhere to share our solution-level aspects and other shared information such as logging profiles.

For instance, adding logging to the solution would create the following project-level file:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.postsharp.org/1.0/configuration" 
         xmlns:d="clr-namespace:PostSharp.Patterns.Diagnostics;assembly:PostSharp.Patterns.Diagnostics" 
         xmlns:p="http://schemas.postsharp.org/1.0/configuration">
  <Property Name="LoggingBackend" Value="Console" />
  <Property Name="LoggingEnabled" Value="{has-plugin('PostSharp.Patterns.Diagnostics')}" Deferred="true" />
  <Multicast>
    <When Condition="{$LoggingEnabled}">
      <d:Log AttributeTargetTypeAttributes="Public" AttributeTargetMemberAttributes="Public" />
    </When>
  </Multicast>
  <d:LoggingProfiles p:Condition="{$LoggingEnabled}">
    <d:LoggingProfile Name="Default" 
        OnEntryOptions="IncludeParameterName | IncludeParameterValue | IncludeThisArgument" 
        OnSuccessOptions="IncludeParameterName | IncludeParameterValue | IncludeReturnValue | IncludeThisArgument" 
        OnExceptionOptions="IncludeParameterName | IncludeParameterValue | IncludeThisArgument" />
  </d:LoggingProfiles>
</Project>

Here is the project-level file you will get by default:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.postsharp.org/1.0/configuration">
  <Using 
File="..\packages\PostSharp.Patterns.Diagnostics.3.1.18-beta\tools\PostSharp.Patterns.Diagnostics.Weaver.dll" /> </Project>

I’ll give more details below about expressions and conditional constructs.

It’s worth noting that PostSharp has had a configuration file since version 1.0. Very soon, I understood it was better to design a system that did not require configuration, so I implemented mechanisms that rendered configuration files unnecessary for most users. We re-introduced the configuration files when we implemented NuGet packaging, because we needed a way to tell PostSharp where the plug-ins (packaged as different NuGet packages) were installed.

It’s also important to understand that this features introduces some kind of “oddity” in the build process, because it usually does not matter if you build a project A from solution B or C, or from no solution at all. Usually, the build process of a project does not depend on the solution from which the build process was started. With this feature however, it does. You should be aware of this issue, and not use solution-level configuration if you plan to build from several solutions.

UPDATE: the next paragraphs of this section have been updated to reflect improvements in build 3.1.21:

The reason why we chose the solution-level configuration file is that it is a simpler model and that it is significantly simpler to build a UI for a simpler model. But once you’re past the Hello World stage, you’ll find more convenient to interact with PostSharp as it was a compiler. Because it, really, is.

There are two alternative ways to share configuration files among several projects. The first way is to add a file named postsharp.config in the directory containing the C#/VB project file or in any parent directory, up to the root. These files will be loaded in order of increasing directory depth, before the psssln file and before the psproj file, so in the following order for instance:

  1. c:\src\MySolution\postsharp.config
  2. c:\src\MySolution\BackEnd\postsharp.config
  3. c:\src\MySolution\BackEnd\MyProject\postsharp.config
  4. c:\src\MySolution\MySolution.pssln
  5. c:\src\MySolution\BackEnd\MyProject\MyProject.psproj

The second way is to add a Using element to your project file, just as in the preceding XML snippet, but pointing to your configuration file instead of to a dll.

Note that these two alternative ways are not supported by the UI, just by the compiler.

XPath expressions

Another challenge we had to solve is that a solution may contain projects that target different platforms, and that not all aspects are available for all platforms. Specifically, our logging aspect is currently only available for .NET 4.0, so for instance you can’t add it to a Windows Phone project. Thus, we needed a way to make solution-level aspects conditional. This is why we added two features to the configuration file: XPath, and conditional constructs.

For the anecdote, I’ve always planned to add XPath support to the configuration file format. However, I only implemented expansion of properties, pretty much like MSBuild 2.0 did. But at the difference of MSBuild, property references were forward compatible with the XPath syntax. For instance {$MyProperty} results in the evaluation of the MyProperty – it previously just was done by property expansion, but now it can be considered as an XPath expression.

The following features of XPath 1.0 are available:

  • Any XPath 1.0 function of the Microsoft’s implementation.
  • XPath variables, for instance $MyProperty, evaluate to PostSharp properties (not to MSBuild properties).
  • Custom function has-plugin(name) evaluates to true if a plug-in is loaded, false otherwise.
  • Custom function environment(variable) returns the value of an environment variable.

In theory, the design would allow for plug-ins to define custom functions, but we did not implement this feature since we did not have any use case.

Conditional constructs

    Some constructs now have a Condition attribute, which is typically set to a boolean XPath expression:
  • The following first-level elements: Property, Using, Multicast, LoggingProfiles
  • Any child element of Multicast or LoggingProfiles

Additionally, elements Multicast and LoggingProfiles accept a child element When, as illustrated in the XML snippet above. All children of the When element are made conditional.

Properties with deferred evaluation

You may have noted the Deferred attribute on the definition of the LoggingEnabled property in the first code snippet. This means that the value of the expression will be evaluated dynamically whenever the property value is requested. By default, a property value is evaluated immediately at the time the property is defined.

In this case, it is useful to have deferred evaluation of the LoggingEnabled property because its value depends on the has-plugin method, and that plug-ins are typically loaded after the property is evaluated: indeed, the property is defined at solution level, and plug-ins are loaded at project level.

Summary

The PostSharp configuration system got a major upgrade in PostSharp 3.1 with XPath expressions and conditional constructs – two necessary features if we wanted to enable for solution-level aspects and solution-level shared configuration.

We understand the documentation of the configuration system is currently weak and we’ll be working on that before the RTM. However, it’s already become much easier to add aspects that really cross-cut all projects.

Happy PostSharping!

-gael

PostSharp 3.1 catches up with two advanced features of the C# compiler: automatic iterators and async methods.  Iterators have been around since NET 3.5. This was the first compiler feature that did not trivially map to MSIL, the level of abstraction at which PostSharp operates. Under the hood, when it meets a iterator, the compiler would generate a class. The parameters and local variables of the method would be transformed into fields, and the body of the method would be generated into the MoveNext method, with multiple exit and entry points.

Before version 3.1, PostSharp did not understand the state machine transformation operated by the compiler. Adding an aspect to an iterator would produce surprising results, because the aspect would actually be applied only to the code that instantiates the state machine class. For instance, an OnException advise would never get invoked because instantiating the state machine is unlikely to throw any exception. Conversely, any exception thrown by the iterator would never be captured because the exception was technically (at MSIL level) thrown by the MoveNext method of the state machine class. The workaround was easy: add an aspect to this MoveNext method. The availability of this workaround was perhaps the reason why users did not complain that PostSharp lacks this ability.

Things became more problematic with async methods. The compiler does the same kind of magic as with iterators, but a more advanced one. With async methods, exceptions thrown by user code would get “transparently” intercepted from within the MoveNext method and assigned to the Task object – before they get any chance to get intercepted by our aspect. Thus, the need for proper support from PostSharp became more urgent. No wonder if this became our number-one feature request on User Voice. We were not able to ship the feature in PostSharp 3.0 for a lot of embarrassing reasons, but now it’s out!

The old, backward-compatible way

PostSharp has been around for 9 years. One of our major concerns has always been to never break backward compatibility. The problem is actually not to ensure that your old code still builds after you upgrade to PostSharp. The real challenge is to guarantee that your code will behave identically.

To ensure behavioral backward compatibility, we had to take this design decision: if you apply an OnMethodBoundaryAspect to an iterator or async method, by default, it won’t be applied to the state machine.

However, we still think that the backward-compatible behavior is odd and that new users would really expect the aspect to be applied to the state machine. Therefore, a warning will be emitted whenever an OnMethodBoundaryAspect is applied to an async or iterator method. To turn off the warning, you have to set the aspect property ApplyToStateMachine to false if you want to maintain the backward-compatible behavior. You can set the property in the constructor if you don’t want to set it explicitly every time the aspect is used.

The following aspect exhibits the backward-compatible behavior of the OnMethodBoundary aspect:

[PSerializable]
class MyAspect : OnMethodBoundaryAspect
{
    public override void OnEntry(MethodExecutionArgs args)
    {
        Console.WriteLine("OnEntry");
    }

    public override void OnSuccess(MethodExecutionArgs args)
    {
        Console.WriteLine("OnSuccess({0})", args.ReturnValue);
    }

    public override void OnExit(MethodExecutionArgs args)
    {
        Console.WriteLine("OnExit");
    }

    public override void OnException(MethodExecutionArgs args)
    {
        Console.WriteLine("OnException({0})", args.Exception.Message);
    }
}

Let’s apply this aspect to an iterator:

static void PrintFruits(bool throwException)
{
    try
    {
        foreach (string fruit in GetFruits(throwException))
        {
            Console.WriteLine("Received: " + fruit);
        }
    }
    catch (Exception e)
    {
        Console.WriteLine("Exception: " + e.Message);
    }
}

[MyAspect(ApplyToStateMachine = false)]
static IEnumerable GetFruits(bool throwException)
{
    yield return "blackcurrant";
    yield return "pomegranate";
    if ( throwException ) throw new Exception("Rotten fruit.");
    yield return "pineapple";
}

Here is the output of this code with the success flow:

OnEntry
OnSuccess(Program+<GetFruits>d__0)
OnExit
Received: blackcurrant
Received: pomegranate
Received: pineapple

You can see that the OnSuccess advice is called before the enumerator has produced the first result, and that the return value printed by the OnSuccess advice does not make sense.

And here is the output with the exception flow:

OnEntry
OnSuccess(Program+<GetFruits>d__0)
OnExit
Received: blackcurrant
Received: pomegranate
Exception: Rotten fruit.

As you can see, the OnException handler is never hit.

Applying the aspect to the state machine

Things get very different when you set the ApplyToStateMachine property to true. Just modify the property and the code above will produce the following output:

OnEntry
Received: blackcurrant
Received: pomegranate
Received: pineapple
OnSuccess()
OnExit

The exception flow is the following:

OnEntry
Received: blackcurrant
Received: pomegranate
OnException(Rotten fruit.)
Exception: Rotten fruit.

As you can see, the behavior is now “as expected”, i.e. it is consistent with the level of abstraction of the source code. The previous behavior was consistent with the level of abstraction of MSIL, and this is why it was less useful.

New advices: OnYield and OnResume

Additionally to applying the aspect to the state machine instead of the method that merely instantiates it, PostSharp 3.1 brings two new advices: OnYield and OnResume. These advices are defined on the new interface IOnStateMachineBoundaryAspect. If you want to use them, you need to have your aspect class implement this interface.

Note that implementing the IOnStateMachineBoundaryAspect has the side effect of settings the default value of the ApplyToStateMachine property to true and to quiet the warning that is otherwise displayed then this property is not set. This is because, if your code implements IOnStateMachineBoundaryAspect, we trust we can put usability prior to backward compatibility.

But let’s go back to the advices themselves. Interestingly, they apply identically – with exactly the same semantics – to both iterator and async methods. In short, OnYield is invoked when the state machine yields the control flow, i.e. when the control flow temporary leaves the state machine to the caller. OnResume is invoked when the state machine gains back the control flow.

With iterators

Let’s see this more concretely, first on iterators. OnYield is invoked after the yield return statement, before the control flow gets back to the consumer of the iterator. OnResume is then invoked when the consumer calls MoveNext. Note that the first time the consumer calls MoveNext, the OnEntry advice is invoked. Also, when the iterators terminates using yield break or simply by letting the control flow fall back, the OnSuccess advice is invoked after OnYield.

What if you want to know which value has just been yielded? Simply read the MethodExecutionArgs.YieldValue property. You can also write this property if you want to change the returned value.

Let’s update our aspect to add tracing of OnYield and OnResume events:

[PSerializable]
class MyAspect : OnMethodBoundaryAspect, IOnStateMachineBoundaryAspect
{
    public override void OnEntry(MethodExecutionArgs args)
    {
        Console.WriteLine("OnEntry");
    }

    public override void OnSuccess(MethodExecutionArgs args)
    {
        Console.WriteLine("OnSuccess({0})", args.ReturnValue);
    }

    public override void OnExit(MethodExecutionArgs args)
    {
        Console.WriteLine("OnExit");
    }

    public override void OnException(MethodExecutionArgs args)
    {
        Console.WriteLine("OnException({0})", args.Exception.Message);
    }

    public void OnResume(MethodExecutionArgs args)
    {
        Console.WriteLine("OnResume");
    }

    public void OnYield(MethodExecutionArgs args)
    {
        Console.WriteLine("OnYield({0})", args.YieldValue);
    }
}

The program output is now the following:

OnEntry
OnYield(blackcurrant)
Received: blackcurrant
OnResume
OnYield(pomegranate)
Received: pomegranate
OnResume
OnYield(pineapple)
Received: pineapple
OnResume
OnSuccess()
OnExit
-------------
OnEntry
OnYield(blackcurrant)
Received: blackcurrant
OnResume
OnYield(pomegranate)
Received: pomegranate
OnResume
OnException(Rotten fruit.)
Exception: Rotten fruit.

With async methods

The behavior of OnYield and OnResume is similar for async methods than for iterators. OnYield and OnResume are invoked upon execution of the await keyword: OnYield gets called when a wait begins, OnResume when a wait ends.

Note there are cases where the await keyword does not result into an execution of the OnYield/OnResume sequence: when the awaited-for task completes synchronously, the execution of the method continues without going through OnYield and OnResume. This is logical if you count that in this case the state machine really does not yield the control flow.

The YieldValue property is not available for async methods.

Let’s test our aspect on the following code:

[MyAspect]
static async Task<string> TimerMethod()
{
    for (int i = 3; i >= 0; i--)
    {
        Console.WriteLine(i + " green bottles");
        await Task.Delay(100);
    }

    return "Done";
}

The output of this program is the following:

OnEntry
3 green bottles
OnYield()
OnResume
2 green bottles
OnYield()
OnResume
1 green bottles
OnYield()
OnResume
0 green bottles
OnYield()
OnResume
OnSuccess()
OnExit

Limitations

Note that the following limitations apply when an aspect is applied to a state machine (whether the additional advices OnYield and OnResume are applied or not):

  • The control flow cannot be changed (the MethodExecutionArgs.FlowBehavior property is ignored).
  • The return value cannot be read or changed (the MethodExecutionArgs.ReturnValue is ignored).

Aspect Composition

Perhaps it goes without saying, but this is never trivial to implement: you can apply many aspects to state machines, apply state-machine-aware and -unaware aspects to a method, and do strange combinations of aspects. This is all supposed to work – and tested.

Use Cases

I believe the new features are very useful in the following use cases:

  • Call stack reconstruction: remember the stack call on entry so that it can be meaningfully displayed if an exception occurs after resume of the async method. Otherwise, you would just see that the call stack of the exception comes from the thread pool.
  • Iterator logging: you can now log the values returned by the iterator.
  • Profiling: you can now accurately compute the time taken by an iterator or async method to complete.
  • Context switching: ensure that the value of some thread-static fields are preserved and meaningful in all parts of a state machine.

Summary

The OnMethodBoundaryAspect can now be applied to state machines like async and iterator methods, and your aspect will not be applied to the state machine itself instead of just to instructions that instantiate the state machine. There is a new interface IOnStateMachineBoundaryAspect with two new advices: OnYield and OnResume. The new feature is designed to be backward compatible with the old (and odd) behavior. You will need to manually set the ApplyToStateMachine property if you want to get rid of the attention-to-odd-but-backward-compatible-behavior warning.

I think this is an exciting feature, and it required a lot of engineering work just to make it work and integrate well with other features of PostSharp.

A last word: PostSharp 3.1 is still beta and there’s still room to improve the API. I’m anxious to hear your feedback so we can take into account before sealing the new feature.

Happy PostSharping!

-gael