Every application needs more than compiled code to work properly. Templates, configuration defaults, sample data files, small images, and localization strings all have to travel with your app somehow. One option that a lot of .NET developers overlook is embedding these files directly into the compiled assembly. Once you know how the manifest, the naming convention, and the read APIs fit together, embedded resources become a genuinely useful tool for building self-contained tools and libraries.
This article walks through how embedded resources work in .NET, how to read them back out at runtime, and a pattern for wrapping that access in a clean, static class instead of scattering reflection calls across your codebase.
What an embedded resource actually is
An embedded resource is any file that gets compiled into your assembly’s binary rather than shipped alongside it on disk. That can be a text file, a JSON document, an image, or even a resx file used for localization. MSBuild controls this through the EmbeddedResource item, and the compiler reads that item to fold the file’s bytes into the assembly during build.
Here is a typical csproj snippet that embeds a couple of files and hooks up a resx file for code generation.
<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>
The first two lines embed test.txt directly. The person.json entry needs a None Remove line first because MSBuild’s default glob already picks up json files as content, and you do not want it included twice under two different build actions. The Values.resx entry is different: it is not just embedded, it also runs through ResXFileCodeGenerator, which produces a matching Designer.cs file with strongly-typed accessors. Once this builds, each file gets a unique name inside what is called the resource manifest, and that name is usually close to the file path, though you can override it.
Finding what got embedded, from the manifest
Because embedded resources live inside a specific Assembly object, you need to know which assembly to ask before you can read anything back. The Assembly class exposes GetManifestResourceNames for exactly this, which is handy when you are not sure what the generated name for a file ended up being.
var names = System.Reflection.Assembly
.GetExecutingAssembly()
.GetManifestResourceNames();
foreach (var name in names)
{
Console.WriteLine(name);
}
Running this against the ItemGroup above, in a project named BedTime, prints out three names.
BedTime.Embedded.Values.resources
BedTime.Embedded.test.txt
BedTime.Embedded.person.json
Notice the pattern: assembly name, then folder path, then file name, each joined with a dot. This convention holds as long as you do not explicitly override the resource name in the project file. When something does not read correctly, this is usually the first place to look, because a mismatched name is the most common reason GetManifestResourceStream silently returns null.
A quick note on the resx format
You will have noticed ResXFileCodeGenerator in the csproj snippet above. Resx is .NET’s own XML based resource format, and it is best known for string localization, where it pairs naturally with satellite assemblies and culture fallback. It is technically possible to store binary data such as images in a resx file as base64 text, and the generated Designer.cs class will happily expose it as a byte array. I would not recommend that route though. Base64 inflates the file size by roughly a third, and you lose the ability to stream the content, which matters once a resource gets larger than a few kilobytes. Stick to resx for strings and use plain EmbeddedResource entries for everything else.
Reading a resource back out at runtime
Reading an embedded resource means opening a stream against the manifest name and copying that stream into whatever shape your code needs, a string, a byte array, or a deserialized object. Here is the minimal version for a plain text file.
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();
This builds the resource name from the assembly name plus the folder and file name, then opens a stream and reads it as UTF8 text. The null-forgiving operator after GetManifestResourceStream is a bit optimistic. If the name is wrong, that call returns null instead of throwing, and the using statement will throw a NullReferenceException a line later with a much less helpful message. It is worth adding an explicit null check with a clear exception message in production code, especially if the resource name is built dynamically rather than as a literal string.
Wrapping resource access in a static class
Scattering GetManifestResourceStream calls with string-built names across a codebase gets messy fast, and it is easy to typo a resource name since the compiler cannot check it for you. A cleaner pattern is a static wrapper class that exposes each embedded resource as a typed property, so callers never touch reflection directly.
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);
Each property hides the reflection call and the resource name behind a normal C# member. TestTxt returns a plain string, and Person deserializes the embedded JSON straight into a record using System.Text.Json. Calling code no longer needs to know the manifest naming convention at all.
Console.WriteLine(Resources.Embedded.TestTxt);
Console.WriteLine(Resources.Embedded.Person);
The first line prints the raw contents of test.txt. The second prints whatever ToString produces for the Person record, which in the original sample formats the person’s hobbies as a grammatically correct list. If you add a custom ToString override on a record like this, keep it simple, since it is easy to end up with formatting logic that belongs in a view layer instead. One thing worth adding on top of this pattern is caching: every call to these properties re-opens the stream and re-parses the JSON, which is wasteful if the resource is read often. A static readonly field or Lazy<T> around the deserialized value fixes that cheaply, since embedded resources never change at runtime.
Trade-offs and when this approach makes sense
Embedding files grows your assembly, and that is the main cost. A single large image or dataset can noticeably increase the size of your DLL or self-contained executable, and unlike loose files on disk, you cannot swap an embedded resource without rebuilding and redeploying the whole assembly. For content that changes independently of your code, such as configuration that ops teams tune per environment, a regular file, environment variable, or configuration provider is almost always the better choice.
Where embedded resources genuinely help is when the asset is small, fixed, and needs to travel with the code no matter what: default templates, seed data for a CLI tool, icons for a desktop app, or the resx-based localization strings this technique is most commonly used for. Single-file deployment and NuGet packages benefit especially, since there is no separate folder of assets that a consumer could accidentally delete or forget to copy.
One thing to watch for going forward is trimming and Native AOT. Since embedded resource access here goes through reflection based APIs, the trimmer can sometimes remove metadata it thinks is unused, and GetManifestResourceStream calls built from dynamic strings are harder for the trimmer to reason about than calls with literal names. If you are publishing trimmed or Native AOT builds, test resource access explicitly after publishing rather than assuming it behaves the same as a regular build, and prefer literal resource name strings over ones assembled at runtime wherever you can.
Leave a Reply