The problem

We want to read a JSON file from disk in our application. We don't want to rely on absolute file paths, hosting environments, copying the files to the output directory etc.

The JSON file looks like this:
my-json-file.json

{
  "data": true
}

We will use a feature called EmbeddedResource. When building the project, the json file will be included in the dll.

To mark a file as an EmbeddedResource, you just add it to your .csproj like this:

<ItemGroup>
    <EmbeddedResource Include="Data\data.json" />
    <EmbeddedResource Include="my-json-file.json" />
</ItemGroup>

That's all there is to it, now, let's look at how we can read this file in our application.

Implementations

We will create the following interface to make it easier for us to test different implementations.

public interface IEmbeddedResourceQuery
{
    Stream? Read<T>(string resource);
    Stream? Read(Assembly assembly, string resource);
    Stream? Read(string assemblyName, string resource);
}

When working with EmbeddedResources you are always going to need an Assembly, it's in the Assembly you will look for the embedded resources.

It will be possible to read embedded resources in three different ways with our interface:

var stream = embeddedResourceQuery.Read<SomeTypeInTheAssembly>("my-json-file.json");
var assembly = Assembly.Load("JOS.MyLibrary);
var stream = embeddedResourceQuery.Read(assembly, "my-json-file.json");
var stream = embeddedResourceQuery.Read("JOS.MyLibrary", "my-json-file.json");

EmbeddedFileProvider

Our first implementation uses the EmbeddedFileProvider which is a nuget package from Microsoft.

The important bits are in the ReadInternal method where we fetch an instance instance of the EmbeddedFileProvider (or create a new one if it hasn't been cached before), then fetches the file information and opening up a read stream. Note that we don't have any error handling here. You might wanna wrap the calls in a try/catch if using this in production.

public class EmbeddedFileProvider_EmbeddedResourceQuery : IEmbeddedResourceQuery
{
    private readonly Dictionary<Assembly, EmbeddedFileProvider> _fileProviders;

    public EmbeddedFileProvider_EmbeddedResourceQuery() : this(Array.Empty<Assembly>())
    {
    }

    public EmbeddedFileProvider_EmbeddedResourceQuery(IEnumerable<Assembly> assembliesToPreload)
    {
        _fileProviders = new Dictionary<Assembly, EmbeddedFileProvider>();
        foreach (var assembly in assembliesToPreload)
        {
            var embeddedFileProvider = new EmbeddedFileProvider(assembly);
            _fileProviders.Add(assembly, embeddedFileProvider);
        }
    }

    public Stream? Read<T>(string resource)
    {
        return ReadInternal(typeof(T).Assembly, resource);
    }

    public Stream? Read(Assembly assembly, string resource)
    {
        return ReadInternal(assembly, resource);
    }

    public Stream? Read(string assemblyName, string resource)
    {
        var assembly = Assembly.Load(assemblyName);
        return ReadInternal(assembly, resource);
    }

    internal Stream? ReadInternal(Assembly assembly, string resource)
    {
        if (!_fileProviders.ContainsKey(assembly))
        {
            _fileProviders[assembly] = new EmbeddedFileProvider(assembly);
        }
            
        return _fileProviders[assembly].GetFileInfo(resource).CreateReadStream();
    }
}

GetManifestResourceStream

This is our second implementation that doesn't use any external dependencies. The important bits here is also in the ReadInternal method where we are using the GetManifestResourceStream method on the assembly to get a stream to read from.

Another important thing here is how the path is built up to read a resource. We need to specify the assembly name (JOS.MyLibrary) and then use dot notation to create the correct path.

Example: You have a file in the folder Data. The correct resource path would be:

// Assembly name.Folder.Filename
JOS.MyLibrary.Data.my-json-file.json"
public class EmbeddedResourceQuery : IEmbeddedResourceQuery
{
    private readonly Dictionary<Assembly, string> _assemblyNames;

    public EmbeddedResourceQuery() : this(Array.Empty<Assembly>())
    {
            
    }

    public EmbeddedResourceQuery(IEnumerable<Assembly> assembliesToPreload)
    {
        _assemblyNames = new Dictionary<Assembly, string>(StringComparer.OrdinalIgnoreCase);
        foreach (var assembly in assembliesToPreload)
        {
            _assemblyNames.Add(assembly, assembly.GetName().Name!);
        }
    }

    public Stream? Read<T>(string resource)
    {
        var assembly = typeof(T).Assembly;
        return ReadInternal(assembly, resource);
    }

    public Stream? Read(Assembly assembly, string resource)
    {
        return ReadInternal(assembly, resource);
    }

    public Stream? Read(string assemblyName, string resource)
    {
        var assembly = Assembly.Load(assemblyName);
        return ReadInternal(assembly, resource);
    }

    internal Stream? ReadInternal(Assembly assembly, string resource)
    {
        if (!_assemblyNames.ContainsKey(assembly))
        {
            _assemblyNames[assembly] = assembly.GetName().Name!;
        }
        return assembly.GetManifestResourceStream($"{_assemblyNames[assembly]}.{resource}");
    }
}

Benchmarks

As you can see, our version that doesn't use the EmbeddedFileProvider is the fastest (and least allocaty) version.

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
11th Gen Intel Core i7-11370H 3.30GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.301
  [Host]   : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT
  .NET 6.0 : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT

Job=.NET 6.0  Runtime=.NET 6.0

|                                        Method |       Mean |     Error |    StdDev |     Median | Ratio | RatioSD |  Gen 0 | Allocated |
|---------------------------------------------- |-----------:|----------:|----------:|-----------:|------:|--------:|-------:|----------:|
|                 EmbeddedResourceQuery_Generic |   578.1 ns |  29.58 ns |  87.22 ns |   575.9 ns |  1.00 |    0.00 | 0.0291 |     184 B |
|                  EmbeddedFileProvider_Generic |   919.8 ns | 124.31 ns | 366.52 ns |   656.3 ns |  1.63 |    0.70 | 0.0687 |     432 B |
| EmbeddedResourceQuery_AssemblyNameAndResource | 2,552.6 ns |  49.82 ns |  61.19 ns | 2,546.8 ns |  4.73 |    1.71 | 0.0496 |     312 B |
|  EmbeddedFileProvider_AssemblyNameAndResource | 3,010.0 ns |  60.06 ns | 119.95 ns | 2,983.9 ns |  5.50 |    1.52 | 0.0877 |     560 B |
|     EmbeddedResourceQuery_AssemblyAndResource | 2,548.1 ns |  47.72 ns |  90.79 ns | 2,526.9 ns |  4.67 |    1.31 | 0.0496 |     312 B |
|      EmbeddedFileProvider_AssemblyAndResource | 3,021.5 ns |  60.03 ns |  73.72 ns | 3,022.3 ns |  5.59 |    2.00 | 0.0877 |     560 B |

Code

All code, complete with tests can be found over at GitHub