背景


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

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

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


解决 .NET Core 在 Linux Container 中获取 CurrentCulture_javaTL;DR 先上结论


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

只需要在生成镜像时为 Linux 设置语言即可,本文末尾附有解决方案。


解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java在 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

解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java_03

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


解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java在 Linux 中获取区域信息


我在 WSL(https://docs.microsoft.com/windows/wsl/install-win10) 中安装了 Debian 10,并安装了 .NET Core 3.1 SDK,下面用 Debian 来进行测试。


解决 .NET Core 在 Linux Container 中获取 CurrentCulture_javalocale 命令


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

locale

解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java_06

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

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

locale -a

解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java_07

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


解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java通过 CurrentThread 获取


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

cd /mnt/d/projects/CultureTest
dotnet run

解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java_09

可以看见 Name 为 en-US 和 locale 命令得到的一致。


解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java在 Docker 容器中获取区域信息


下面将测试程序放入容器中运行。



解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java发布测试程序



先发布 CultureTest 项目:

dotnet publish -c Release

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

解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java_12


解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java创建 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 命令运行测试项目


解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java生成镜像


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

docker build -t culture-test .
  • -t culture-test 设置镜像的名称为 culture-test

  • 不要漏掉最后的 .

解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java_15


解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java运行并查看结果


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

docker run culture-test

解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java_17

发现 Name 后没有任何内容。

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


解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java在容器中执行 locale


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

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

  • 通过 exec 执行容器里的 /bin/bash

解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java_19

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

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


解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java原因


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

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

  • 修改程序,增加对 CurrentCulture.Name 的判断:如果 CurrentCulture.Name == String.Empty,则为程序设置一个默认 Culture

  • 修改运行环境,将默认语言信息设置为需要的值(例如 en-US


解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java为容器中的 Linux 设置语言信息


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

通过搜索,找到了 StackOverflow 上的提问:How to set the locale inside a Debian/Ubuntu Docker container?(https://stackoverflow.com/questions/28405902/how-to-set-the-locale-inside-a-debian-ubuntu-docker-container),并从中得到了解决方案。



方案一:通过安装 locales-all 包


通过安装 locales 和 locales-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"]

解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java_22


解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java方案二:通过安装 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

解决 .NET Core 在 Linux Container 中获取 CurrentCulture_java_24

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