Category: roslyn

RSCG–Advice- part 8

If you create a Roslyn Source Code Generator that uses inside a base class or a static class, the best way, in my opinion , is to create 2 separate nuget packages : one for the code generated and one for the base class. This way the project that will use your RSCG will use just the nuget package that have the base class and get rid of the generator . More, you can add a third project to register some extensions to Web project .

Let’s see in practice with RSCG_AMS :

I have a NuGet package with the base class AboutMySoftware –  https://www.nuget.org/packages/AMS_Base/

The RSCG generates derived classes of AboutMySoftware  – has a dependency in the AMS_Base   –  the name is RSCG_AMS : https://www.nuget.org/packages/RSCG_AMS/ . This will generate code during the build and will not be included in the output.

Finally, I have an extension for WebAPI – AMSWebAPI – that registers the  endpoint in the Web Project to display /ams url .

I think that this is a normal organization of RSCG projects.

 

Also, add documentation  for each class / public item generated. There can be projects that will require this ( as a compiler/ property flag )

RSCG–AMS – About My software –Documentation– part 7

Now it is time to let others know about the project. And the first step is to make documentation. And , because a picture is worth many words, here is the picture:

Also, instructions about how to use will help the programmers:

For a DLL it is simple :

<ItemGroup>
    <PackageReference Include="AMS_Base" Version="2021.6.29.1820" />
    <PackageReference Include="RSCG_AMS" Version="2021.6.29.1820" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
  </ItemGroup>

For an ASP.NET Core application:

  <PackageReference Include="AMSWebAPI" Version="2021.6.29.1820" />
    <PackageReference Include="AMS_Base" Version="2021.6.29.1820" />
    <PackageReference Include="RSCG_AMS" Version="2021.6.29.1820" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />

and the code will be

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
    endpoints.UseAMS();
});

RSCG–AMS – About My software –Reading csproj– part 6

Now it is time to put some more data – like authors and version. I have read a lot ( and tried a lot) about  CompilerVisibleProperty and  CompilerVisibleItemMetadata ( see https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md  ) . However, I was unable to get the data ( Authors and Version) from there .

So this is what I was get, to read the csproj near the program:

private ItemsFromCSPROJ TryGetPropertiesFromCSPROJ(GeneratorExecutionContext context)
{
    var ret= new ItemsFromCSPROJ();
    try
    {
        var dirFolder = ((dynamic)(context.Compilation)).Options?.SourceReferenceResolver?.BaseDirectory;
        if (string.IsNullOrWhiteSpace(dirFolder))
            return ret;

        var file = Directory.GetFiles(dirFolder, "*.csproj");
        if (file.Length != 1)
            throw new ArgumentException($"find files at {dirFolder} :{file.Length} ");

        var xmldoc = new XmlDocument();
        xmldoc.Load(file[0]);
        XmlNode node;
        node = xmldoc.SelectSingleNode("//Authors");
        ret.Authors = node?.InnerText;
        node = xmldoc.SelectSingleNode("//Version");
        ret.Version = node?.InnerText;
        return ret;
    }
    catch(Exception )
    {
        //maybe log warning? 
        return ret;
    }

}

Next time I will show how it looks

RSCG–AMS – About My software –NuGet– part 5

The problem with RSCG is to differentiate  between the generator and the code generated. In my case , the base class should be in one nuget, the generator in other ( to can remove it from build) and the WebAPI in another.

That took me a whole day and the result is ok . Pain Points:

https://turnerj.com/blog/the-pain-points-of-csharp-source-generators 

CI action and Deploy to nuget

PackageReference Include=”Microsoft.VisualStudio.Web.CodeGeneration.Design

Now it works for WebAPI with

<PackageReference Include=”AMSWebAPI” Version=”2021.6.26.1937″ />
<PackageReference Include=”AMS_Base” Version=”2021.6.26.1937″ />
<PackageReference Include=”RSCG_AMS” Version=”2021.6.26.1937″ ReferenceOutputAssembly=”false” OutputItemType=”Analyzer” />

And I hve seen that I am not the only one to differentiate between CI servers – for example,

https://github.com/VerifyTests/DiffEngine/blob/master/src/DiffEngine/BuildServerDetector.cs

https://github.com/dotnet/Nerdbank.GitVersioning/blob/master/src/NerdBank.GitVersioning/CloudBuildServices/GitLab.cs

https://github.com/cake-build/cake/blob/develop/src/Cake.Common.Tests/Fixtures/Build/GitLabCIInfoFixture.cs

But now the work is done and you can access all AMS via web ,

app.UseEndpoints(endpoints =>
             {
                 endpoints.MapControllers();
                 endpoints.UseAMS();
             });

either to AMS/index.html , either to AMS/all .

RSCG–AMS – About My software –WebAPI– part 4

Now it should be an easy way to see in the WebAPI. First, return the data for all software that respected that :

public static IEndpointRouteBuilder UseAMS(this IEndpointRouteBuilder endpoints)
{
    endpoints.MapGet("/ams/All", async app =>
    {
                
            var data = AboutMySoftware.AllDefinitions.Select(it => it).ToArray();
        await app.Response.WriteAsJsonAsync(data);
    });
    return endpoints;
}

Now, how can I make a small html to display things ? I can do with Razor Library – but it is too big and maybe the developers do not want to have this dependency. So I decided for https://www.nuget.org/packages/Transplator/  – fairly easy to use. And is another RSCG that converts template code into C#  code.

So now the code looks like this:

public static IEndpointRouteBuilder UseAMS(this IEndpointRouteBuilder endpoints)
{
    endpoints.MapGet("/ams/All", async app =>
    {
                
            var data = AboutMySoftware.AllDefinitions.Select(it => it).ToArray();
        await app.Response.WriteAsJsonAsync(data);
    });
    endpoints.MapGet("/ams/index", app =>
    {
        var response = new ASMTemplate().Render();
        app.Response.ContentType = "text/html";
        return app.Response.WriteAsync(response);
    });
    return endpoints;
}

where the ASMTemplate is

<style>
table {
  font-family: arial, sans-serif;
  border-collapse: collapse;
  width: 100%;
}

td {
  border: 1px solid #dddddd;
  text-align: left;
  padding: 8px;
}

th{
background-color: black;
  color: white;
  border: 1px solid #dddddd;
  text-align: left;
  padding: 8px;
  }
tr:nth-child(even) {
  background-color: #dddddd;
}
</style>

<table>
<tr>
<th>Nr</td>
<th>Component</th>
<th>Date</th>
<th>Commit</th>
<th>RepoUrl</th>
</tr>

{%~ int i=1; ~%}
{%~ foreach(var item in AMS.AboutMySoftware.AllDefinitions){ %}
<tr>
<td>{% i++ %}</td>
<td>{% item.Key %} </td>
<td>{% item.Value.DateGenerated %} </td>
<td>{% item.Value.CommitId %} </td>
<td>{% item.Value.RepoUrl %}</td>
</tr>
{% } %}
</table>

It is time now to make the nuget packages.

RSCG–AMS – About My software –Multiple assemblies– part 3

The problem that I face now – and must be solved  – is what to do if I have multiple assemblies / dlls / asp.net core that wants to have the About My Software listed ? It will be a name conflict between the classes – or, if we put in different namespaces, will be difficult to find them to be listed .

For the second problem – it is relatively clear – we can have a Dictionary with the key AssemblyName and the value the instance of the AMS class for this assembly.

But how to initialize ?

First thing that I thought – static constructor . In the static constructor for AMS in the each assembly class – add to the above Dictionary the instance.

But , but … the static constructor is not called unless a class instance /static method  is called. So … ?

So ModuleInitializer to the rescue:

https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.moduleinitializerattribute?view=net-5.0

The code generated is now ( for an assembly with the name AMSConsole)

public class AboutMySoftware_AMSConsole : AboutMySoftware
{
    [System.Runtime.CompilerServices.ModuleInitializer]
    public static void Add_AboutMySoftware_AMSConsole()
    {
        AboutMySoftware.AllDefinitions.Add("AMSConsole", new AboutMySoftware_AMSConsole());
    }
    public AboutMySoftware_AMSConsole()
    {
        AssemblyName = "AMSConsole";
        DateGenerated = DateTime.ParseExact("20210624191615", "yyyyMMddHHmmss", null);
        CommitId = "not in a CI run";
        RepoUrl = "not in a CI run";
    }


}

The code to retrieve is modified like

Console.WriteLine("Show About My Software versions");
var amsAll = AboutMySoftware.AllDefinitions;
foreach (var amsKV in amsAll)
{
    var ams = amsKV.Value;

    Console.WriteLine($"{amsKV.Key}.{nameof(ams.AssemblyName)} : {ams.AssemblyName}");
    Console.WriteLine($"{amsKV.Key}.{nameof(ams.DateGenerated)} : {ams.DateGenerated}");
    Console.WriteLine($"{amsKV.Key}.{nameof(ams.CommitId)} : {ams.CommitId}");
    Console.WriteLine($"{amsKV.Key}.{nameof(ams.RepoUrl)} : {ams.RepoUrl}");
}

So far so good. Next implementation for WebAPI

RSCG–AMS – About My software –work– part 2

So now it is time to work at implementation This will be a standard RSCG –  generating code. I make also a test console to display the values.

The implementation will consider the fact that we can have many Source Control providers – each one with his ideas about variables. So I made 2 classes – one base abstract

abstract class AMS
{
    public AMS(GeneratorExecutionContext  context)
    {
        AssemblyName = context.Compilation.AssemblyName;
        GeneratedDate = DateTime.UtcNow;

    }
    public string AssemblyName { get; internal set; }
    public DateTime GeneratedDate { get; internal set; }

    public string CommitId { get; internal set; }
    public string RepoUrl { get; internal set; }
}

and one implementation for Github

//https://docs.github.com/en/actions/reference/environment-variables
class AMSGitHub : AMS
{
    public AMSGitHub(GeneratorExecutionContext  context):base(context)
    {
        CommitId = Environment.GetEnvironmentVariable("GITHUB_SHA");
        RepoUrl = Environment.GetEnvironmentVariable("GITHUB_SERVER_URL") + "/" + Environment.GetEnvironmentVariable("GITHUB_REPOSITORY");
    }
}

The code for generator is a bit more complicated:

var nameSpace = "AMS";            
var ams = new AMSGitHub(context);
var classDef=$@"
using System;
namespace {nameSpace} {{ 
public class AboutMySoftware{{
public string AssemblyName {{ get {{ return  ""{ams.AssemblyName}"" ; }} }}
public DateTime DateGenerated {{ get {{ return DateTime.ParseExact(""{ams.GeneratedDate.ToString("yyyyMMddHHmmss")}"", ""yyyyMMddHHmmss"", null); }} }}
public string CommitId  {{ get {{ return  ""{ams.CommitId}"" ; }}}}
public string RepoUrl {{ get {{ return  ""{ams.RepoUrl}"" ; }}}}
}}
        
}}";

The console to test has the following code

static void Main(string[] args)
{
    Console.WriteLine("Show About My Software versions");
    var ams = new AboutMySoftware();
    Console.WriteLine($"{nameof(ams.AssemblyName)} : {ams.AssemblyName}");
    Console.WriteLine($"{nameof(ams.DateGenerated)} : {ams.DateGenerated}");
    Console.WriteLine($"{nameof(ams.CommitId)} : {ams.CommitId}");
    Console.WriteLine($"{nameof(ams.RepoUrl)} : {ams.RepoUrl}");
}

and the output , in GitHub actions , is

Show About My Software versions
AssemblyName : AMSConsole
DateGenerated : 06/24/2021 03:16:51
CommitId : d8cb041470d93f68a4dc7fca7d131c207db8ab69
RepoUrl : https://github.com/ignatandrei/RSCG_AMS

RSCG–AMS – About My software –idea – part 1

Every product should have an About page . In the About page should be listed

  1. The product name
  2. The version  of the product
  3. Link to latest version ?
  4. Built date+ time
  5. The commit ID
  6. The authors
  7. Link to the License
  8. Other components version and link to about
  9. Third Party notices
  10. Repository  link ( github, gitlab, …)
  11. Documentation Link
  12. Release Notes link
  13.   Maybe log file ?
  14. Maybe latest errors ?
  15. Maybe system.info ?

 

This should be available for

  1. any dll – as a class
  2. any console project – as Console.WriteLine
  3. for any ASP.NET Core app
    1.   as a class
    2. as a  WebAPI
    3. as an HTML UI

You can see an example at https://netcoreblockly.herokuapp.com/AMS

Benchmarking RSCG vs Reflection

I make a microservices Buffet . In this I consider having email as a service . When the DevOps wants email, he can choose between various plugins ( simple smtp email, gmail, exchange, others). Those plugins can have various properties – that must be edited by the primary administrator of the microservice. The properties can be discovered at runtime ( via Reflection ) o r at build time ( via Roslyn Source Code Generators  – RSCG ).

But – we should see what is faster, right ?   And the feeling is that RSCG is always faster – but it is , really ? Let’s see…

So = let’s make a test with https://github.com/dotnet/BenchmarkDotNet . You can have the source code by going to https://github.com/ignatandrei/AOP_With_Roslyn/tree/master/AOPMethods .

First , the class that is tested

public partial class EmailSmtpClientMS 
{

    public EmailSmtpClientMS()
    {

        Port = 25;

    }
    public string Name { get; set; }


    public string Type
    {
        get
        {
            return this.GetType().Name;
        }
    }
    public string Host { get; set; }
    public int Port { get; set; }

    public string Description
    {
        get
        {
            return $"{Type} {Host}:{Port}";
        }
    }
}

 

Second, with AOPMethods I generate the read properties values – properties that  you can read – via a dictionary and via a switch. This can be achieved simply :

[AutoMethods(template = TemplateMethod.CustomTemplateFile, CustomTemplateFileName = "ClassToDictionary.txt")]
    public partial class EmailSmtpClientMS 

And this will be generated by RSCG for the switch

protected object GetValueProperty(string propName)
{
    switch (propName)
    {
        //true true
        case "Name":
            return this.Name;


        //true false
        case "Type":
            return this.Type;


        //true true
        case "Host":
            return this.Host;


        //true true
        case "Port":
            return this.Port;


        //true false
        case "Description":
            return this.Description;



        default:
            throw new ArgumentException("cannot find property " + propName);
    }
}

and for the dictionary

private IDictionary<string, PropertyHelper> MyProperties()
{
    var data = new Dictionary<string, PropertyHelper>();
    PropertyHelper ph;
    ph = new PropertyHelper();
    ph.Name = "Name";
    ph.Type = "string";
    ph.CanRead = !false;
    ph.CanWrite = !false;

    ph.Value = this.Name;

    data.Add("Name", ph);
    //Name string     
    ph = new PropertyHelper();
    ph.Name = "Type";
    ph.Type = "string";
    ph.CanRead = !false;
    ph.CanWrite = !true;

    ph.Value = this.Type;

    data.Add("Type", ph);
    //Type string     
    ph = new PropertyHelper();
    ph.Name = "Host";
    ph.Type = "string";
    ph.CanRead = !false;
    ph.CanWrite = !false;

    ph.Value = this.Host;

    data.Add("Host", ph);
    //Host string     
    ph = new PropertyHelper();
    ph.Name = "Port";
    ph.Type = "int";
    ph.CanRead = !false;
    ph.CanWrite = !false;

    ph.Value = this.Port;

    data.Add("Port", ph);
    //Port int     
    ph = new PropertyHelper();
    ph.Name = "Description";
    ph.Type = "string";
    ph.CanRead = !false;
    ph.CanWrite = !true;

    ph.Value = this.Description;

    data.Add("Description", ph);
    //Description string     

    return data;

}

 

The spec for benchmark are :


BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19043.1052 (21H1/May2021Update)
Intel Core i7-6600U CPU 2.60GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=5.0.301
  [Host]     : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT
  DefaultJob : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT

 

Third, I benchmark obtaining one single property – the Host – via the 3 methods:

public partial class EmailSmtpClientMSOneProperty: EmailSmtpClientMS
{
    [Benchmark]
    public string GetHostReflection()
    {
        return this.GetType().GetProperty("Host").GetValue(this).ToString();
    }
    [Benchmark]
    public string GetHostViaDictionary()
    {
        return this.ReadMyProperties()["Host"].ToString();
    }
    [Benchmark]
    public string GetHostViaSwitch()
    {
        return this.GetValueProperty("Host").ToString();
    }
}

And in Program.cs

BenchmarkRunner.Run<EmailSmtpClientMSOneProperty>(
    ManualConfig
        .Create(DefaultConfig.Instance)
        .WithOption(ConfigOptions.DisableOptimizationsValidator, true)
        );

( of course, I have added on the class
//[SimpleJob(RuntimeMoniker.Net50)]
//[ShortRunJob(RuntimeMoniker.Net50)]
//[DryJob(RuntimeMoniker.Net50)]
[Orderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)]
//[SimpleJob(RuntimeMoniker.NetCoreApp31)]
[RPlotExporter]
[CsvMeasurementsExporter]
[MemoryDiagnoser]
[HtmlExporter]
[MarkdownExporterAttribute.GitHub]
)
The results are in ns –so, the less/smaller , that means  better results.

The results are here in HTML form :

 

 

Method Mean Error StdDev Median Gen 0 Gen 1 Gen 2 Allocated
GetHostViaSwitch 18.07 ns 0.434 ns 0.549 ns 18.02 ns
GetHostReflection 144.13 ns 2.582 ns 5.501 ns 142.13 ns
GetHostViaDictionary 451.59 ns 12.363 ns 33.635 ns 441.72 ns 0.3057 640 B

The graphic may be more interesting:

 

Surprised ? The RSCG Switch Property is indeed the fastest one – but the Reflection is faster than RSCG Property Dictionary ( or , at least , for my implementation).

However, I realized that in real life , I will retrieve all properties in a Dictionary to be edited . So all implementations should occur the penalty of creating a Dictionary . Time for next benchmark . This time , the code is

[Benchmark]
public IDictionary<string,object> GetHostReflection()
{
    var props = this.GetType()
        .GetProperties()
        .Where(it=> it.CanWrite)
        .ToDictionary(it => it.Name, it=>it.GetValue(this));
        ;
    return props;
                
}
[Benchmark]
public IDictionary<string, object> GetHostViaDictionary()
{
    var props = this.ReadMyProperties();
    return props;
            
}
[Benchmark]
public IDictionary<string, object> GetHostViaSwitch()
{
    var props = ReadProperties
        .ToDictionary(it => it, it => GetValueProperty(it));
    return props;
            
            
}

 

and the results are:

Method Mean Error StdDev Median Gen 0 Gen 1 Gen 2 Allocated
GetHostViaDictionary 462.1 ns 14.70 ns 40.97 ns 453.6 ns 0.3052 640 B
GetHostViaSwitch 479.5 ns 7.34 ns 7.54 ns 479.9 ns 0.2708 568 B
GetHostReflection 973.0 ns 78.35 ns 231.01 ns 911.5 ns 0.1984 416 B

 

Now the graphic will help:

 

Interesting , right ?

Reflection = as normal –  is the slowest one. But the difference between RSCG switch and RSCG Dictionary is not too much…

Conclusion 1:  the feeling  was right at the end. But – the first result was deceiving

Conclusion 2: Creating a dictionary is more time consuming than a simple reflection for one property retrieved

Conclusion 3: I do prefer RSCG Dictionary vs RSCG switch – less work for me as a programmer and similar time results.

Conclusion 4: do not over engineer if you do not feel the need . For just one property, Reflection is better….

Conclusion 5: This is not final. I should also write the values of the properties . Maybe next time a new benchmark….

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.