Easy access to Embedded Resource Streams with T4

Easy access to Embedded Resource Streams with T4

So, you like to use CopyAlways and relative paths for files that you need as an input stream to your application. Maybe it's a CSV file, maybe some JSON data - either way it's just not something you want to force into a long C# string!

It's understandable, but very bad practice.

What could go wrong?

It will fall over sooner or later! Making assumptions on the executing file system is eventually going to lead to code that fails in a production environment. Even worse, in your DevOps agent! You'll then have the pleasure of uttering those most famous words...

Well, it works for me!

What should we be doing?

In C# we have the very simple option of setting a file as an EmbeddedResource.

image.png

What's great is that we can access this file from the Assembly instead of the file system. And it will always be available regardless of where or how the application is being hosted.

What's not so great about this is that it is not strongly typed! We have to hard code a string that represents the path and filename relative to the project root (but with . instead of /). We also need to read the Resource as a Stream like so:

var resourceName = "EmbeddedResourceT4.SomeFolder.SomeSubfolder.LoremIpsum.txt";
using (Stream stream = assembly.GetManifestResourceStream(resourceName))
  using (StreamReader reader = new StreamReader(stream))
  {
      string result = reader.ReadToEnd();
      return result;
  }

Not difficult, but not really elegant!

I feel like there is a suggestion coming...

Well, you'd be right!

I am providing a T4 template example of how to automate strongly typed code generation for your EmbeddedResources. It comes with a few caveats...

  • You need Visual Studio, it doesn't work with VSCode. We are accessing the EnvDTE which is part of the Visual Studio T4 template design time execution.
  • I am assuming any embedded files are String content and we don't mind streaming them immediately to memory.

If you want to go ahead and get more creative, you may want to vary the code generation by file extension, and use different methods to handle reading the stream. i.e. I have one example where any .json files get automatically parsed to JsonDocument objects.

Below you will find the T4 template file EmbeddedResourceStream.tt and beneath that an example of the auto-generated output EmbeddedResourceStream.cs.

// This is an auto-generated file.
<#@ template language="C#" hostSpecific="true" debug="True" #>
<#@ output extension="cs" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="EnvDte" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System" #>
<#
  var visualStudio = (this.Host as IServiceProvider).GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE;
  var project = visualStudio.Solution.FindProjectItem(this.Host.TemplateFile).ContainingProject as EnvDTE.Project;
  var projectItems = GetProjectItemsRecursively(project.ProjectItems);
#>
using System.IO;
using System.Reflection;

public static class EmbeddedResourceStream
{
<# 
foreach(var embededResource in projectItems.Keys){
    WriteLine("    public static string " + embededResource 
                     + " => ReadResource(Assembly.GetExecutingAssembly().GetName().Name + \"."
                     + projectItems[embededResource]
                     + "\");"
                     );
}
#>

    private static string ReadResource(string resourceName){
        using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
          using (StreamReader reader = new StreamReader(stream))
          {
              string result = reader.ReadToEnd();
              return result;
          }
    }
}
<#+
    public Dictionary<string,string> GetProjectItemsRecursively(EnvDTE.ProjectItems items)
    {
        var embededItems = new Dictionary<string,string>();
        if (items == null) return embededItems;
        var root = Host.ResolvePath("");

        foreach(EnvDTE.ProjectItem item in items)
        {
            string fName = item.FileNames[1];//.Replace("\\", "/");
            if(!fName.EndsWith("\\") && item.Properties.Item("BuildAction").Value.ToString() == "3"){
                var fullName = fName.Substring(root.Length + 1).Replace("\\", ".");
                var fullNameSplit = fullName.Split('.');
                embededItems.Add(fullNameSplit[fullNameSplit.Length-2], fullName);
            }

            var childItems = GetProjectItemsRecursively(item.ProjectItems);
            foreach (var childItem in childItems)
            {
                if(!embededItems.ContainsKey(childItem.Key)){
                    embededItems.Add(childItem.Key, childItem.Value);
                }
            }
        }
        return embededItems;
    }
#>

...which produces...

// This is an auto-generated file.
using System.IO;
using System.Reflection;

public static class EmbeddedResourceStream
{
    public static string LoremIpsum => ReadResource(Assembly.GetExecutingAssembly().GetName().Name + ".SomeFolder.SomeSubfolder.LoremIpsum.txt");

    private static string ReadResource(string resourceName){
        using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
          using (StreamReader reader = new StreamReader(stream))
          {
              string result = reader.ReadToEnd();
              return result;
          }
    }
}

Now, whenever you flag a new project file as an EmbeddedResource, this T4 file regenerates and creates a static property to easily consume like this...

 Console.WriteLine(EmbeddedResourceStream.LoremIpsum);

You can get my example code from GitHub

Happy hacking!!!