Skip to main content

Simple but effective use of C# Source Generators

Most C# source generator examples I have encountered included more advanced features like augmenting existing classes with generated code. There are much simpler scenarios where they are useful though. For instance, just avoiding typing repetitive code. For the full source code, consult my GitHub repo here.

Imagine writing a number of overloads where only the function name changes. In this case, I want to create HTML tags as strings using a function. The example is a set of overloads for the anchor ('a') tag:


public static Node a(params Node[] nodes) =>
    element(nameof(a), Array.Empty<IAttribute>(), nodes);
public static Node a(params IAttribute[] attributes) =>
    element(nameof(a), attributes, Array.Empty<Node>());
public static Node a(IEnumerable<IAttribute> attributes, params Node[] children) =>
    element(nameof(a), attributes, children);

Now I want the have the same overloads for all HTML tags. That's a lot of repetition. To create a source generator for this all I basically have to do is create a text template for the above. Now I am not going to describe the nuts and bolts of C# source generators because there is plenty of documentation. So I will only show the source of the generator here:

	
[Generator]
public class ElementsGenerator : ISourceGenerator
{
    private string GetElementMethods(string tagName) =>
        $@"
            public static Node {tagName}(params Node[] nodes) =>
                element(nameof({tagName}), Array.Empty<IAttribute>(), nodes);

            public static Node {tagName}(params IAttribute[] attributes) =>
                element(nameof({tagName}), attributes, Array.Empty<Node>());

            public static Node {tagName}(IEnumerable<IAttribute> attributes, params Node[] children) =>
                element(nameof({tagName}), attributes, children);
        ";


    public void Execute(GeneratorExecutionContext context)
    {
        string[] tagNames = new string[]
        {
            "a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "@base", "basefont",
            "bdi", "bdo", "big", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content",
            "data", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption",
            "figure", "font", "footer", "form", "frame", "fraeset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html",
            "i", "iframe", "img", "input", "ïns", "kbd", "label", "legend", "li", "link", "main", "map", "mark", "menu", "menuitem",
            "meta", "meter", "nav", "noembed", "noframes", "noscript", "@object", "ol", "optgroup", "option", "output", "p", "param",
            "picture", "pre", "progress", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp", "script", "section", "select", "shadow", "slot",
            "small", "source", "span", "strike", "strong", "style", "sub", "summary", "sup", "svg", "table", "tbody", "td", "template", "text",
            "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"
        };

        var methodsStringBuilder = new StringBuilder();

        for (int i = 0; i < tagNames.Length; i++)
        {
            methodsStringBuilder.Append(GetElementMethods(tagNames[i]));
            methodsStringBuilder.Append(Environment.NewLine);
        }

        string classText =
        $@"
            using System;

            namespace Radix.Components.Html;

            public static partial class Elements
            {{
                public static Node text(string text) => new Text(text);

                public static Concat concat(params Node[] nodes) => new(nodes);

                public static Element element(Name name, IEnumerable<IAttribute> attributes, params Node[] children) => new(name, attributes, children);

                {methodsStringBuilder}
            }}
        ";

        // Register the source
        context.AddSource("Elements", classText);
    }

    public void Initialize(GeneratorInitializationContext context) { }
}

As you can see, the only method that is implemented is the execute method and the only thing it does is adding generated code to the compilation of the project where the generator project is included.

Comments

Popular posts from this blog

Running Microsoft Playwright in an Azure Function using C#

When you have tried to run MS Playwright using C# in the context of an Azure Function, you probably have run into this message: The driver it is referring to resides in the .playwright folder that is copied to the build output folder. Now, the output folder structure of an Azure Function project is different from most projects in the sense that there is an extra nested bin folder where the drivers should actually be copied.  The build target that the Playwright team uses, at the time of writing (version 1.15.4), always copies the folder containing the driver to the root of the output folder. So the fix here is to add an extra build target to your project file, the corrects for the extra nested folder:   <Target Name="FixPlaywrightCopyAfterBuild" AfterTargets="Build">     <ItemGroup>       <_BuildCopyItems Include="$(OutDir).playwright\**" />     </ItemGroup>     <Message Text="[Fix] Copying files to the ...

Model View Update (MVU) pattern using ASP .NET components and Blazor

A primer on Model View Update (MVU) (UPDATE: The MVU library will be available via  Blazique/Blazique (github.com)  soon. The projects will be removed from Radix with the next major release after the move has been completed). The Model-View-Update (MVU) architecture is a sophisticated blueprint for designing interactive programs, including web applications and games. This architecture is characterized by a unidirectional data flow and is composed of three integral components: the Model, the View, and the Update. The Model is the heart of your application, encapsulating its state. The model preferably should be represented by an immutable data type. The View function is tasked with creating the visual representation of your application. It is a pure function that takes the current model as input and returns a user interface description, such as HTML elements or graphics. The View function refrains from performing any side effects...

Blazique demo implementation of Conduit, from RealWorld

In the previous post, I announced Blazique, the MVU library for Blazor. An implementation of Conduit, the "mother of all demo applications", is in the works, and pretty far along at  Blazique/Conduit: RealWorld sample app Conduit implementation using Blazique (github.com)  ...  Conduit is an initiative from RealWorld to provide an exemplary real-world application to show the capabilities of libraries and frameworks. At the time of writing, more than 150 implementations are using a multitude of (combinations of) backend and frontend technology stacks. Check it out on  Welcome on RealWorld | RealWorld (main--realworld-docs.netlify.app)  and  gothinkster/realworld: "The mother of all demo apps" — Exemplary fullstack Medium.com clone powered by React, Angular, Node, Django, and many more (github.com) .