Experiments with Roslyn

AOP and code generation is an aspect that is still not clear with Roslyn.
Currently I have experimented mostly with Fody do perform this tasks, and it works fine but I would prefer to do things the Roslyn way.

Recently I found this repository https://github.com/AArnott/CodeGeneration.Roslyn. AArnott works on the Visual Studio Team and has created great tools for Roslyn.

This nuget allows you to create your own code generators, but what I loved about it is that it provides "design time" experience, in the sense that you get intellisense for the code you generate, and is really simple to use.

I play around with it. This is my story...

In WebMap we have to insert some interfaces on the model of the generated code. But we would like it to be cleaner.

So the idea was. What about having just an attribute that generates those interfaces. Something like the

    [Model]
    partial class Goo
    {

    }
    class Program
    {
        static void Main(string[] args)
        {
            var g = new Goo();
            g.UniqueID = "a"; //<-- This uniqueID property was generated
        }
    }

Implementing this is fairly simple.

  1. First create a library for your attribute. The attribute project must use nuget CodeGeneration.Roslyn.Attributes.

And the code for the attribute is pretty simple:

using CodeGeneration.Roslyn;  
using System;  
using System.Diagnostics;  
using Validation;



[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
[CodeGenerationAttribute("WebMapModelHelperGenerator, CodeGeneratorLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")]
[Conditional("CodeGeneration")]
public class ModelAttribute : Attribute  
{
    private bool _viewmodel;
    private bool _container;
    private bool _viewmanager;
    private bool _top;

    public ModelAttribute(bool viewmodel=false,bool container=false,bool viewmanager=false,bool top=false)
    {
        _viewmodel = viewmodel;
        _container = container;
        _viewmanager = viewmanager;
        _top = top;
    }

    public bool IsViewModel { get { return _viewmodel; } }
    public bool AddsContainer { get { return _container; } }
    public bool AddsViewManager { get { return _viewmanager; } }
    public bool IsTop { get { return _top;  } }
}

NOTE: notice the [CodeGenerationAttribute("WebMapModelHelperGenerator, CodeGeneratorLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] attribute. The string value of the attribute corresponds to the Assembly Qualified name of the Generator class. To get the Assembly Qualified name you can use something like this and prepend it with the class name.

  1. The generator code is also pretty simple. Create another library project. You do not need to reference the attributes library.
    And add a reference to Nuget CodeGeneration.Roslyn
    I omitted the body because it is too long. But this is the general structure of the generator.
using CodeGeneration.Roslyn;  
using Microsoft.CodeAnalysis;  
using Microsoft.CodeAnalysis.CSharp;  
using Microsoft.CodeAnalysis.CSharp.Syntax;  
using System;  
using System.Collections.Generic;  
using System.Threading;  
using System.Threading.Tasks;  
using Validation;

public class WebMapModelHelperGenerator : ICodeGenerator  
{

    private bool viewmodel;
    private bool container;
    private bool viewmanager;
    private readonly bool isTop;

    public WebMapModelHelperGenerator(AttributeData attributeData)
    {
        Requires.NotNull(attributeData, nameof(attributeData));
        var constructorArgumentsByPosition = attributeData.ConstructorArguments;

        for (var i = 0; i < constructorArgumentsByPosition.Length; i++)
        {
            switch (i)
            {
                case 0:
                    this.viewmodel = (bool)constructorArgumentsByPosition[i].Value;
                    break;
                case 1:
                    this.container = (bool)constructorArgumentsByPosition[i].Value;
                    break;
                case 2:
                    this.viewmanager = (bool)constructorArgumentsByPosition[i].Value;
                    break;
                case 3:
                    this.isTop = (bool)constructorArgumentsByPosition[i].Value;

                    break;
            }
        }

    }

    public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(MemberDeclarationSyntax applyTo, CSharpCompilation compilation, IProgress<Diagnostic> progress, CancellationToken cancellationToken)
    {
        var results = SyntaxFactory.List<MemberDeclarationSyntax>();

        // Our generator is applied to any class that our attribute is applied to.
        var applyToClass = (ClassDeclarationSyntax)applyTo;


        // Apply a suffix to the name of a copy of the class.
        var copy = applyToClass;
        //Use the WithXXX methods to created a mofied copy
        results = results.Add(copy);
        // Return our modified copy. It will be added to the user's project for compilation.
        return Task.FromResult<SyntaxList<MemberDeclarationSyntax>>(results);
    }
}

To modify the class use any of the WithXXX methods to return a modified copy.

Generating the API calls to build an element with Roslyn can be cumbersome but using Roslyn Quouter makes the work pretty easy

  1. The easiest way to try this generator is on the same solution. For example add a console project. Install nuget CodeGeneration.Roslyn.BuiltTime. Then add a reference to the Attribute library and to the Generator library. And build.
    That is all you need to do.

So for the previous example, classes with the [Model] attribute will automatically have the UniqueID property. If you press F12 to navigate to the property definition you will see something like this:

// ------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
// ------------------------------------------------------------------------------

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;

namespace ConsoleApp1  
{
    partial class Program
    {
        [Model()]
        public partial class Goo : WebMap.Interfaces.IDependentModel
        {
            public string UniqueID
            {
                get;
                set;
            }

        }
    }

}

NOTE: One issue that I found is that in this particular case I need the class to be partial. I could write a Roslyn analyzer to avoid this problem.

  1. How to package it. Just create a nuget from your code generator library. Make sure you create this nuget as a tools nuget (a tools nuget packages assemblies on the tools dir instead of the lib dir), also you must add a build folder to your nuget with a .targets file called packageid.targets with contents like:
<?xml version="1.0" encoding="utf-8" ?>  
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">  
  <ItemGroup>
    <GeneratorAssemblySearchPaths Include="$(MSBuildThisFileDirectory)..\tools" />
  </ItemGroup>
</Project>  

SUMMARY

I think this code generators are great. Fully integrated with VS, they are easy to implement and also pretty transparent. It is a lot harder to detect issues when you are doing IL generation. So post say that is also works on the MAC version of Visual Studio but I have not yet tried that. And it works transparently on command line builds