# Using Source Generators to create a Blazor icon library

In this article, you'll learn how to utilize Source Generators to create an icon library for Blazor apps. Source Generators are a powerful tool, which can be used to generate additional C# code during compilation. We won't be designing the icons ourselves, instead, we'll use Lucide, an awesome open-source project with hundreds of beautiful icons. Let's get started!

# Setting up the solution

First, we'll create the directory structure below. In the `icons` folder, we'll put a couple of SVG files from the Lucide library.

```plaintext
.
├── icons
│   ├── file-1.svg
│   ├── file-2.svg
│   ├── ...
│   └── file-n.svg
└── src
    ├── Lucide.Blazor.sln
    ├── Lucide.Blazor
    │   └── Lucide.Blazor.csproj
    ├── Lucide.Blazor.Generators
    │   └── Lucide.Blazor.Generators.csproj
    └── Lucide.Blazor.App
        └── Lucide.Blazor.App.csproj
```

With the commands below, you can create the necessary source files we'll need for development. Make sure that the `Lucide.Blazor.Generators.csproj` project is targeting the `netstandard2.0` framework. This is a requirement to use Source Generators.

```bash
# ⬇ creates a blank solution
dotnet new sln -n "Lucide.Blazor"

# ⬇ creates a class library
dotnet new classlib -n "Lucide.Blazor.Generators" -f "netstandard2.0"

# ⬇ creates a razor class library 
dotnet new razorclasslib -n "Lucide.Blazor" -f "net7.0"

# ⬇ creates an empty Blazor WASM project
dotnet new blazorwasm-empty -n "Lucide.Blazor.App" -f "net7.0"

# ⬇ adds the generator project to the solution
dotnet sln "Lucide.Blazor.sln" add "Lucide.Blazor.Generators/Lucide.Blazor.Generators.csproj"

# ⬇ add the component project to the solution
dotnet sln "Lucide.Blazor.sln" add "Lucide.Blazor/Lucide.Blazor.csproj"

# ⬇ add the Blazor app to the solution
dotnet sln "Lucide.Blazor.sln" add "Lucide.Blazor.App/Lucide.Blazor.App.csproj"

# ⬇ add a project reference from the generator to the component library
dotnet add "Lucide.Blazor/Lucide.Blazor.csproj" reference "Lucide.Blazor.Generators/Lucide.Blazor.Generators.csproj"

# ⬇ add a project reference from the component library to the app
dotnet add "Lucide.Blazor.App/Lucide.Blazor.App.csproj" reference "Lucide.Blazor/Lucide.Blazor.csproj"
```

Once all projects are created, open the `Lucide.Blazor.Generators.csproj` file and make the following adjustments:

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <!-- ⬇ Helpful to debug the generator -->
    <IsRoslynComponent>true</IsRoslynComponent>
  </PropertyGroup>
  <!-- ⬇ Necessary packages  -->
  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
  </ItemGroup>
</Project>
```

Then edit the `Lucide.Blazor.csproj` file and make these changes:

```xml
<Project Sdk="Microsoft.NET.Sdk.Razor">
  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <SupportedPlatform Include="browser" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="7.0.4" />
  </ItemGroup>
  <ItemGroup>
    <!-- ⬇ Need to add "ReferenceOutputAssembly" and "OutputitemType" attributes -->
    <ProjectReference Include="..\Lucide.Blazor.Generators\Lucide.Blazor.Generators.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
  </ItemGroup>
  <ItemGroup>
    <!-- ⬇ Adds the svg files as additional analyzer files to the project -->
    <AdditionalFiles Include="..\..\icons\*.svg" />
  </ItemGroup>
</Project>
```

# Creating the Source Generator

The concept of the generator is to loop over each SVG file found in the `icons` folder and build a static dictionary of icon data. The file name will be used as a key and the file contents will be used as the value.

Add a new class to the project and ensure it has a `[Generator]` attribute and implements the `IIncrementalGenerator` interface.

```csharp
[Generator]
public class Generator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext ctx)
    {
    }
}
```

Then, we'll implement the `Initialize` method, by fetching all the SVG files and turning them into an array of key-value tuples.

```csharp
// fetch all SVG files 
var files = ctx.AdditionalTextsProvider.Where(file => file.Path?.EndsWith(".svg") == true);

// create key-value tuples and convert to an array with Collect()
var iconsProvider = files.Select((file, cancel)
    => (
        Name: Path.GetFileNameWithoutExtension(file.Path),
        Svg: file.GetText(cancel)?.ToString()
    )).Collect();
```

Next, we'll register the source output. This is the generation part, where we'll build the dictionary of icon names and svg contents in a static C# class. To do so, we'll use a `StringBuilder` using the following templates:

```plaintext
// Template for creating a key value pair in C# on dictionary initialization
$$"""{ "{{Name}}", {{"\"\"\""}}{{value}}{{"\"\"\""}
```

```plaintext
// Template for creating a static class with prefilled icon key-value pairs
$$"""
namespace Lucide.Blazor.Data;

public static class IconSet
{
    public static IReadOnlyDictionary<string, string> Icons = new Dictionary<string, string>()
    {
        {{sb}}
    };
}
""";
```

```csharp
ctx.RegisterSourceOutput(iconsProvider, (spc, iconsArray) =>
{
    var sb = new StringBuilder();

    foreach (var (Name, Svg) in iconsArray)
    {
        var value = Extract(Name, Svg);
        // Add individual key value pairs to the StringBuilder
        sb.AppendLine("<keyvaluepair_template>");
    }
    
    // compose the whole dictionary
    var fileTemplate = "<class_template>"
    
    // Adds the file template with specific name
    spc.AddSource("IconSet.g.cs", fileTemplate);
});
```

We are also applying some additional logic to the file contents, using the `Extract` method. The reason for this is that we don't want the whole SVG file as part of the dictionary, as we will reconstruct the SVG element using Blazor later on. All we need is the child elements of the SVG file, which we can obtain like this:

```csharp
private string Extract(string name, string? value)
{
    var svg = XDocument.Parse(value);
    var elements = svg.Root.Descendants()
        .Select(element =>
        {
            element.Name = element.Name.LocalName;
            return element.ToString(SaveOptions.DisableFormatting);
        });

    var paths = string.Concat(elements);
    return paths;
}
```

When you compile the project now, you should be able to see a generated `IconSet.g.cs`file in the `Lucide.Blazor.csproj` project.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1678970980059/d40bda3b-6f76-4590-ad34-8bfb5f8e44d1.png align="center")

# Creating the Blazor component

Now that our data is generated, we can start building the UI component. Head over to the `Lucide.Blazor.csproj` project and add a new class `Icon.cs` and ensure it implements `ComponentBase`.

```csharp
public class Icon : ComponentBase
{
}
```

First, we'll add a couple of parameters that we want to make configurable, which are related to the SVG structure.

```csharp
[Parameter] public string Name { get; set; } = "";
[Parameter] public string Css { get; set; } = "";
[Parameter] public string Width { get; set; } = "24";
[Parameter] public string Height { get; set; } = "24";
[Parameter] public string ViewBox { get; set; } = "0 0 24 24";
[Parameter] public string Fill { get; set; } = "none";
[Parameter] public string Stroke { get; set; } = "currentColor";
[Parameter] public string StrokeWidth { get; set; } = "2";
[Parameter] public string StrokeLinecap { get; set; } = "round";
[Parameter] public string StrokeLinejoin { get; set; } = "round";
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary<string, object> Attributes { get; set; } = new Dictionary<string, object>();
```

Next, we'll override the `BuildRenderTree` method. The concept is that we look up an icon by its key in the generated dictionary and then we use a `RenderTreeBuilder` to reconstruct an SVG element, using the provided parameters.

```csharp
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
    base.BuildRenderTree(builder);

    var icon = IconSet.Icons.FirstOrDefault(i => i.Key.Equals(Name, StringComparison.OrdinalIgnoreCase));

    builder.OpenElement(0, "svg");
    builder.AddAttribute(1, "xmlns", "http://www.w3.org/2000/svg");
    builder.AddAttribute(2, "width", Width);
    builder.AddAttribute(3, "height", Height);
    builder.AddAttribute(4, "viewBox", ViewBox);
    builder.AddAttribute(5, "fill", Fill);
    builder.AddAttribute(6, "stroke", Stroke);
    builder.AddAttribute(7, "stroke-width", StrokeWidth);
    builder.AddAttribute(8, "stroke-linecap", StrokeLinecap);
    builder.AddAttribute(9, "stroke-linejoin", StrokeLinejoin);

    if (Attributes?.Any() == true)
    {
        builder.AddMultipleAttributes(10, Attributes);
    }

    builder.AddMarkupContent(11, icon.Value);
    builder.CloseElement();
}
```

The last method we'll override is `OnParametersSet`, where we will do some validation of the parameters.

```csharp
protected override void OnParametersSet()
{
    base.OnParametersSet();

    if (string.IsNullOrWhiteSpace(Name)) throw new ArgumentNullException(nameof(Name));

    if (!string.IsNullOrWhiteSpace(Css))
    {
        Attributes["class"] = Css;
    }
}
```

# Trying it out

With the coding out of the way, we can now try out the library in the Blazor WASM project. Navigate to the `Index.razor` page and add the code below to show an icon on the page.

```xml
@page "/"
<Icon Name="bug" Stroke="red" Width="250" Height="250" />
```

When you launch the app you'll see a successfully rendered icon on your screen.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1678974372600/49bb5007-2f99-4f0c-a9d6-7e1d5cd6142d.png align="center")

# Source code and more

Check out the full source code or Nuget package on [GitHub](https://github.com/brecht-vde/lucide-blazor). Where you can also find out how to unit test Source Generators and Blazor components using the Verify library.

# References

* [https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview)
    
* [https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md)
    
* [https://medium.com/@saravananganesan/exploring-c-source-generators-part-1-understanding-registerpostinitializationoutput-49dfd08ca052](https://medium.com/@saravananganesan/exploring-c-source-generators-part-1-understanding-registerpostinitializationoutput-49dfd08ca052)
    
* [https://andrewlock.net/series/creating-a-source-generator/](https://andrewlock.net/series/creating-a-source-generator/) [https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/)
    
* [https://www.youtube.com/watch?v=azJm\_Y2nbAI&ab\_channel=JetBrains](https://www.youtube.com/watch?v=azJm_Y2nbAI&ab_channel=JetBrains)
    
* [https://lucide.dev/](https://lucide.dev/)
    
* [https://github.com/VerifyTests](https://github.com/VerifyTests)
