.NET 7 Embedded Raw Media Using ResourceCreator

Except for the most basic applications, we often need to bundle one or more resources with our programs or libraries. By resources, we are referring to data such as strings, images, and even sound or video media content.

These resources are typically attached to a compiled execution image; they are embedded in the binary, if you will. Including these resources in your .NET project’s output is normally a very simple affair. This is because, for the most part, Visual Studio‘s managed resources editor does the job with no incident.

That job being: the visual creation of a .resx file, which allows for the attachment to the executable of resources that are either linked at compile time or embedded in the resources file.

Unfortunately, embedding media content in a resources file results in runtime errors.

This article seeks to demonstrate some outstanding bugs with Visual Studio‘s resource editor, and how I got around them with a little dotnet tool I made.

Link or Die: How Embedding Fails

It always gives me a sense of relief (and disbelief) when, after finding a significant bug and developing a workaround for it, said bug still exists over a year later when I get around to writing an article on it.

When adding resources to our .resx file via Visual Studio‘s managed resources editor, by default, each resource is configured to be linked at compile time.

Let’s say we want our application to play an audio file. Using the managed resources editor, we’ll see that adding a particular .wav file to our resources file will also add it to our project so that it’s linked at compile time.

Shows how a resource linked at compile time occupies a place in our project's structure.
Our new audio resource: “MurderedVincentLaugh.wav”.

The strangely named audio file shown above is something I used in my Vision application. Let’s just leave it at that.

If we peer inside the Resources.resx file, we’ll see a reference to our audio file via a relative path.

Entry for Audio File in Resources.resx
<data name="MurderedVincentLaugh" type="System.Resources.ResXFileRef, System.Windows.Forms">
  <value>
	  ..\Resources\MurderedVincentLaugh.wav;System.IO.MemoryStream, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  </value>
</data>

Get Out of My Project! Let’s Directly Embed It

You may have one or more reasons for not wanting a bunch of loose resource assets present in your project’s structure. Luckily for you, we can open up the .resx file via the managed resources editor, click on the resource, and then adjust its manner of persistence via the Properties window.

Shows how we can change how a resource is persisted to our resources file.
Some types of resources allow for direct embedding into a resources file, configurable via the Properties window.

By selecting the option to embed the resource in our .resx file, the resource no longer needs to be part of our project, as it now exists in the resources file.

Partial Entry for Embedded Audio File in Resources.resx
<data name="MurderedVincentLaugh" mimetype="application/x-microsoft.net.object.binary.base64">
  <value>
      AAEAAAD/////AQAAAAAAAAAEAQAAABZTeXN0ZW0uSU8uTWVtb3J5U3RyZWFtCgAAAAdfYnVmZmVyB19v
      cmlnaW4JX3Bvc2l0aW9uB19sZW5ndGgJX2NhcGFjaXR5C19leHBhbmRhYmxlCV93cml0YWJsZQpfZXhw
      b3NhYmxlB19pc09wZW4dTWFyc2hhbEJ5UmVmT2JqZWN0K19faWRlbnRpdHkHAAAAAAAAAAACAggICAgB
      AQEBCQIAAAAAAAAAAAAAACBTHwAgUx8AAAEAAQoPAgAAACBTHwACUklGRhhTHwBXQVZFZm10IBAAAAAB
      AAIAgLsAAADuAgAEABAATElTVBoAAABJTkZPSVNGVA4AAABMYXZmNTguMjkuMTAwAGRhdGEgPR8AAAAA
      AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
      <!--And on and on and on...-->
  </value>
</data>

Neat. Functionally, the resource asset behaves the same in our code whether it is linked at compile time or embedded in the .resx file ahead of time. Namely, accessing Resources.MurderedVincentLaugh will return an UnmanagedMemoryStream, with which we can feed the audio data through some playback mechanism.

But It Won’t Work!

It would’ve worked just fine if we left our resource configured to be linked at compile time, but changing it to be directly embedded in the .resx file will result in an exception thrown when accessing the resource.

System.Runtime.Serialization.SerializationException: 'Type 'System.IO.MemoryStream' in Assembly 'System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e' is not marked as serializable.'
Ouch. Something isn’t getting baked into our executable image correctly.

Functionally identical to using a resource that was linked at compile time; unfortunately, choosing to embed the resource directly in the .resx file has left us with a dud.

Why Doesn’t It Work?

Knowing the exact cause isn’t all that important here. Clearly, there is a bug happening with Visual Studio‘s managed resources editor when embedding a resource into a .resx file — a bug that persists even with .NET 7 and the newest version of Visual Studio 2022.

For some reason, perhaps due to how the managed resources editor baked in the resource, it’s actually trying to directly deserialize the resource as a MemoryStream. You normally deserialize data into a Stream, not as a Stream.

We’re going to say “goodbye” to the managed resource editor and take matters into our own hands in order to embed this resource into our assembly.

Embedding Resources With ResourceCreator

It would be much simpler if we could embed the raw byte data of our media into our assembly. At runtime, we would then take the bytes and pipe that data through a Stream.

We don’t (to my knowledge) have that level of control when working with .resx files, so let’s skip them altogether and jump straight to working with .resources files, which are what .resx files compile into anyway.

To generate a .resources file, you’ll have to write some code — or, you could make use of a dotnet tool I created.

Installing ResourceCreator
dotnet tool install BadEcho.ResourceCreator --global

Whether or not you want it installed globally is up to you, of course. The tool is published on NuGet, so the above command should simply work.

The tool can be invoked with the command resource-creator:

C:\src\resources>resource-creator --help
Description:
  Create a .resources file from a directory containing files we wish to bundle as resources for a program.

Usage:
  resource-creator <RESOURCES_PATH> [options]

Arguments:
  <RESOURCES_PATH>  The path to the directory containing the resources.

Options:
  -o, --output <OUTPUT_PATH>     The path to the output resources file. [default: out.resources]
  -f, --filter <SEARCH_PATTERN>  A filter comprising one or more search patterns matched against the names of 
                                 files to be included in the output resources file. Each pattern can contain a 
                                 combination valid literal and wildcard characters, with each pattern separated 
                                 by a comma. [default:*.*]
  --version                      Show version information
  -?, -h, --help                 Show help and usage information

Hopefully the usage is self-explanatory. We simply take all the media resources we want to embed, throw them into a directory, and then run our tool. Something like the following:

resource-creator C:\src\resources -o Sounds.resources

This will generate a Sounds.resources file which we can then attach to our project, with its Build Action set to Embedded resource.

Creating the Accessor Class

We got our baked-in resources attached to the project. We now need a nice way of accessing them.

What I do is create a class with the same name as the .resources file; so, in this case: Sounds.cs.

In this class, we want to initialize a ResourceManager for our .resources file, provide a property that returns the audio resource in question, and finally include a general helper method that we’ll be calling into for this and any other resource we have bundled into the particular .resources file.

Sounds.cs
public static class Sounds
{
    private static readonly ResourceManager _Manager = new(
        "BadEcho.Embedding.Stuff.Properties.Sounds",
        typeof(Sounds).Assembly);

    /// <summary>
    /// Gets a stream for the rather pleasant sound effect of Vincent Price laughing at us for
    /// being murdered.
    /// </summary>
    public static Stream MurderedVincentLaugh
        => GetStream(nameof(MurderedVincentLaugh));

    private static Stream GetStream(string name)
    {
        UnmanagedMemoryStream? stream = _Manager.GetStream(name, CultureInfo.InvariantCulture);

        if (stream == null)
            throw new BadImageFormatException(Strings.SoundMissingResource.InvariantFormat(name));

        return stream;
    }
}

Adding another sound file is as simple as adding another property (after generating a new .resources file).

Voilà! We have working fully-embedded media resources. Enjoy.