Photo by Kurt Cotoaga on Unsplash

Can’t load embedded resources with culture name suffix in .Net Core.

Xavier Solau
YounitedTech
Published in
7 min readOct 24, 2023

--

Have you ever used embedded resources in your .Net Core application? Sure, this is quite convenient and it is not really complicated to load! But did you try to name your embedded resources with a culture name suffix like MyResourceFile.fr-FR.json ? Did you encounter some difficulties? Well, it’s possible because loading it is not that straightforward!

In this article I will explain how to do it! Spoiler alert, I am going to talk about satellite assembly!

What’s Embedded resources?

Basically, embedded resources are resources inserted in the assembly itself.

Photo by Marek Okon on Unsplash

Let’s take an example with a localization sample. We are going to write a program to load translated messages from a Json file matching a specified culture name parameter. All messages are identified with a message key and stored in the Json files as key-value.

For this article we will use a console project that you can create with the command:

dotnet new console -n MyEmbeddedLoadingProject

Basically the program will load the localization resources and display all key-value pairs on the console.

Once the project is created, you can use your favorite text editor to create a new Json file to store the translation messages and you can save it as global.json in the project folder (In our example MyEmbeddedLoadingProject).

Let’s say the file will look like this:

{
"HelloKey": "Hello world!"
}

Then we can define it as an embedded resource in the project file like this:

<ItemGroup>
<None Remove="global.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="global.json" />
</ItemGroup>

That’s all, when you build your project, an assembly containing the resource file is generated.

Note that internally the resources are stored in the assembly with a name prefixed by the assembly name. In our example, the embedded resource name is actually MyEmbeddedLoadingProject.global.json.

How to load it?

Well the file is now embedded but how can we load and use the resource file? This is what we are going to see.

Photo by Rhys Moult on Unsplash

First we will add a Nuget that will help to load the embedded files:
Microsoft.Extensions.FileProviders.Embedded
You can go into the project folder and use the command:

dotnet add package Microsoft.Extensions.FileProviders.Embedded

Note that you could simply use the Assembly GetManifestResourceStream methods to load embedded resources but this Nuget brings the IFileProvider abstraction that can be very convenient. In addition the EmbeddedFileProvider provides some uniformization about the way the embedded resources are named internally.

In the project, we are going to add a class Loader with a method LoadAndDisplay that will load an embedded resource file within a given assembly.

using System.Reflection;

namespace MyEmbeddedLoadingProject
{
public class Loader
{
private readonly Assembly assembly;

/// <summary>
/// Setup Loader with the given assembly.
/// </summary>
/// <param name="assembly"></param>
public Loader(Assembly assembly)
{
this.assembly = assembly;
}

/// <summary>
/// Load and display all values from the given localization
/// embedded file.
/// </summary>
/// <returns>The asynchronous task result.</returns>
public Task LoadAndDisplay(string fileName)
{
// TODO
return Task.CompletedTask;
}
}
}

To implement the method, we can create an EmbeddedFileProvider instance with the assembly as constructor parameter and get the IFileInfo from it. Once this is done we can call a method LoadAndDisplayFromFileInfo to actually load the file and to write the entries onto the console.

/// <summary>
/// Load and display all values from the given localization embedded file.
/// </summary>
/// <returns>The asynchronous task result.</returns>
public Task LoadAndDisplay(string fileName)
{
// Get the EmbeddedFileProvider instance with the assembly.
var fileProvider = new EmbeddedFileProvider(this.assembly);

// Get the file info matching the given fileName argument.
var globalFileInfo = fileProvider.GetFileInfo(fileName);

// Then we can just call the method that take the fileInfo.
return LoadAndDisplayFromFileInfo(globalFileInfo);
}

The method LoadAndDisplayFromFileInfo is just going to check if the file exists and load it as if it was a basic file.

private async Task LoadAndDisplayFromFileInfo(IFileInfo fileInfo)
{
// Check that the file exists
if (!fileInfo.Exists)
{
Console.WriteLine($"Embedded resource '{fileInfo.Name}' doesn't exist.");
return;
}

Console.WriteLine($"Loading embedded resource '{fileInfo.Name}'.");

// Get the file content stream.
using var fileStream = fileInfo.CreateReadStream();

// Load the Json data.
var entries = await JsonSerializer
.DeserializeAsync<Dictionary<string, string>>(fileStream);

// Display all localization entries.
foreach (var entry in entries)
{
Console.WriteLine($"Localization entry '{entry.Key}' is '{entry.Value}'.");
}
}

Then we can use it in the Program.cs file to load the global.json file.

var loader = new Loader(typeof(Program).Assembly);
// Load & display the global file.
await loader.LoadAndDisplay("global.json");

Now if you run the program you will see that the embedded global.json file is properly loaded and displayed on the console.

Loading embedded resource 'global.json'.
Localization entry 'HelloKey' is 'Hello world!'.

How about a culture name suffix?

Now we are going to add a global.CultureName.json file for each supported culture.

Let’s say that we want to add the French culture so we add the global.fr-FR.json file that will look like this:

{
"HelloKey": "Bonjour à tous!"
}

We also need to update the project file to add the new embedded resource:

<ItemGroup>
<None Remove="global.json" />
<None Remove="global.fr-FR.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="global.json" />
<EmbeddedResource Include="global.fr-FR.json">
<DependentUpon>global.json</DependentUpon>
</EmbeddedResource>
</ItemGroup>

Note that the DependentUpon element is optional, it is just here to tell that the French file is linked to a ‘root’ file. It allows the solution explorer to display de files as a tree.

Global.json file is displayed as the root of all culture specific files.

Then we can update the Program.cs file to load the French global.fr-FR.json file.

var loader = new Loader(typeof(Program).Assembly);
// Load & display the global file.
await loader.LoadAndDisplay("global.json");

// Load & display the French file.
await loader.LoadAndDisplay("global.fr-FR.json");

Now if you run the program you will see that the French file is not found.

Loading embedded resource 'global.json'.
Localization entry 'HelloKey' is 'Hello world!'.

Embedded resource 'global.fr-FR.json' doesn't exist.

Well it didn’t work as expected… Next we are going to see how we can deal with it!

Is the resource in a satellite assembly?

Basically the raison why our program can’t find the file in the assembly is because it is not in the assembly. It is in fact in a satellite assembly.

If we have a look in the binary output folder we can see that there is a new folder named with the culture name fr-FR in addition of the usual generated assemblies. Inside the folder we can see that a resources assembly is also generated. This is a satellite assembly that is here to prevent the .Net Core runtime to load all culture specific data if it doesn’t need it.

In our example it is named like this: MyEmbeddedLoadingProject.resources.dll.

Now we are going to adapt our code to work with those satellite assemblies. In short, the Assembly class provides a method GetSatelliteAssembly to load the given culture specific resources.

We try to get the culture name from the file if any. Depending if we got a culture name we load the satellite assembly in order to use it in the EmbeddedFileProvider.

/// <summary>
/// Load and display all values from the given localization embedded file.
/// </summary>
/// <returns>The asynchronous task result.</returns>
public Task LoadAndDisplay(string fileName)
{
// Try to get the cultureInfo from the file name to know witch
// satellite assembly needs to be loaded.
var cultureInfo = TryGetCultureInfo(fileName);

// Make sure the resource file name is defined without its culture
// name.
var baseResourceFileName = GetBaseResourceName(fileName, cultureInfo);

// Get the main assembly or the satellite assembly depending if there
// is a culture name specified.
var assemblyToLoad = cultureInfo == null
? this.assembly
: this.assembly.GetSatelliteAssembly(cultureInfo);

// Get the EmbeddedFileProvider instance with the assembly.
var fileProvider = new EmbeddedFileProvider(
assemblyToLoad,
this.assembly.GetName().Name);

// Get the file info matching the given fileName argument.
var globalFileInfo = fileProvider.GetFileInfo(baseResourceFileName);

// Then we can just call the method that take the fileInfo.
return LoadAndDisplayFromFileInfo(globalFileInfo);
}

Note that we need to give the assembly name in the EmbeddedFileProvider because it is not accessible from the satellite assembly. It is actually used internally to load embedded resources that are prefixed with it.

We add the two helper methods to get the CultureInfo and the base resource file name:

private CultureInfo? TryGetCultureInfo(string fileName)
{
// Find the CultureInfo matching the file name suffix.
var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures);

return allCultures.FirstOrDefault(
ci => fileName.EndsWith($".{ci.Name}.json"));
}

private string GetBaseResourceName(string fileName,
CultureInfo? cultureInfo)
{
// Remove the culture name from the file name.
return cultureInfo == null
? fileName
: fileName.Replace($".{cultureInfo.Name}.json", ".json");
}

We can run the application and this time all resources are successfully loaded!

Loading embedded resource 'global.json'.
Localization entry 'HelloKey' is 'Hello world!'.

Loading embedded resource 'global.json'.
Localization entry 'HelloKey' is 'Bonjour à tous!'.

A last word?

The full article code sources can be found in this repository.

And if you want to use a Json base localization solution in your dotnet project, you might by interested by this project too.

Well, I hope you found this article helpful!

Thanks for reading!

Photo by Markus Spiske on Unsplash

--

--