Category: roslyn

AutoActions for Skinny controllers–custom template

Now I want to let the user make his own template. For this, I have enriched the attribute AutoActionsAttribute with a

public string CustomTemplateFileName { get; set; }

 

The code was pretty easy, just reading from GeneratorExecutionContext . AdditionalFiles instead of reading from the template in the dll

 

switch (templateId)
{

case TemplateIndicator.None:
	context.ReportDiagnostic(DoDiagnostic(DiagnosticSeverity.Info, $"class {myController.Name} has no template "));
	continue;
case TemplateIndicator.CustomTemplateFile:

	var file = context.AdditionalFiles.FirstOrDefault(it => it.Path.EndsWith(templateCustom));
	if (file == null)
	{
		context.ReportDiagnostic(DoDiagnostic(DiagnosticSeverity.Error, $"cannot find {templateCustom} for  {myController.Name} . Did you put in AdditionalFiles in csproj ?"));
		continue;
	}
	post = file.GetText().ToString();
	break;

default:
	using (var stream = executing.GetManifestResourceStream($"SkinnyControllersGenerator.templates.{templateId}.txt"))
	{
		using var reader = new StreamReader(stream);
		post = reader.ReadToEnd();

	}
	break;
}

 

There are 2 small catches

1 see the EndsWith  ? The GeneratorExecutionContext . AdditionalFiles  gives you the full path

2. the additional files should be registered in the .csproj

<ItemGroup>
<AdditionalFiles Include=”Controllers\CustomTemplate1.txt” />
</ItemGroup>

 

Now the user can define his own template for the controller like this


[AutoActions(template = TemplateIndicator.CustomTemplateFile, FieldsName = new[] { "*" } ,CustomTemplateFileName = "Controllers\\CustomTemplate1.txt")]
    [Route("api/[controller]/[action]")]
    [ApiController]
    public partial class CustomTemplateController : ControllerBase
    {
        private readonly RepositoryWF repository;

        public CustomTemplateController ()
        {
            //do via DI
            repository = new RepositoryWF();
        }

    }

And this is all ! ( ok. some documentation should be involved)

AutoActions for Skinny controllers–code improvements and more docs

I realized that this code

Assembly.GetExecutingAssembly();

was executing for each controller. So I decided to move to a class variable and attribute once.

Also, I may want to have all fields – so I decided to express via a special field *

The code modifications were, thanks to Linq, pretty small:

bool All = fields.Contains(“*”);

var memberFields = myController
     .GetMembers()
     .Where(it => All || fields.Contains(it.Name))

Also,  modified the documents to show most errors and their problems:

error CS0260: Missing partial modifier on declaration of

Failed to load API definition.
Fetch error undefined /swagger/v1/swagger.json

Any open source project becomes a marathon, not a sprint.

AutoActions for Skinny controllers-documentation improvements

One improvement is to move  Initialize in other partial class. . It was difficult every time I need to activate the debugger

public void Initialize(GeneratorInitializationContext context)
         {

            context.RegisterForSyntaxNotifications(() => new SyntaxReceiverFields());
             //in development
             //Debugger.Launch();
         }

Second was to improve the Nuget package description. I have added

<Version>2020.11.28.2108</Version>
    <Authors>Andrei Ignat</Authors>
    <PackageTags>RoslynCodeGenerators C# CSharp SkinnyControllers</PackageTags>
    <PackageProjectUrl>https://github.com/ignatandrei/AOP_With_Roslyn</PackageProjectUrl>
    <RepositoryUrl>https://github.com/ignatandrei/AOP_With_Roslyn</RepositoryUrl>
    <RepositoryType>GIT</RepositoryType>

     <PackageLicenseExpression>MIT</PackageLicenseExpression>
      <Copyright>MIT</Copyright>

Third was to improve documentation. I was adding a readme.txt.

<ItemGroup>
   <None Include=”../readme.txt”>
     <Pack>True</Pack>
     <PackagePath></PackagePath>
   </None>
</ItemGroup>

Fourth – automatic builds. You can see them at https://github.com/ignatandrei/AOP_With_Roslyn/actions

AutoActions for Skinny controllers–second template

The second template was pretty easy. All the date was inside the class definition :

class ClassDefinition
     {
         public string ClassName;
         public string NamespaceName;
         public Dictionary<string,MethodDefinition[]> DictNameField_Methods;
         public string version = ThisAssembly.Info.Version;   
     }
     class MethodDefinition
     {
         public string Name { get; set; }
         public string FieldName { get; set; }
         public string ReturnType;
         public bool ReturnsVoid;
         //name, type
         public Dictionary<string, ITypeSymbol> Parameters;

        public string parametersDefinitionCSharp => string.Join(“,”, Parameters.Select(it => it.Value.ContainingNamespace + “.” + it.Value.Name + ” ” + it.Key).ToArray());
         public string parametersCallCSharp => string.Join(“,”, Parameters.Select(it => it.Key).ToArray());

        public int NrParameters
         {
             get
             {
                 return Parameters?.Count ?? 0;
             }
         }

    }

so the only thing that I needed is to modify the template definition

I was having one problem: I have hardcoded the names in the first stage- so I have had problems with duplicate names: . As an example, it was:

namespace {{NamespaceName}} {
   [GeneratedCode(“SkinnyControllersGenerator”, “{{version}}”)]
   [CompilerGenerated]
   partial class WeatherForecastController{

and now it is

namespace {{NamespaceName}} {
   [GeneratedCode(“SkinnyControllersGenerator”, “{{version}}”)]
   [CompilerGenerated]
   partial class {{ClassName}}{

Besides this, the SCRIBAN documentation is pretty self explanatory to male the second template ready

AutoActions for Skinny controllers- templating-part2

Now I want to add templates to my controller  generators . But this feature changed the whole perspective about the project. Why ? Because a template do not apply just to a field – but to the whole controller. So instead of having attributes on the field

[AutoActions]

private readonly RepositoryWF repository;

I will have attributes on the controller

[AutoActions(template = TemplateIndicator.AllPost,FieldsName =new[] { “repository” })]

[ApiController]

[Route(“[controller]/[action]”)]

public partial class WeatherForecastController : ControllerBase

I was thinking out about this modification. It is better , because now we can see also the route – if the routing is not well, the actions are not working ( if it is not a pure REST , then the route must have [action])

The code- oh, the code. It is a mostly complete refactoring of the code . The fact that I have organized into methods – processClass, processFields have make the pain less … but anyway – you will find the modifications here: https://github.com/ignatandrei/AOP_With_Roslyn/commit/eba37f0c7b1aadb56e40b52af57afedc9925eb5a .

And SCRIBAN performed well – no big problems. I have just made a template that makes the actions POST, named AllPost.

Deployed also on NuGet

AutoActions for Skinny controllers–user customization

The generation of controllers actions is now very rude:

  1. Http Method: if the method has no arguments, I assume GET . Else it is POST
  2. What is generated in the Action Body is hard-coded. What if the user wants something special ?
  3. The answer from the Action is hardcoded to the answer that the method returns. What if we want something different ?

So some customization should be involved. I need

  1. a template engine – I choose https://github.com/lunet-io/scriban , to make a change from Razor Pages
  2. a decision – choose a template for each action. This should / could be implemented by the programmer – as is the person who  knows best how to do generate from his actions. However, this should be implemented . 

So, first question : how the programmer specifies the decision ( and this decision could be different for each class / field instance) ? This is quite a problem. The programmer should make either a simple decision for their public functions ( like REST API ), either a complicated one – like for doing HttpGet[{id}] or Level 3 REST API ( read above). So , basic, the programmer should indicate a function that depends on

  1. Name of the public method
  2. Return Type
  3. Parameters of the function

and return different template. The template is taking as parameters the same things  – and return how the Action will look like.

And now I have seen the light . The function and the template are taking the same arguments. And what is great in programming are pointers. So , the light version of pointers in this case is an enum . I will use an enum , put into https://www.nuget.org/packages/SkinnyControllersCommon/ , in order to indicate the template ,

The programmer will improve SkinnyControllersCommon with a new enum and a new template, put a PR , and voila!  – new version of template for all programmers!

The implementation next time

AutoActions for Skinny controllers-Templating-part1

In order to do templating, I have transform all text generation into classes

Example:

code.AppendLine($”{fieldName}.{ms.Name}({parametersCall});”);

into

class MethodDefinition

{

public string ClassName { get; set; }

public string Name { get; set; }

public Type ReturnType;

public bool ReturnsVoid;

//name, type

public Dictionary<string, string> Parameters;

}

( of course, this definition has changed multiple times…)

Then I have added SCRIBAN to the project to can add a template engine that can interpret the classes .

And – it works –  this is mostly because in our days a template engine can be found easy !

But the level where it works is at class , not at field level. The decision will be taken tomorrow.

AutoActions for Skinny controllers–small customizations

Logging Roslyn Code Generator:

For each program that you develop , it is important ( if not vital ) to see logging. In the Roslyn  Analyzer / Code Generators, the diagnostics are logged with DiagnosticDescriptor and Diagnostic – think that you should see in the output console when compiling and this will become clear.

More, the enum DiagnosticSeverity Enum (Microsoft.CodeAnalysis) | Microsoft Docs  can be seen as default for Warn and Info – but not for Info. Info can be seen only with

dotnet build -v diag

The code that I used is:

static Diagnostic DoDiagnostic(DiagnosticSeverity ds,string message)
         {
             //info  could be seen only with
             // dotnet build -v diag
             var dd = new DiagnosticDescriptor(“SkinnyControllersGenerator”, $”StartExecution”, $”{message}”, “SkinnyControllers”, ds, true);
             var d = Diagnostic.Create(dd, Location.Create(“skinnycontrollers.cs”, new TextSpan(1, 2), new LinePositionSpan()));
             return d;
         }

And I called like this

string name = $”{ThisAssembly.Project.AssemblyName} {ThisAssembly.Info.Version}”;
context.ReportDiagnostic(DoDiagnostic(DiagnosticSeverity.Info,name));

( Yes , I used a Roslyn Code Generator – GitHub – kzu/ThisAssembly: Exposes project and assembly level information as constants in the ThisAssembly class using source generators powered by Roslyn. – inside another code generator – and no reference used on deploy – how cool is that ? )

Also, I use this to display some warnings when something is wrong , but I do not generate code. For example:

var ms = m as IMethodSymbol;
if (ms is null)
{
     context.ReportDiagnostic(DoDiagnostic(DiagnosticSeverity.Warning, $”{m.Name} is not a IMethodSymbol”));
     continue;

}

Intellisense for Code Generators:

This is something easy. First, I did not have intellisense. After reading , I discovered that I should put above the class declaration :

[GeneratedCode(“”{ThisAssembly.Info.Product}””, “”{ThisAssembly.Info.Version}””)]
[CompilerGenerated]

( Yes, again code generator  – thanks,  KZU ! – to the rescue).

In this manner , if something wrong, I can see the version of the SkinnyControllersGenerator right away .

AutoActions for Skinny controllers–deploying at NuGet

First time I was thinking that is enough to do

dotnet pack

to make a Nuget package to deploy on NUGET. It was not. I realized that the generator was not starting at all! And it was not in the project Dependencies=>Analyzers .

Time to read more from https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.cookbook.md , I realized that I was wrong – for analyzers /source generators we need more. So I put

<!– Package the generator in the analyzer directory of the nuget package –>
<None Include=”$(OutputPath)\$(AssemblyName).dll” Pack=”true” PackagePath=”analyzers/dotnet/cs” Visible=”false” />

to include in the nuget package at analyzers.

After this, the next error was

CSC : warning CS8032: An instance of analyzer SkinnyControllersGenerator.AutoActionsGenerator cannot be created from C:\Users\Surface1\.nuget\packages\skinnycontrollersgenerator\2020.11.24.2135\analyzers\dotnet\cs\SkinnyControllersGenerator.dll : Exception has been thrown by the target of an invocation..

Most probably, this comes from the fact that the SkinnyControllersGenerator has a dependency from another Dll/Nuget, SkinnyControllersCommon . Time to read more from https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.cookbook.md . I figured that the example with Newtonsoft.JSON is to complicated for me, so I took the decision to just include the file AutoActionsAttribute as link.

And now it works! – see https://www.nuget.org/packages/SkinnyControllersGenerator/

AutoActions for Skinny controllers–first implementation

Now with the implementation.

First, I find all the fields declarations that had  the  Autonotify 

if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax
                         && fieldDeclarationSyntax.AttributeLists.Count > 0)
             {
                 foreach(var al in fieldDeclarationSyntax.AttributeLists)
                 {
                     var att = al.Attributes;
                     foreach(var at in att)
                     {
                         var x = at.Name as IdentifierNameSyntax;
                         if(autoActions.Contains(x.Identifier.Text))
                         {
                             CandidateFields.Add(fieldDeclarationSyntax);
                             return;
                         }
                     }
                 }
                
             }

Second , I must find all the methods ( without the constructor) for generating data:

if (!(context.SyntaxReceiver is SyntaxReceiverFields receiver))
                 return;
             var compilation = context.Compilation;
             var fieldSymbols = new List<IFieldSymbol>();
             foreach (var field in receiver.CandidateFields)
             {
                 SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree);
                 foreach (var variable in field.Declaration.Variables)
                 {
                     var fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
                     var attr = fieldSymbol.GetAttributes();
                     if (attr.Any(ad => ad.AttributeClass.Name == autoActions))
                     {
                         fieldSymbols.Add(fieldSymbol);
                     }
                 }

                foreach (var group in fieldSymbols.GroupBy(f => f.ContainingType))
                 {
                     string classSource = ProcessClass(group.Key, group.ToArray(),   context);
                     if (string.IsNullOrWhiteSpace(classSource))
                         continue;

                    context.AddSource($”{group.Key.Name}_autogenerate.cs”, SourceText.From(classSource, Encoding.UTF8));
                 }
             }

You can find the source code at https://github.com/ignatandrei/AOP_With_Roslyn/releases/tag/2020.11.23 .

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.