开发专栏
解决 .NET Core 在 Linux Container 中获取 CurrentCulture 不正确的问题
4 个月前
392
.NET Core Linux Docker

将项目迁移到 .NET Core 并在 Linux 容器中运行时发现 CurrentThread.CurrentCulture 的值为 Empty。本文记录解决此问题的方案。

背景

在将公司一款基于 .NET Framework 的控制台程序迁移到 .NET Core 3.1 时,发现程序中本地化的部分失效,症状类似于对 Thread.CurrentThread.CurrentCulture.Name 的值进行 Substring() 操作时抛出 ArgumentOutOfRangeException 异常。

该程序在 Windows Container 中工作良好,迁移为 .NET Core 后在我的 Windows 开发机上也运行良好,一旦部署到 K8s 的 Linux 容器中就会出现问题。容器使用的是基于微软官方的 .NET Runtime 3.1 镜像。

本文按我当时解决此问题的思路记录,从 Windows 开始,挨个环境测试 CurrentThread.CurrentCulture

TL;DR 先上结论

.NET Core Runtime 的 Linux 镜像没有设置语言信息,导致通过 CurrentThread.CurrentCulture 获取的 NameString.Empty

只需要在生成镜像时为 Linux 设置语言即可。本文内电梯直达:为容器中的 Linux 设置语言信息

在 Windows 中获取区域设置

先创建一个名为 CultureTest 的控制台项目看看效果,这里使用 .NET Core 的 LTS 版本 .NET Core 3.1 为例:

dotnet new console -o CultureTest --framework netcoreapp3.1

然后进入 CultureTest 文件夹,将生成的 Program.cs 替换为如下内容:

using System;
using System.Globalization;
using System.Linq;
using System.Threading;

namespace CultureTest
{
    class Program
    {
        static void Main()
        {
            PrintProperty(Thread.CurrentThread.CurrentCulture);

            Thread.Sleep(TimeSpan.FromDays(1));
        }

        private static void PrintProperty(CultureInfo cultureInfo)
        {
            var printableProperties = cultureInfo
                                            .GetType()
                                            .GetProperties()
                                            .Where(p => p.PropertyType.IsValueType 
                                                        || p.PropertyType == typeof(string));

            foreach (var property in printableProperties)
            {
                Console.WriteLine($"{property.Name}: {property.GetValue(cultureInfo)}");
            }
        }
    }
}
  • PrintProperty() 方法主要用途是将 CultureInfo 类中所有值类型和 string 类型的属性找到,并将我们传入的 Thread.CurrentThread.CurrentCulture 对象的这些属性值都打印出来。
  • Thread.Sleep() 是为在后面测试 docker 时用于防止程序运行后直接退出之用。

我在一台区域设置为 中文(简体,中国) 的 Windows 10 PC 上运行上述代码:

dotnet run

run_in_zh_cn_windows

可以看见 Namezh-CN 和 Windows 一致。
查看信息后,由于有 Thread.Sleep() 的逻辑,需要通过 Ctrl + C 来停止程序的运行(后面 Linux 和 Docker 中也一样)。

在 Linux 中获取区域信息

我在 WSL 中安装了 Debian 10,并安装了 .NET Core 3.1 SDK,下面用 Debian 来进行测试。

locale 命令

在 Linux 中,可以使用 locale 命令查看当前语言环境信息:

locale

locale

关注 LANG 的值,现在显示为 en_US.UTF-8

locale 命令加上 -a 选项后可以查看可用的语言环境信息:

locale -a

locale_a

可以看到这个 Debian 除了当前的 en_US.UTF-8,还支持其它几种语言环境。

通过 CurrentThread 获取

由于是 WSL,可以通过 /mnt 中挂载的 Windows 文件系统,直接导航到上一节创建的项目中,并运行:

cd /mnt/d/projects/CultureTest
dotnet run

run_in_en_us_linux

可以看见 Nameen-USlocale 命令一致。

在 Docker 容器中获取区域信息

发布测试程序

先发布 CultureTest 项目:

dotnet publish -c Release

默认会发布到 .\bin\Release\netcoreapp3.1\publish\ 文件夹下,可以使用 dir(Windows) 或 ls(Linux) 命令查看发布结果。

publish

创建 Dockerfile

接下来为 CultureTest 生成镜像。

首先在 CultureInfo 项目根目录(.csproj 所在的目录)下创建 Dockerfile 并填入以下内容:

FROM mcr.microsoft.com/dotnet/runtime:3.1

COPY ./bin/Release/netcoreapp3.1/publish/ /app/
WORKDIR /app
ENTRYPOINT ["dotnet", "CultureTest.dll"]
  • 这里使用 .NET Core 官方提供的 .NET Runtime 镜像 mcr.microsoft.com/dotnet/runtime:3.1 作为 Runtime
  • 拷贝刚刚发布到 .\bin\Release\netcoreapp3.1\publish\ 的程序到容器的 /app 文件夹下
  • 将容器的工作目录设为 /app 文件夹
  • 通过 dotnet CultureTest.dll 命令运行测试项目

生成镜像

Dockerfile 所在的目录下执行 docker build 命令生成镜像:

docker build -t culture-test .
  • -t culture-test 是设置镜像的名称为 culture-test
  • 不要漏掉最后的 .

docker_build

运行并查看结果

通过上一步创建的 culture-test 镜像生成一个容器,并查看执行结果:

docker run culture-test

run_in_default_docker

发现 Name 后没有任何内容。

经过测试,CurrentThread.CurrentCulture 不会为 null,并且 Name 属性的值为 String.Empty 而非 null。这也是我遇到问题的原因,对 String.Empty 进行了 Substring() 操作,所以抛出了 ArgumentOutOfRangeException 异常,问题重现。

在容器中执行 locale

进入容器,查看 Linux 的语言环境信息:

docker run -d culture-test
docker exec -it [your_container_id] /bin/bash
  • 通过 -d 让程序后台运行(有 Thread.Sleep() 在,所以程序不会退出,这样我们就能进入到容器内执行命令),这一步执行后会返回容器 id
  • 通过 exec 执行容器里的 /bin/bash

locale_in_docker

发现 locale 命令返回的 LANG 也是空白的。

并且 locale -a 命令返回的 CPOSIX 都是默认不含语言的环境。

原因

如果使用过 Linux 的 GUI 安装 Linux,一般会让选择语言和地区,但 mcr.microsoft.com/dotnet/runtime:3.1 以及它基于的 Debian 镜像,都没有设置语言,所以导致我们通过 locale 或是 C# 的 CurrentThread.CurrentCulture 获取到的都是空白的内容。

那么结合上面的信息,要想让依赖于区域语言信息的程序不报错,有两种方案:

  • 修改程序,增加对 CurrentCulture.Name 的判断:如果 CurrentCulture.Name == String.Empty,则为程序设置一个默认 Culture
  • 修改运行环境,将默认语言信息设置为需要的值(例如 en-US

为容器中的 Linux 设置语言信息

虽然最后我选择的是修改程序,但也来了解一下这种情况如何为容器中的 Linux 设置语言信息吧。

通过搜索,找到了 StackOverflow 上的提问:How to set the locale inside a Debian/Ubuntu Docker container?,并从中得到了解决方案。

通过安装 locales-all 包

通过安装 localeslocales-all 包,可以把所有支持的语言信息都安装到系统中,再通过环境变量设置需要的语言。

修改 Dockerfile

FROM mcr.microsoft.com/dotnet/runtime:3.1

# 安装所有支持的语言信息,并设置 en_US.UTF-8 为当前语言
RUN apt-get update
RUN apt-get install -y locales locales-all
ENV LANG en_US.UTF-8

COPY ./bin/Release/netcoreapp3.1/publish/ /app/
WORKDIR /app
ENTRYPOINT ["dotnet", "CultureTest.dll"]

docker_all

通过安装 locales 包,并修改 locale.gen 文件

修改 Dockerfile

FROM mcr.microsoft.com/dotnet/runtime:3.1

# 安装 locales 包,并修改 locale.gen 文件,再设置语言
RUN apt-get update
RUN apt-get install -y locales
RUN sed -i -e '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
ENV LANG en_US.UTF-8  

COPY ./bin/Release/netcoreapp3.1/publish/ /app/
WORKDIR /app
ENTRYPOINT ["dotnet", "CultureTest.dll"]

安装 locales 后,会生成 /etc/locale.gen 文件,文件内容类似于:

# en_SG ISO-8859-1
# en_SG.UTF-8 UTF-8
# en_US ISO-8859-1
# en_US.ISO-8859-15 ISO-8859-15
# en_US.UTF-8 UTF-8
# en_ZA ISO-8859-1
# en_ZA.UTF-8 UTF-8
# en_ZM UTF-8
# en_ZW ISO-8859-1
# en_ZW.UTF-8 UTF-8

通过 sed 命令:

  • /en_US.UTF-8:将包含 en_US.UTF-8 字样的行
  • /s:执行替换
  • /^#:将行首的 #
  • /:替换为空白

然后执行 locale-gen 命令并设置 LANG 的值为 en_US.UTF-8

docker_need

这两种方式都能保证 CurrentThread.CurrentCulture 获取到正确的 Culture Name。