While code is inarguably the bedrock of any software application, it’s not the only thing necessary to deliver a user experience. Whether you’re building a website, desktop application, or mobile app, you’ll likely need non-code assets. These assets include images, videos, third-party file formats, and more. Additionally, you should include localization values to support a variety of languages and grow your user base.

This post will explore embedded resources in .NET and point to some material I’ve written for JetBrains about localizing your ASP.NET applications.

Embedded Resources

An embedded resource is any file you want to include in your final built assembly. This can include anything from text files to binary formats like images or videos. Embedded resources use the EmbeddedResource MSBuild element, which the compiler reads to transform project assets into binary embedded in your assemblies. Here are a few examples.

<ItemGroup>  
  <EmbeddedResource Include="Embedded\test.txt" />  
  <None Remove="Embedded\person.json" />  
  <EmbeddedResource Include="Embedded\person.json" />  
  <EmbeddedResource Update="Embedded\Values.resx">  
    <Generator>ResXFileCodeGenerator</Generator>  
    <LastGenOutput>Values.Designer.cs</LastGenOutput>  
  </EmbeddedResource>
</ItemGroup>

Once an asset is embedded, it is given a unique name in the resource manifest, which is typically similar to its file path but is overridable.

Remember, assets are stored within the Assembly, which means you must know which assembly has your resources to get the names from the manifest. Here is an example.

var names =   
	System  
	.Reflection  
	.Assembly  
	.GetExecutingAssembly()  
	.GetManifestResourceNames();  
  
foreach (var name in names)  
{  
    Console.WriteLine(name);  
}

Combined with our ItemGroup from above, we’d get the following output.

BedTime.Embedded.Values.resources
BedTime.Embedded.test.txt
BedTime.Embedded.person.json

You may have also noticed the use of ResXFileCodeGenerator. The Resx file format is a unique format used by .NET applications to store mostly string values, but it can also be adapted to store binary formats like images in base64 strings. I don’t recommend it, but it’s possible.

The .resx format is best suited for localization, which I wrote about previously for JetBrains here. Check it out. It also generates a C# class for accessing values for straightforward usage. The format is also used by many of the features in .NET, including ASP.NET Core.

Accessing Embedded Resources from C#

I recommend folks access embedded resources by writing static wrapper classes that formalize the process of accessing assembly artifacts. let’s look at how we might access one of the files from the previous section, and then we’ll create the wrapper class for all resources.

var info = Assembly.GetExecutingAssembly().GetName();  
var name = info.Name;  
using var stream = Assembly  
    .GetExecutingAssembly()  
    .GetManifestResourceStream($"{name}.Embedded.test.txt")!;  
using var streamReader = new StreamReader(stream, Encoding.UTF8);  
return streamReader.ReadToEnd();

Resource names will typically follow this convention (Assembly Name).(Folders).(Filename) unless an explicit name exists. You can also use the GetManifestResourceNames method to find a resource name if you can’t figure it out by convention.

Now, let’s wrap all our resources in a static class.

using System.Reflection;
using System.Text;
using System.Text.Json;

namespace BedTime.Embedded;

public static class Resources
{
    public static class Embedded
    {
        public static string TestTxt
        {
            get
            {
                var info = Assembly.GetExecutingAssembly().GetName();
                var name = info.Name;
                using var stream = Assembly
                    .GetExecutingAssembly()
                    .GetManifestResourceStream($"{name}.Embedded.test.txt")!;
                using var streamReader = new StreamReader(stream, Encoding.UTF8);
                return streamReader.ReadToEnd();
            }
        }

        public static Person Person
        {
            get
            {
                var info = Assembly.GetExecutingAssembly().GetName();
                var name = info.Name;
                using var stream = Assembly
                    .GetExecutingAssembly()
                    .GetManifestResourceStream($"{name}.Embedded.person.json")!;
                return JsonSerializer.Deserialize<Person>(stream)!;
            }
        }
    }
}

public record Person(string Name, string[] Hobbies)
{
    public override string ToString()
    {
        return $"{Name} likes {Hobbies.ToOxfordComma()}";
    }
};

public static class EnumerableExtensions
{
    public static string ToOxfordComma(this string[]? items)
    {
        var result = items?.Length switch
        {
            // three or more items
            >=3 => $"{string.Join(", ", items[..^1])}, and {items[^1]}",
            // 1 item or 2 items
            not null => string.Join(" and ", items),
            // null
            _ => string.Empty
        };

        return result;
    }
}

You can see why I recommend this route. You have more control over how you read values from the assembly and how to transform them into something useful. In the case of Person, it’s a JSON file that we ultimately want to turn into an instance of a class. It’s also much nicer to use in your code.

Console.WriteLine(Resources.Embedded.TestTxt);  
Console.WriteLine(Resources.Embedded.Person);

You can also add caching of elements to reduce resource-intensive actions like serialization and deserialization.

Conclusion

Embedded resources are a nice feature of the .NET programming stack and can be used to deliver application-critical assets bundled alongside your code. This reduces the need to read files from disk or deal with network calls to retrieve assets. This will increase the size of your assemblies, so use this approach sparingly and with caution. I also recommend hand-writing your embedded resource access to give you more granular control over the final result and to reduce conversion noise in your more critical code.

I hope you found this post helpful, and thanks, as always, for reading.