消除附属程序集
消除附属程序集
附属程序集(又叫卫星程序集)是用于存储应用程序中特定语言或区域性资源的程序集,通常包含翻译后的字符串、图像和其他本地化内容。它们使得应用程序可以轻松支持多语言界面,而无需更改主程序代码。在 .NET 等框架中,附属程序集常按语言文件夹(如 en-US
、zh-CN
)组织,与主程序集分离,便于独立更新和维护。
而在一些时候,我们需要只提供一个exe,还能提供多语言的支持(比如安装程序),这种时候就需要消除附属程序集了。
重要
我没有经过更多的测试,我只能确保在我的项目中这种方式是可行的,欢迎讨论。
- 使用 SDK-Style 项目
- 面向 .NET Framework 4.8
global.json
中的 SDK 版本是 9.0.300
在 SDK-Style 项目中, *.resx
文件会自动被添加到 EmbeddedResource
项。
而,以 Resource.resx
为例,Resource.*.resx
会被识别成特定文化(Culture)的资源文件,从而生成特定文化的附属程序集。
如存在 Resource.zh-Hans.resx
,那么生成 zh-Hans/***.resources.dll
。
通过观察 msbuild
日志,可以注意到[1]一个特殊的项元数据——WithCulture
,在包含特定文化的时候,此值为True
根据其名称推测,WithCulture
的作用是生成特定文化的资源文件。
步骤
既然如此,那么,我们只需要将值为 Resource.zh-Hans.resx
的 EmbeddedResource
项的元数据 WithCulture
设置为 False
即可消除附属程序集,使其资源打包进 exe
中。
<EmbeddedResource Update="Resource.*.resx" WithCulture="false" />
但此时还有一个问题,通过 ResXFileCodeGenerator
(或 PublicResXFileCodeGenerator
)生成出来的代码,默认使用的 ResourceManager
类是不会加载在主程序集中的特定文化的资源的。
于是,我开始在 Nuget.Org 上翻找着合适的源生成器,找到了 Microsoft.CodeAnalysis.ResxSourceGenerator
[1:1]。
通过观察[1:2],它生成的结果是带 partial 修饰的!
这给了我一个机会,抢在初始化 ResourceManager
之前塞自己的逻辑进去的机会。
通过编写一个自定义的 ResourceManager
实现加载主程序集中的资源
internal sealed class SingleFileResourceManager : ResourceManager
{
private readonly HashSet<CultureInfo> _cultures;
public SingleFileResourceManager()
{
_cultures = Init();
}
public SingleFileResourceManager(Type resourceSource) : base(resourceSource)
{
_cultures = Init();
}
public SingleFileResourceManager(string baseName, Assembly assembly) : base(baseName, assembly)
{
_cultures = Init();
}
public SingleFileResourceManager(string baseName, Assembly assembly, Type usingResourceSet) : base(baseName, assembly, usingResourceSet)
{
_cultures = Init();
}
private HashSet<CultureInfo> Init()
{
return [.. MainAssembly
.GetManifestResourceNames()
.Where(i => i.StartsWith(BaseName) && i.EndsWith(".resources"))
.Select(i =>
{
var name = i.Substring(BaseName.Length , i.Length - BaseName.Length - ".resources".Length );
if (string.IsNullOrEmpty(name))
return CultureInfo.InvariantCulture;
name = name.Substring(1);
return CultureInfo.GetCultureInfo(name);
})];
}
protected override ResourceSet InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents)
{
Dictionary<CultureInfo, int> levels = new(_cultures.Count);
foreach (var item in _cultures)
{
var current = culture;
int level = 0;
while (item.Name != current.Name)
{
level++;
current = current.Parent;
if (current.Name == CultureInfo.InvariantCulture.Name)
{
level = int.MaxValue;
break;
}
}
levels[item] = level;
}
int v = int.MaxValue;
foreach (var item in levels)
{
if (item.Value <= v)
{
v = item.Value;
culture = item.Key;
}
}
var name = GetResourceFileName(culture);
var stream = MainAssembly.GetManifestResourceStream(name);
return new(stream);
}
}
但此时还是不够,这个源生成器会为 Resource.zh-Hans.resx
也生成一个代码,除非它的WithCulture
元数据被设置为 True
。
或者 GenerateSource
元数据的值为 False
[1:3]!
通过翻阅源代码得知 GenerateSource
也会影响源生成器是否生成代码。
于是 最终的项目文件变成了这样
<ItemGroup>
<EmbeddedResource Update="Resources\Resource.resx" WithCulture="false" EmitFormatMethods="true" Public="true" GenerateSource="true"/>
<EmbeddedResource Update="Resources\Resource.*.resx" WithCulture="false" GenerateSource="false"/>
<PackageReference Include="Microsoft.CodeAnalysis.ResxSourceGenerator" Version="5.0.0-1.25277.114">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>