Category: roslyn

AOP with Roslyn–part 5 – getting line number right

There is a problem with inserting lines  – the number of lines is not the same. What I mean by this?

Look at this example

 

 

using System;
namespace Test1
{
    class Program
    {
        static void Main(string[] args)
        {
              var dt=DateTime.Now;
        }
     }
}

 

The DateTime.Now is on line 8

When we insert lines before and after each method, the DateTime.Now is on line 9

using System;
namespace Test1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(" + "\"start Program_Main_6\"" + @");
            var dt = DateTime.Now;
            Console.WriteLine(" + "\"end Program_Main_6\"" + @");
        }
    }
}

This is usually not so important  – until you think about exception and stack trace. If we are moving the number of lines like so, the stack trace that will be raised by the program will make no sense to the programmer that grabs the code from the source control. More, the problem is aggravating with each method – so, if you have 30 methods in a file(I exaggerate a bit ) the number of lines will be increased with 30 for the last method. For others , will be with 29, 28 and so on, until the first method.

Fortunately, there is a solution – the #line directive.

See https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-line and  https://blogs.msdn.microsoft.com/abhinaba/2005/10/10/c-fun-with-line-directive/

So, what we need is to generate the #line <number> directive.

 

 

AOP with Roslyn–part 4–custom code to the end of the method

 

A good AOP needs also custom code to the beginning and to the end of the method. So we need to add formatter for the last line.

That means that the MetodRewriter constructor will look like this:

 

public MethodRewriter(string formatterFirstLine,string formatterLastLine=null)
{
   FormatterFirstLine = formatterFirstLine;
   FormatterLastLine = formatterLastLine;
}

 

More, we should put code at the beginning and at the end of the method

if (cmdLastLine!= null)
 blockWithNewStatements = blockWithNewStatements.Insert(0, cmdLastLine);
//code
if(cmdFirstLine != null)
 blockWithNewStatements = blockWithNewStatements.Insert(0, cmdFirstLine);

And the test will make the sentence

var dt=DateTime.Now;

between start and end lines:

[TestMethod]
 public void TestMethodRewriterLastLine()
 {

 var rc = new RewriteCode(
 formatterFirstLine: "Console.WriteLine(\"start {nameClass}_{nameMethod}_{lineStartNumber}\");",
 formatterLastLine: "Console.WriteLine(\"end {nameClass}_{nameMethod}_{lineStartNumber}\");"
 );
 rc.Code = @"
using System;
namespace Test1
{
 class Program
 {
 static void Main(string[] args)
 {
 var dt=DateTime.Now;
 }
 }
}";
 var result = rc.RewriteCodeMethod();
 var newCode = @"
using System;

namespace Test1
{
 class Program
 {
 static void Main(string[] args)
 {
 Console.WriteLine(" + "\"start Program_Main_6\"" + @");
 var dt = DateTime.Now;
 Console.WriteLine(" + "\"end Program_Main_6\"" + @");
 }
 }
}";
 Assert.AreEqual(result.Replace(Environment.NewLine, ""), newCode.Replace(Environment.NewLine, ""));
 }


 
 }

 

AOP with Roslyn–part 3–custom code at beginning of each method

Last time(http://msprogrammer.serviciipeweb.ro/2017/11/27/aop-with-roslynpart-2/)  we have injected a new code into each method . However, this code was pretty much hardcoded into the MethodRewriter class – it was a simple

Console.WriteLine(\”{nameClass}_{nameMethod}_{lineStartNumber}\”);

 

Now we want to can customize this code at the will of the programmer. For this, I have modified classes RewriteCode and MethodRewriter to accept a parameter named Formatter . To run the test that worked previously, I made a parameterless  constructor for RewriteCode in order to preserve compatiblity.

 

 

public RewriteCode(): this("Console.WriteLine(\"{nameClass}_{nameMethod}_{lineStartNumber}\");")
{

}
public RewriteCode(string formatter)
{
    Formatter = formatter;
}

 
The VisitMethod is now much simpler:

string nameVariable =Formatter.FormatWith(new { nameClass,nameMethod,lineStartNumber=lineStart.Line});
var cmd = SyntaxFactory.ParseStatement(nameVariable);

 
( The .FormatWith is an extension from https://github.com/crozone/FormatWith )
 
Also, I have made a new test to test this one – and I have inserted into code an variable named s:
 
string s=\”this is method {nameMethod} from class {nameClass} at line {lineStartNumber}\”;”
 
 
The test is

[TestMethod]
public void TestMethodRewriterAddVariable()
{

var rc = new RewriteCode(
"string s=\"this is method {nameMethod} from class {nameClass} at line {lineStartNumber}\";"
);
rc.Code = @"
using System;
namespace Test1
{
    class Program
    {
        static void Main(string[] args)
        {
              var dt=DateTime.Now;
        }
     }
}";
            var result = rc.RewriteCodeMethod();
            var newCode = @"
using System;

namespace Test1
{
    class Program
    {
        static void Main(string[] args)
        {
            string s = ""this is method Main from class Program at line 6"";
            var dt = DateTime.Now;
        }
    }
}";
            Assert.AreEqual(result.Replace(Environment.NewLine, ""), newCode.Replace(Environment.NewLine, ""));
        }
    }

AOP with Roslyn–part 2

I want to transform code by injecting some simple code, like “Console.WriteLine(“method”)

So this code:

 

using System;
namespace Test1
{
    class Program
    {
        static void Main(string[] args)
        {
              var dt=DateTime.Now;
        }
     }
}

 

should be modified to this code:

 

using System;

namespace Test1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(" + "\"Program_Main_6\"" + @");//this is automatically added
            var dt = DateTime.Now;
        }
    }
}

 
How I do the code: I derive from CSharpSyntaxRewriter and override the VisitMethodDeclaration  . I will construct a new node with the Console.WriteLine statement inserted

 

public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node)
        {


            if (node.Body == null || node.Body.Statements.Count == 0)
                return base.VisitMethodDeclaration(node);
            var parent = node.Parent as ClassDeclarationSyntax;
            if (parent == null)
                return base.VisitMethodDeclaration(node);

            var nameMethod = node.Identifier.Text;
            var nameClass = parent.Identifier.Text;
            Console.WriteLine(nameMethod);
            node = (MethodDeclarationSyntax)base.VisitMethodDeclaration(node);
            var lineStart = node.GetLocation().GetLineSpan().StartLinePosition;
            string nameVariable = $"{nameClass}_{nameMethod}_{lineStart.Line}";
            var cmd = SyntaxFactory.ParseStatement($"Console.WriteLine(\"{nameVariable}\");//this is automatically added");

            var blockWithNewStatements = new SyntaxList<StatementSyntax>();
            
            for (int i = node.Body.Statements.Count - 1; i >= 0; i--)
            {
                var st = node.Body.Statements[i];
                blockWithNewStatements = blockWithNewStatements.Insert(0, st);
            }

            blockWithNewStatements = blockWithNewStatements.Insert(0, cmd);

            var newBlock = SyntaxFactory.Block(blockWithNewStatements);

            var newMethod = SyntaxFactory.MethodDeclaration
                (node.AttributeLists, node.Modifiers, node.ReturnType,
                node.ExplicitInterfaceSpecifier, node.Identifier, node.TypeParameterList,
                node.ParameterList, node.ConstraintClauses,
                newBlock,
                node.ExpressionBody, node.SemicolonToken);


            var newNode = node.ReplaceNode(node, newMethod);

            return base.VisitMethodDeclaration(newNode);
        }

 

As test, I have created a TestMethodRewriterSimple that verifies that.

        [TestMethod]
        public void TestMethodRewriterSimple()
        {

            var rc = new RewriteCode();
            rc.Code = @"
using System;
namespace Test1
{
    class Program
    {
        static void Main(string[] args)
        {
              var dt=DateTime.Now;
        }
     }
}";
            var result= rc.RewriteCodeMethod();
            var newCode = @"
using System;

namespace Test1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(" + "\"Program_Main_6\"" + @");//this is automatically added
            var dt = DateTime.Now;
        }
    }
}";
            Assert.AreEqual(newCode.Replace(Environment.NewLine,""), newCode.Replace(Environment.NewLine, ""));
        }
    }

You can find the code on https://github.com/ignatandrei/AOP_With_Roslyn

AOP with Roslyn

What I am interested in is to make a tool that logs in when the program comes in and out of a method.

It’s good for identifying problems in code and for logging.

What already exists on the market:

PostSharp – one of the best – see https://www.postsharp.net/alternatives
Cecil http://www.mono-project.com/docs/tools+libraries/libraries/Mono.Cecil/
Fody https://github.com/Fody
NConcern: https://github.com/Virtuoze/NConcern
What I do not like is that they are hard to configure. So, instead of writing a configurator, I’m going to write a Roslyn AOP so it’s easy to use POST build event in a CI scenario

I’ve inspired at https://github.com/KeenSoftwareHouse/SpaceEngineers – See https://github.com/KeenSoftwareHouse/SpaceEngineers/tree/master/Sources/VRage.Scripting. Do not compile in VS2017, but you can analyze the code …

The second source of inspiration was http://cezarywalenciuk.pl/blog/programing/post/roslyn-kompilator-net-rewrite-z-csharpsyntaxrewriter – in Polish, but you can see the code … (it gets complicated at the end)

My code will be on https://github.com/ignatandrei/AOP_With_Roslyn

Andrei Ignat weekly software news(mostly .NET)

* indicates required

Please select all the ways you would like to hear from me:

You can unsubscribe at any time by clicking the link in the footer of our emails. For information about our privacy practices, please visit our website.

We use Mailchimp as our marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.