blog post image
Andrew Lock avatar

Andrew Lock

~5 min read

Disabling localized satellite assemblies during dotnet publish

Share on:

In this brief post I show how to reduce the size of the publish output for .NET apps by preventing the publishing of localization resources when they're not needed.

What are localization satellite assemblies?

Localization is an important aspect for many applications, and refers to the process of adapting an application such that it uses the paradigms and aspects of the target culture. This is a complex topic in general, and is more than just changing the language; it also involves changing the currencies, number rules, and other culture-specific aspects.

The functionality required to work with a given culture is encapsulated in "satellite" resource assemblies. These assemblies are named based on the parent assembly, with an added .resources suffix, and placed in a folder corresponding to the appropriate culture.

This document describes the algorithm for how satellite resource assemblies are loaded in .NET Core.

You can see this pattern if you look inside the System.CommandLine package for example. The parent assembly System.CommandLine.dll is in the root folder, and there are System.CommandLine.resources.dll in each of the "culture" folders:

The folder structure for System.CommandLine

When you publish a project that references System.CommandLine, these resource assembly folders are also copied to the output folder. This can significantly increase the final output size of the app. The resource assemblies for System.CommandLine for example add up to ~260KB while the System.CommandLine.dll file is only ~205KB. Obviously not all libraries contain satellite assemblies, but if they did, you might see your published application size doubling.

Do you need localized resources?

Obviously those localization assemblies are there for a reason. They contain localized versions of error messages for example. The question is whether or not these assemblies are important to you. Localizing your applications can provide a better user experience, and may be explicitly required for some applications. But the process of localization can also be difficult and somewhat expensive.

In some cases, particularly when you're building a web app, you may know that your application will only ever run under a single culture, which may even be the invariant culture.

If that's the case, then you know that those additional culture dlls will never be used, but they'll still be copied to the publish output, and increase the overall size of your published application. It would be nice to be able to exclude these dlls.

Using globalization invariant mode

I initially thought I might be able to exclude these satellite assemblies by explicitly enabling globalization invariant mode. Globalization invariant mode is an opt-in feature in .NET Core that ensures that all culture behave like the invariant culture. This removes the typical dependencies and inconsistencies introduced across different operating systems. It's worth reading the docs before enabling this mode to understand all the differences it introduces.

There are several ways to enable globalization invariant mode, including setting an MSBuild property, either in your project or at the command line when you publish your app. I added the switch to my project as follows:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <!-- Added this 👇 -->
    <InvariantGlobalization>true</InvariantGlobalization>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
  </ItemGroup>

</Project>

Unfortunately this has no impact on the publish output 😅 That's not entirely surprising given that this mode is a runtime feature: you can enable this mode with an environment variable or with a runtimeconfig.json switch as described here.

Fundamentally, globalization invariant mode makes all cultures behave like the invariant culture, but it doesn't affect anything at build or publish time as far as I can tell.

Disabling copying of satellite assemblies during publish

Finally, we come to the solution. The .NET Core 2.1 SDK added support for a new MSBuild property, <SatelliteResourceLanguages>. You can set this property to a semi-colon separated list of satellite resource assembly cultures that should be copied to the build and publish output directories.

For example, perhaps you know that you will always be running with the es culture. If so, you can add the <SatelliteResourceLanguages> element to your .csproj file like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <!-- Add this 👇 -->
    <SatelliteResourceLanguages>es</SatelliteResourceLanguages>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
  </ItemGroup>

</Project>

Now when you build or publish the project, only the es folder and its satellite assembly is copied to the build output:

Only es has been copied to the build output

If you want multiple cultures, separate them with a ;, for example to include both es and fr:

<SatelliteResourceLanguages>es;fr</SatelliteResourceLanguages>

which gives:

Only es and fr have been copied to the build output

If you don't want to copy any of the satellite assemblies, you can use en.

<SatelliteResourceLanguages>en</SatelliteResourceLanguages>

and sure enough:

None of the assemblies have been copied to the build output

Caveats and troubleshooting

For the most part, this feature works as you would expect, and can potentially significantly reduce the size of the published output for scenarios where you know you're not going to need the satellite assemblies. I only ran into a couple of things to watch out for:

  1. You should add the <SatelliteResourceLanguages> to the project that references the NuGet package which contains the resources.
  2. You must specify the cultures to keep exactly

On the first point, imagine you have a class library which references the System.CommandLine package. This library is then referenced by an ASP.NET Core project. You then publish and deploy the web project, and you don't want to deploy all the satellite assemblies.

You might think that you need to add <SatelliteResourceLanguages> to the ASP.NET Core project, but you actually need to add it to the library project, as that's the project that references the System.CommandLine package.

This requirements means that you need to keep a close on which packages have satellite assemblies, and which projects are referencing these projects. Alternatively, you can add the <SatelliteResourceLanguages> element to all projects to be on the safe side. One obvious way to do that is using a Directory.Build.props.

<Project>
  <PropertyGroup>
    <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
  </PropertyGroup>
</Project>

The other thing to be aware of is that you must specify the culture exactly. en will ensure there's no satellite assemblies, but otherwise you need to specify the culture the same as the folder in the build output. To use the System.CommandLine example again, you could use es, de, or pt-BR, for example, but you can't use pt or zh, as they don't appear exactly in this form:

The folder structure for System.CommandLine

If you specify a culture that isn't in the output, no error is generated, but no satellite assemblies will be copied.

That's about all there is to it. Hopefully you find this tip useful at some point!

Summary

In this post I showed how you can avoid copying localization satellite assemblies to the build output by adding the <SatelliteResourceLanguages> property to a project. You can use this property to ensure only specific cultures are copied to the build output, or you can specify en to ensure no satellite assemblies are copied.

  • Buy Me A Coffee
  • Donate with PayPal
Andrew Lock | .Net Escapades
Want an email when
there's new posts?