Join our Discord server for community support and discussions Icon description

C# 9.0 Source Generation isĀ progressing quite nicely latelyĀ (Thanks, Jared!), with the addition of the ability to interact with the MSBuild environment such as getting Properties and Items to control how the generation happens.

In this post, Iā€™ll explain how to parseĀ .reswĀ files of a project to generate an enum that contains all the resources.

The full sample for this article isĀ here in the Fonderie Generators project.

Reading msbuild items and properties

In theĀ Roslyn generators cookbook, new entries have been added to include the APIs needed to get information from msbuild. In order to make the reading of those properties easier, hereā€™s a small extensions class:

internal static class SourceGeneratorContextExtensions
{
    private const string SourceItemGroupMetadata = "build_metadata.AdditionalFiles.SourceItemGroup";

    public static string GetMSBuildProperty(
        this SourceGeneratorContext context,
        string name,
        string defaultValue = "")
    {
        context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.{name}", out var value);
        return value ?? defaultValue;
    }

    public static string[] GetMSBuildItems(this SourceGeneratorContext context, string name)
        => context
            .AdditionalFiles
            .Where(f => context.AnalyzerConfigOptions
                .GetOptions(f)
                .TryGetValue(SourceItemGroupMetadata, out var sourceItemGroup)
                && sourceItemGroup == name)
            .Select(f => f.Path)
            .ToArray();
}

Letā€™s dive into what those extensions do.

GetMSBuildProperty

TheĀ GetMSBuildPropertyĀ method is assuming that a defined property has a non-empty value, as per the msbuild semantics. Hereā€™s how to get the default namespace for the current project:

var defineConstants = context.GetMSBuildProperty("RootNamespace");

Assuming that the associated msbuild property is added in the generatorā€™s associated props file:

<ItemGroup>
    <CompilerVisibleProperty Include="RootNamespace" />
</ItemGroup>

GetMSBuildItems

ForĀ GetMSBuildItems, since the roslyn APIs does not provide a way to discriminate items per their MSBuild item name, we need to use some metadata that can be added to theĀ AdditionalFilesĀ items. In order to get theĀ reswĀ files from a WinUI project, we can do the following:

var priResources = context.GetMSBuildItems("PRIResource");

For this code to work, we need to change a little bit the way items are added to the roslyn context:

<Target Name="_InjectAdditionalFiles" BeforeTargets="GenerateMSBuildEditorConfigFileShouldRun">
    <ItemGroup>
        <AdditionalFiles Include="@(PRIResource)" SourceItemGroup="PRIResource" />
    </ItemGroup>
</Target>

The use of a target here is needed because of the way NuGet packages property or targets files are handled by msbuild. If the ItemGroup is included directly at the root of the project, its evaluation is performed too early in the build process. This sequence misses items being added by the project or dynamically by other targets.

At this point, thereā€™s no clear injection point to execute this targe in Roslyn, butĀ GenerateMSBuildEditorConfigFileShouldRunĀ seems like an appropriate location for doing so at this point, right before the capture of the properties and items by the build.

Finally, to be able to discriminate items in theĀ AdditionalFilesĀ group, we use theĀ SourceItemGroupĀ metadata. For Roslyn to pick it up, we need to add the following:

<ItemGroup>
	<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="SourceItemGroup" />
</ItemGroup>

Generating code from the resw file

Now that we can read the items and properties, we can write a small generator that creates an enum with all the names found in aĀ reswĀ file:

[Generator]
public class ReswConstantsGenerator : ISourceGenerator
{
    public void Initialize(InitializationContext context)
    {
        // Debugger.Launch(); // Uncomment this line for debugging
        // No initialization required for this one
    }

    public void Execute(SourceGeneratorContext context)
    {
        var resources = context.GetMSBuildItems("PRIResource");

        if (resources.Any())
        {
            var sb = new IndentedStringBuilder();

            using (sb.BlockInvariant($"namespace {context.GetMSBuildProperty("RootNamespace")}"))
            {
                using (sb.BlockInvariant($"internal enum PriResources"))
                {
                    foreach (var item in resources)
                    {
                        XmlDocument doc = new XmlDocument();
                        doc.Load(item);

                        // Extract all localization keys from Win10 resource file
                        var nodes = doc.SelectNodes("//data")
                            .Cast<XmlElement>()
                            .Select(node => node.GetAttribute("name"))
                            .ToArray();

                        foreach (var node in nodes)
                        {
                            sb.AppendLineInvariant($"{node},");
                        }
                    }
                }
            }

            context.AddSource("PriResources", SourceText.From(sb.ToString(), Encoding.UTF8));
        }
    }
}

This will generate a file which contains an enum with all the resource names, in the default namespace of the current assembly.

Note that this generator does not validate the nameā€™s format, and if there are reserved characters or keywords, those are needed to be rewritten for C# to accept it.

Wrapping up

This simple sample should most likely be improved.

For instance, it could be interesting to create a generator analyzing another generator to create aĀ .targetsĀ file which contains the appropriateĀ CompilerVisibleItemMetadataĀ andĀ CompilerVisiblePropertyĀ for that generator to work properly.

The extension also only supports getting the identity of an item, but getting additional metadata would be useful, like getting theĀ LinkĀ attribute when dealing with linked files in projects.

You can find theĀ sample of this article here, and as of the writing of this post, Visual Studio 16.8 Preview 2.1 does not yet show the generated code or highlights properly but builds with the generated code properly. This should be improving the next previews.

Until next time, happy generation!

Uno Platform 5.2 LIVE Webinar – Today at 3 PM EST – Watch