Search code examples
c#aoppostsharp

Have PostSharp help to log IF statements


The behavior I'm looking for would be something like the following:

[IfElseLogging]
public void Foo(string bar)
{
    if (bar == "somedumbstringbecausethisishorriblewayofdoingthings")
    {
        // here the aspect would log that the if statement above was true,
        // and thus took this path
    }
    else
    {
        // here the aspect would log that the if statement above was false,
        // and thus took this path
    }
}

If there was a way to hijack the if functionality, I would be able to adorn it with the logging that Postsharp uses and dust off my hands. The other reason I'm adding to the question is that I'm not sure I'm being clear as to what I'm asking.

This is for a project that was done by a very Jr developer. My task is not only to refactor but to document what exactly the code does to the level of flow charts. Given the nature of the code, lack of descriptions, poor techniques and practices, nature of the project, etc... Here are the things I'm struggling with:

  • I can't simply debug and step through the code locally
  • I can't create unit tests and refactor safely
  • I can't understand what this code does!
  • I have no original requirement documents to understand what the goal of the functionality is

So I'm looking to use Postsharp to basically give me my code paths and unit test criteria as I don't know how else to get it.

On another thought, is it possible to have an aspect that triggers an event when an if statement is found?


Solution

  • Aspect oriented programming with PostSharp will allow you to creator interceptors that hook into your code at well-defined points like method calls and property access. This is done be rewriting the IL generated from your source code.

    In your case you are looking for specific if statements but they are not easily recognized in the generated IL. Here is what is generated for your sample method:

              nop
              ldarg.1     
              ldstr       "somedumbstringbecausethisishorriblewayofdoingthings"
              call        System.String.op_Equality
              stloc.0
              ldloc.0
              brfalse.s   NotEqual
              nop
    
    **True branch**
    
              br.s        Return
    NotEqual: nop
    
    **False branch**
    
    Return:   ret
    

    However, this is the non-optimized version. The optimizing compiler generates this code:

              ldarg.1
              ldstr       "somedumbstringbecausethisishorriblewayofdoingthings"
              call        System.String.op_Equality
              brfalse.s   NotEqual
    
    **True branch**
    
              ret
    
    NotEqual:
    
    **False branch**
    
               ret
    

    So if you want to do IL rewriting you could conceivably intercept method calls, look for an IL signature that looks something like ldstr, "...", call System.String.op_Equality, brfalse.s and then rewrite the IL to modify the code. However, this approach is extremely fragile. Small differences in the source code may generate IL with a different signature and only the compiler writers will really know the entire range of IL signatures you have too look for. If the conditional statement involves several terms combined with logical operators then it can become very complicated to determine where to insert your instrumentation code. And then there are false positives. Personally, I do not believe this is a viable strategy for instrumenting your code.

    However, you have another option. You can compile the source code using Roslyn and use Roslyn rewriters to modify the original source tree. This is AOP at the source level as opposed to PostSharp that offers AOP at the IL level.

    To rewrite a syntax tree you need to derive a class from CSharpSyntaxRewriter. This rewriter will modify if statements so the VisitIfStatement method is overridden:

    class IfStatementRewriter : CSharpSyntaxRewriter {
    
      public override SyntaxNode VisitIfStatement(IfStatementSyntax ifStatement) {
        var containsMagicString = ifStatement
          .Condition
          .DescendantNodes()
          .Any(
            syntaxNode => syntaxNode.ToString() == @"""somedumbstringbecausethisishorriblewayofdoingthings"""
          );
        if (!containsMagicString)
          // Do not modify if statement.
          return ifStatement;
        // Only look at the "true" part and assume it is a block (has curly braces).
        var block = ifStatement.Statement as BlockSyntax;
        if (block == null)
          // Do not modify if statement.
          return ifStatement;
        // Insert instrumentation code at start of block.
        var instrumentationStatements = CreateInstrumentationStatements("True branch");
        var modifiedStatements = block.Statements.InsertRange(0, instrumentationStatements);
        var modifiedBlock = block.WithStatements(modifiedStatements);
        return ifStatement.WithStatement(modifiedBlock);
      }
    
    }
    

    To insert instrumentation code System.Console.WriteLine("True branch"); the following rather hairy method is used:

    IEnumerable<StatementSyntax> CreateInstrumentationStatements(String text) {
      return SyntaxFactory.SingletonList<StatementSyntax>(
        SyntaxFactory.ExpressionStatement(
          SyntaxFactory.InvocationExpression(
            SyntaxFactory.MemberAccessExpression(
              SyntaxKind.SimpleMemberAccessExpression,
              SyntaxFactory.MemberAccessExpression(
                SyntaxKind.SimpleMemberAccessExpression,
                SyntaxFactory.IdentifierName("System"),
                SyntaxFactory.IdentifierName("Console")
              )
              .WithOperatorToken(SyntaxFactory.Token(SyntaxKind.DotToken)),
              SyntaxFactory.IdentifierName("WriteLine")
            )
            .WithOperatorToken(SyntaxFactory.Token(SyntaxKind.DotToken))
          )
          .WithArgumentList(
            SyntaxFactory.ArgumentList(
              SyntaxFactory.SeparatedList(
                new[] {
                  SyntaxFactory.Argument(
                    SyntaxFactory.LiteralExpression(
                      SyntaxKind.StringLiteralExpression,
                      SyntaxFactory.Literal(text)
                    )
                  )
                }
              )
            )
          )
        )
        .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
      );
    }
    

    You can now modify a syntax tree:

    var root = (CompilationUnitSyntax) syntaxTree.GetRoot();
    var rewriter = new IfStatementRewriter();
    var rewrittenRoot = rewriter.Visit(root);
    

    You will need to expand the code considerably to better filter for if statements and to handle all the different ways if statements can be written but compared to doing IL rewriting this should give you a much higher chance of success.