[C#] カルチャ依存問題からの完全脱却、インバリアントモードとコード分析の有効化

Windows 上の .NET 5 以降での破壊的変更です。文字列のソート、順序比較のデフォルトの動作が変更されました。パフォーマンスにも影響します。
実行環境に依存せず、一番良いパフォーマンスにする設定方法について記載します。

まずはシンプルなコードで変更内容を見てみましょう。

string[] src = [ "あ", "ア", "ア" ];

// 何らかのソート処理
src.OrderBy(x => x).ToArray();
Array.Sort(src);

// 結果
// ア ア あ (.NET 5 より前のデフォルト動作、.NET Framework も同じ)
// あ ア ア (.NET 5 以降のデフォルト動作)

これ以外の文字列でも、色々なメソッドで影響を受けます。

今回特に問題になるのは IComparer<string> の引数があるメソッドです。

少なからずカルチャ依存問題は .NET Framework の頃からありました。
しかし、.NET Core 2.0 で導入されているインバリアントモードを有効にすることで、カルチャ依存問題から脱却できます。

環境

前提

この対応を行う目的は、実行環境(地域・言語設定)により動作が異ならないようにすることと、性能を向上させることです。

  • CultureInfo.InvariantCulture を使用して、カルチャに依存しないようになります。
  • StringComparer.Ordinal を使用して、バイナリ比較かつ高速化になります。
  • ❗ 任意の CultureInfo (例えば ja-JP など) が使用できなくなる点は注意です。

まず、アプリ開発をするのか、ライブラリ開発をするのかで、必要な対応方法が異なります。
最初にオススメの設定方法を示します。各設定の説明については後述します。

アプリ開発向けのオススメ設定方法

アプリ開発の場合は、インバリアントモードを有効にするだけで設定完了です。
CultureInfo を指定しても強制的に InvariantCulture になるため、コーディング上で特に気を付けることはないと思います。

グローバリゼーション インバリアント モードの設定

以下の設定で、インバリアントモードを有効にします。

アプロプロジェクトファイルの .csproj に以下を追加します。

<PropertyGroup>
  <InvariantGlobalization>true</InvariantGlobalization>
  <PredefinedCulturesOnly>false</PredefinedCulturesOnly>
</PropertyGroup>

もし、アプリ以外にライブラリプロジェクトがある場合は、Directory.Build.props に設定してソリューション内の全てのプロジェクトに適用すると良いでしょう。
ライブラリプロジェクトでの余計なコード分析の通知を抑制することができます。

インバリアント カルチャの文字列フォーマットの設定

InvariantCulture でも北米フォーマットになる部分があるため、ISO 8601 形式に変更します。必須ではありませんがオススメです。

アプリプロジェクトに以下のコードを追加します。ModuleInitializer を使用せず、エントリポイントの最初の方に実行しても良いと思います。

internal static class Initialier
{
    [ModuleInitializer]
    public static void Init()
    {
        var c = (CultureInfo)CultureInfo.InvariantCulture.Clone();
        c.DateTimeFormat.LongDatePattern = "yyyy'-'MM'-'dd";
        c.DateTimeFormat.LongTimePattern = "HH':'mm':'ss";
        c.DateTimeFormat.MonthDayPattern = "MM'-'dd";
        c.DateTimeFormat.YearMonthPattern = "yyyy'-'MM";
        c.DateTimeFormat.ShortDatePattern = "yyyy'-'MM'-'dd";
        c.DateTimeFormat.ShortTimePattern = "HH':'mm";

        CultureInfo.DefaultThreadCurrentCulture = c;
    }
}

詳しくは後述します。

アプリ開発向けの設定は、以上で完了です。

ライブラリ開発向けのオススメ設定方法

ライブラリ開発をする場合は、使用者側がインバリアントモードで実行するかどうかは分かりません。
そのため、明示的にインバリアントを指定するように自分でコーディングする必要があります。
コード分析を有効にして、カルチャ依存のコードを検出することが重要になります。

コード分析のエラー修正方法

下記で行う各コード分析を有効にすると、いくつかビルド警告(エラー)が発生するようになります。まずはエラーの修正方法について説明します。

以下のような方向で修正しましょう。

  • string.ToLower()string.ToLowerInvariant() に変更する。
  • IFormatProvider を指定するオーバーロードがある場合は、CultureInfo.InvariantCulture を指定する。
  • StringComparison を指定するオーバーロードがある場合は、StringComparison.Ordinal or OrdinalIgnoreCase を指定する。
  • IComparer<string> を指定するオーバーロードがある場合は、StringComparer.Ordinal or OrdinalIgnoreCase を指定する。
  • 文字列補間($)は、String.Create(IFormatProvider, DefaultInterpolatedStringHandler) を使用する。
  • ❗ 外部ライブラリの処理で、内部的に ToString() される可能性があるメソッドは使用しない、もしくは事前に string にして指定する。

修正イメージです。

string str = "abc";
- str.ToLower();
+ str.ToLowerInvariant();
- str.IndexOf("bc");
+ str.IndexOf("bc", StringComparison.Ordinal);
- float.TryParse(str, out _);
+ float.TryParse(str, CultureInfo.InvariantCulture, out _);

float num = 1.23;
- num.ToString();
+ num.ToString(CultureInfo.InvariantCulture);

string[] strings = new string[] { "A", "a" };
- strings.OrderBy(x => x).ToArray();
+ strings.OrderBy(x => x, StringComparer.Ordinal).ToArray();

.NET コード分析の設定

EnableNETAnalyzers は .NET 5 以降で既定で有効になりますが、netstandard などで有効にするには明示的に設定する必要があります。

Directory.Build.props または各ライブラリ .csproj に以下を追加します。

<Project>
  <PropertyGroup>
    <EnableNETAnalyzers>true</EnableNETAnalyzers>
    <WarningsAsErrors>$(WarningsAsErrors);CA1304;CA1305;CA1309;CA1310;CA1311</WarningsAsErrors>
  </PropertyGroup>
</Project>

<WarningsAsErrors> の設定については、.editorconfig に記述した方が見やすく、柔軟に設定できます。ただし、<WarningsAsErrors> の設定が優先されます。

[*.{cs,vb}]
dotnet_diagnostic.CA1304.severity = error
dotnet_diagnostic.CA1305.severity = error
dotnet_diagnostic.CA1309.severity = error
dotnet_diagnostic.CA1310.severity = error
dotnet_diagnostic.CA1311.severity = error

ルールについて詳しくは後述します。

Meziantou.Analyzer コード分析の設定

前述の .NET コード分析 だけでは、コード検出が不十分な場合があります。
Meziantou.Analyzer が細かくコード分析してくれるため、NuGet から追加で導入します。

多数のコード分析が入っています。ここでは今回必要な最小限の設定のみを行うため、必要なルールのみ error、その他は none に設定してあります。
error に適用したルールについて詳しくは後述します。

それでは Global AnalyzerConfig の設定ファイル Meziantou.Analyzer.globalconfig を作成し、以下の設定をします。

ℹ️ Meziantou.Analyzer v2.0.163 前提での設定です。

# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files#global-analyzerconfig
is_global = true

# https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/README.md

# MA0001: StringComparison is missing
dotnet_diagnostic.MA0001.severity = none

# MA0002: IEqualityComparer<string> or IComparer<string> is missing
dotnet_diagnostic.MA0002.severity = error

# MA0003: Add parameter name to improve readability
dotnet_diagnostic.MA0003.severity = none

# MA0004: Use Task.ConfigureAwait
dotnet_diagnostic.MA0004.severity = none

# MA0005: Use Array.Empty<T>()
dotnet_diagnostic.MA0005.severity = none

# MA0006: Use String.Equals instead of equality operator
dotnet_diagnostic.MA0006.severity = none

# MA0007: Add a comma after the last value
dotnet_diagnostic.MA0007.severity = none

# MA0008: Add StructLayoutAttribute
dotnet_diagnostic.MA0008.severity = none

# MA0009: Add regex evaluation timeout
dotnet_diagnostic.MA0009.severity = none

# MA0010: Mark attributes with AttributeUsageAttribute
dotnet_diagnostic.MA0010.severity = none

# MA0011: IFormatProvider is missing
dotnet_diagnostic.MA0011.severity = none

# MA0012: Do not raise reserved exception type
dotnet_diagnostic.MA0012.severity = none

# MA0013: Types should not extend System.ApplicationException
dotnet_diagnostic.MA0013.severity = none

# MA0014: Do not raise System.ApplicationException type
dotnet_diagnostic.MA0014.severity = none

# MA0015: Specify the parameter name in ArgumentException
dotnet_diagnostic.MA0015.severity = none

# MA0016: Prefer using collection abstraction instead of implementation
dotnet_diagnostic.MA0016.severity = none

# MA0017: Abstract types should not have public or internal constructors
dotnet_diagnostic.MA0017.severity = none

# MA0018: Do not declare static members on generic types (deprecated; use CA1000 instead)
dotnet_diagnostic.MA0018.severity = none

# MA0019: Use EventArgs.Empty
dotnet_diagnostic.MA0019.severity = none

# MA0020: Use direct methods instead of LINQ methods
dotnet_diagnostic.MA0020.severity = none

# MA0021: Use StringComparer.GetHashCode instead of string.GetHashCode
dotnet_diagnostic.MA0021.severity = none

# MA0022: Return Task.FromResult instead of returning null
dotnet_diagnostic.MA0022.severity = none

# MA0023: Add RegexOptions.ExplicitCapture
dotnet_diagnostic.MA0023.severity = none

# MA0024: Use an explicit StringComparer when possible
dotnet_diagnostic.MA0024.severity = none

# MA0025: Implement the functionality instead of throwing NotImplementedException
dotnet_diagnostic.MA0025.severity = none

# MA0026: Fix TODO comment
dotnet_diagnostic.MA0026.severity = none

# MA0027: Prefer rethrowing an exception implicitly
dotnet_diagnostic.MA0027.severity = none

# MA0028: Optimize StringBuilder usage
dotnet_diagnostic.MA0028.severity = none

# MA0029: Combine LINQ methods
dotnet_diagnostic.MA0029.severity = none

# MA0030: Remove useless OrderBy call
dotnet_diagnostic.MA0030.severity = none

# MA0031: Optimize Enumerable.Count() usage
dotnet_diagnostic.MA0031.severity = none

# MA0032: Use an overload with a CancellationToken argument
dotnet_diagnostic.MA0032.severity = none

# MA0033: Do not tag instance fields with ThreadStaticAttribute
dotnet_diagnostic.MA0033.severity = none

# MA0035: Do not use dangerous threading methods
dotnet_diagnostic.MA0035.severity = none

# MA0036: Make class static
dotnet_diagnostic.MA0036.severity = none

# MA0037: Remove empty statement
dotnet_diagnostic.MA0037.severity = none

# MA0038: Make method static (deprecated, use CA1822 instead)
dotnet_diagnostic.MA0038.severity = none

# MA0039: Do not write your own certificate validation method
dotnet_diagnostic.MA0039.severity = none

# MA0040: Forward the CancellationToken parameter to methods that take one
dotnet_diagnostic.MA0040.severity = none

# MA0041: Make property static (deprecated, use CA1822 instead)
dotnet_diagnostic.MA0041.severity = none

# MA0042: Do not use blocking calls in an async method
dotnet_diagnostic.MA0042.severity = none

# MA0043: Use nameof operator in ArgumentException
dotnet_diagnostic.MA0043.severity = none

# MA0044: Remove useless ToString call
dotnet_diagnostic.MA0044.severity = none

# MA0045: Do not use blocking calls in a sync method (need to make calling method async)
dotnet_diagnostic.MA0045.severity = none

# MA0046: Use EventHandler<T> to declare events
dotnet_diagnostic.MA0046.severity = none

# MA0047: Declare types in namespaces
dotnet_diagnostic.MA0047.severity = none

# MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none

# MA0049: Type name should not match containing namespace
dotnet_diagnostic.MA0049.severity = none

# MA0050: Validate arguments correctly in iterator methods
dotnet_diagnostic.MA0050.severity = none

# MA0051: Method is too long
dotnet_diagnostic.MA0051.severity = none

# MA0052: Replace constant Enum.ToString with nameof
dotnet_diagnostic.MA0052.severity = none

# MA0053: Make class sealed
dotnet_diagnostic.MA0053.severity = none

# MA0054: Embed the caught exception as innerException
dotnet_diagnostic.MA0054.severity = none

# MA0055: Do not use finalizer
dotnet_diagnostic.MA0055.severity = none

# MA0056: Do not call overridable members in constructor
dotnet_diagnostic.MA0056.severity = none

# MA0057: Class name should end with 'Attribute'
dotnet_diagnostic.MA0057.severity = none

# MA0058: Class name should end with 'Exception'
dotnet_diagnostic.MA0058.severity = none

# MA0059: Class name should end with 'EventArgs'
dotnet_diagnostic.MA0059.severity = none

# MA0060: The value returned by Stream.Read/Stream.ReadAsync is not used
dotnet_diagnostic.MA0060.severity = none

# MA0061: Method overrides should not change default values
dotnet_diagnostic.MA0061.severity = none

# MA0062: Non-flags enums should not be marked with "FlagsAttribute"
dotnet_diagnostic.MA0062.severity = none

# MA0063: Use Where before OrderBy
dotnet_diagnostic.MA0063.severity = none

# MA0064: Avoid locking on publicly accessible instance
dotnet_diagnostic.MA0064.severity = none

# MA0065: Default ValueType.Equals or HashCode is used for struct equality
dotnet_diagnostic.MA0065.severity = none

# MA0066: Hash table unfriendly type is used in a hash table
dotnet_diagnostic.MA0066.severity = none

# MA0067: Use Guid.Empty
dotnet_diagnostic.MA0067.severity = none

# MA0068: Invalid parameter name for nullable attribute
dotnet_diagnostic.MA0068.severity = none

# MA0069: Non-constant static fields should not be visible
dotnet_diagnostic.MA0069.severity = none

# MA0070: Obsolete attributes should include explanations
dotnet_diagnostic.MA0070.severity = none

# MA0071: Avoid using redundant else
dotnet_diagnostic.MA0071.severity = none

# MA0072: Do not throw from a finally block
dotnet_diagnostic.MA0072.severity = none

# MA0073: Avoid comparison with bool constant
dotnet_diagnostic.MA0073.severity = none

# MA0074: Avoid implicit culture-sensitive methods
dotnet_diagnostic.MA0074.severity = none

# MA0075: Do not use implicit culture-sensitive ToString
dotnet_diagnostic.MA0075.severity = none

# MA0076: Do not use implicit culture-sensitive ToString in interpolated strings
dotnet_diagnostic.MA0076.severity = error

# MA0077: A class that provides Equals(T) should implement IEquatable<T>
dotnet_diagnostic.MA0077.severity = none

# MA0078: Use 'Cast' instead of 'Select' to cast
dotnet_diagnostic.MA0078.severity = none

# MA0079: Forward the CancellationToken using .WithCancellation()
dotnet_diagnostic.MA0079.severity = none

# MA0080: Use a cancellation token using .WithCancellation()
dotnet_diagnostic.MA0080.severity = none

# MA0081: Method overrides should not omit params keyword
dotnet_diagnostic.MA0081.severity = none

# MA0082: NaN should not be used in comparisons
dotnet_diagnostic.MA0082.severity = none

# MA0083: ConstructorArgument parameters should exist in constructors
dotnet_diagnostic.MA0083.severity = none

# MA0084: Local variables should not hide other symbols
dotnet_diagnostic.MA0084.severity = none

# MA0085: Anonymous delegates should not be used to unsubscribe from Events
dotnet_diagnostic.MA0085.severity = none

# MA0086: Do not throw from a finalizer
dotnet_diagnostic.MA0086.severity = none

# MA0087: Parameters with [DefaultParameterValue] attributes should also be marked [Optional]
dotnet_diagnostic.MA0087.severity = none

# MA0088: Use [DefaultParameterValue] instead of [DefaultValue]
dotnet_diagnostic.MA0088.severity = none

# MA0089: Optimize string method usage
dotnet_diagnostic.MA0089.severity = none

# MA0090: Remove empty else/finally block
dotnet_diagnostic.MA0090.severity = none

# MA0091: Sender should be 'this' for instance events
dotnet_diagnostic.MA0091.severity = none

# MA0092: Sender should be 'null' for static events
dotnet_diagnostic.MA0092.severity = none

# MA0093: EventArgs should not be null
dotnet_diagnostic.MA0093.severity = none

# MA0094: A class that provides CompareTo(T) should implement IComparable<T>
dotnet_diagnostic.MA0094.severity = none

# MA0095: A class that implements IEquatable<T> should override Equals(object)
dotnet_diagnostic.MA0095.severity = none

# MA0096: A class that implements IComparable<T> should also implement IEquatable<T>
dotnet_diagnostic.MA0096.severity = none

# MA0097: A class that implements IComparable<T> or IComparable should override comparison operators
dotnet_diagnostic.MA0097.severity = none

# MA0098: Use indexer instead of LINQ methods
dotnet_diagnostic.MA0098.severity = none

# MA0099: Use Explicit enum value instead of 0
dotnet_diagnostic.MA0099.severity = none

# MA0100: Await task before disposing of resources
dotnet_diagnostic.MA0100.severity = none

# MA0101: String contains an implicit end of line character
dotnet_diagnostic.MA0101.severity = none

# MA0102: Make member readonly
dotnet_diagnostic.MA0102.severity = none

# MA0103: Use SequenceEqual instead of equality operator
dotnet_diagnostic.MA0103.severity = none

# MA0104: Do not create a type with a name from the BCL
dotnet_diagnostic.MA0104.severity = none

# MA0105: Use the lambda parameters instead of using a closure
dotnet_diagnostic.MA0105.severity = none

# MA0106: Avoid closure by using an overload with the 'factoryArgument' parameter
dotnet_diagnostic.MA0106.severity = none

# MA0107: Do not use culture-sensitive object.ToString
dotnet_diagnostic.MA0107.severity = none

# MA0108: Remove redundant argument value
dotnet_diagnostic.MA0108.severity = none

# MA0109: Consider adding an overload with a Span<T> or Memory<T>
dotnet_diagnostic.MA0109.severity = none

# MA0110: Use the Regex source generator
dotnet_diagnostic.MA0110.severity = none

# MA0111: Use string.Create instead of FormattableString
dotnet_diagnostic.MA0111.severity = error

# MA0112: Use 'Count > 0' instead of 'Any()'
dotnet_diagnostic.MA0112.severity = none

# MA0113: Use DateTime.UnixEpoch
dotnet_diagnostic.MA0113.severity = none

# MA0114: Use DateTimeOffset.UnixEpoch
dotnet_diagnostic.MA0114.severity = none

# MA0115: Unknown component parameter
dotnet_diagnostic.MA0115.severity = none

# MA0116: Parameters with [SupplyParameterFromQuery] attributes should also be marked as [Parameter]
dotnet_diagnostic.MA0116.severity = none

# MA0117: Parameters with [EditorRequired] attributes should also be marked as [Parameter]
dotnet_diagnostic.MA0117.severity = none

# MA0118: [JSInvokable] methods must be public
dotnet_diagnostic.MA0118.severity = none

# MA0119: JSRuntime must not be used in OnInitialized or OnInitializedAsync
dotnet_diagnostic.MA0119.severity = none

# MA0120: Use InvokeVoidAsync when the returned value is not used
dotnet_diagnostic.MA0120.severity = none

# MA0121: Do not overwrite parameter value
dotnet_diagnostic.MA0121.severity = none

# MA0122: Parameters with [SupplyParameterFromQuery] attributes are only valid in routable components (@page)
dotnet_diagnostic.MA0122.severity = none

# MA0123: Sequence number must be a constant
dotnet_diagnostic.MA0123.severity = none

# MA0124: Log parameter type is not valid
dotnet_diagnostic.MA0124.severity = none

# MA0125: The list of log parameter types contains an invalid type
dotnet_diagnostic.MA0125.severity = none

# MA0126: The list of log parameter types contains a duplicate
dotnet_diagnostic.MA0126.severity = none

# MA0127: Use String.Equals instead of is pattern
dotnet_diagnostic.MA0127.severity = none

# MA0128: Use 'is' operator instead of SequenceEqual
dotnet_diagnostic.MA0128.severity = none

# MA0129: Await task in using statement
dotnet_diagnostic.MA0129.severity = none

# MA0130: GetType() should not be used on System.Type instances
dotnet_diagnostic.MA0130.severity = none

# MA0131: ArgumentNullException.ThrowIfNull should not be used with non-nullable types
dotnet_diagnostic.MA0131.severity = none

# MA0132: Do not convert implicitly to DateTimeOffset
dotnet_diagnostic.MA0132.severity = none

# MA0133: Use DateTimeOffset instead of relying on the implicit conversion
dotnet_diagnostic.MA0133.severity = none

# MA0134: Observe result of async calls
dotnet_diagnostic.MA0134.severity = none

# MA0135: The log parameter has no configured type
dotnet_diagnostic.MA0135.severity = none

# MA0136: Raw String contains an implicit end of line character
dotnet_diagnostic.MA0136.severity = none

# MA0137: Use 'Async' suffix when a method returns an awaitable type
dotnet_diagnostic.MA0137.severity = none

# MA0138: Do not use 'Async' suffix when a method does not return an awaitable type
dotnet_diagnostic.MA0138.severity = none

# MA0139: Log parameter type is not valid
dotnet_diagnostic.MA0139.severity = none

# MA0140: Both if and else branch have identical code
dotnet_diagnostic.MA0140.severity = none

# MA0141: Use pattern matching instead of inequality operators for null check
dotnet_diagnostic.MA0141.severity = none

# MA0142: Use pattern matching instead of equality operators for null check
dotnet_diagnostic.MA0142.severity = none

# MA0143: Primary constructor parameters should be readonly
dotnet_diagnostic.MA0143.severity = none

# MA0144: Use System.OperatingSystem to check the current OS
dotnet_diagnostic.MA0144.severity = none

# MA0145: Signature for [UnsafeAccessorAttribute] method is not valid
dotnet_diagnostic.MA0145.severity = none

# MA0146: Name must be set explicitly on local functions
dotnet_diagnostic.MA0146.severity = none

# MA0147: Avoid async void method for delegate
dotnet_diagnostic.MA0147.severity = none

# MA0148: Use pattern matching instead of equality operators for discrete value
dotnet_diagnostic.MA0148.severity = none

# MA0149: Use pattern matching instead of inequality operators for discrete value
dotnet_diagnostic.MA0149.severity = none

# MA0150: Do not call the default object.ToString explicitly
dotnet_diagnostic.MA0150.severity = none

# MA0151: DebuggerDisplay must contain valid members
dotnet_diagnostic.MA0151.severity = none

# MA0152: Use Unwrap instead of using await twice
dotnet_diagnostic.MA0152.severity = none

# MA0153: Do not log symbols decorated with DataClassificationAttribute directly
dotnet_diagnostic.MA0153.severity = none

# MA0154: Use langword in XML comment
dotnet_diagnostic.MA0154.severity = none

# MA0155: Do not use async void methods
dotnet_diagnostic.MA0155.severity = none

# MA0156: Use 'Async' suffix when a method returns IAsyncEnumerable<T>
dotnet_diagnostic.MA0156.severity = none

# MA0157: Do not use 'Async' suffix when a method does not return IAsyncEnumerable<T>
dotnet_diagnostic.MA0157.severity = none

# MA0158: Use System.Threading.Lock
dotnet_diagnostic.MA0158.severity = none

# MA0159: Use 'Order' instead of 'OrderBy'
dotnet_diagnostic.MA0159.severity = none

# MA0160: Use ContainsKey instead of TryGetValue
dotnet_diagnostic.MA0160.severity = none

この設定ファイルを各プロジェクトに読み込ませます。
Directory.Build.props または各ライブラリ .csproj に以下を追加します。

<ItemGroup>
  <GlobalAnalyzerConfigFiles Include="Meziantou.Analyzer.globalconfig" />
</ItemGroup>

BannedApiAnalyzers コード分析の設定

InvariantCultureOrdinal, OrdinalIgnoreCase 以外を使用できないように禁止します。

Microsoft.CodeAnalysis.BannedApiAnalyzersNuGet から導入してください。

次に BannedApiAnalyzers.globalconfig を作成し、以下の設定をします。設定項目が1つのみなので、.editorconfig に直接設定しても良いでしょう。

# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files#global-analyzerconfig
is_global = true

# https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.BannedApiAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers.md

# RS0030: 禁止 API を使用しない
dotnet_diagnostic.RS0030.severity = error

禁止にする API の設定ファイル BannedSymbols.txt を作成し、以下の内容を記述します。

P:System.Globalization.CultureInfo.CurrentCulture
P:System.Globalization.CultureInfo.CurrentUICulture
P:System.Globalization.CultureInfo.InstalledUICulture

P:System.Globalization.DateTimeFormatInfo.CurrentInfo
P:System.Globalization.NumberFormatInfo.CurrentInfo

P:System.StringComparer.CurrentCulture
P:System.StringComparer.CurrentCulture
P:System.StringComparer.CurrentCultureIgnoreCase
P:System.StringComparer.InvariantCulture
P:System.StringComparer.InvariantCultureIgnoreCase

F:System.StringComparison.CurrentCulture
F:System.StringComparison.CurrentCultureIgnoreCase
F:System.StringComparison.InvariantCulture
F:System.StringComparison.InvariantCultureIgnoreCase

M:System.FormattableString.CurrentCulture

過激派は string.JoinStringBuilder を追加したこちらをどうぞ。

P:System.Globalization.CultureInfo.CurrentCulture
P:System.Globalization.CultureInfo.CurrentUICulture
P:System.Globalization.CultureInfo.InstalledUICulture

P:System.Globalization.DateTimeFormatInfo.CurrentInfo
P:System.Globalization.NumberFormatInfo.CurrentInfo

P:System.StringComparer.CurrentCulture
P:System.StringComparer.CurrentCulture
P:System.StringComparer.CurrentCultureIgnoreCase
P:System.StringComparer.InvariantCulture
P:System.StringComparer.InvariantCultureIgnoreCase

F:System.StringComparison.CurrentCulture
F:System.StringComparison.CurrentCultureIgnoreCase
F:System.StringComparison.InvariantCulture
F:System.StringComparison.InvariantCultureIgnoreCase

M:System.FormattableString.CurrentCulture

M:System.String.Join(System.Char,System.Object[])
M:System.String.Join`1(System.Char,System.Collections.Generic.IEnumerable{``0})
M:System.String.Join(System.String,System.Object[])
M:System.String.Join`1(System.String,System.Collections.Generic.IEnumerable{``0})

M:System.Text.StringBuilder.Append(System.SByte)
M:System.Text.StringBuilder.Append(System.Byte)
M:System.Text.StringBuilder.Append(System.Int16)
M:System.Text.StringBuilder.Append(System.Int32)
M:System.Text.StringBuilder.Append(System.Int64)
M:System.Text.StringBuilder.Append(System.Single)
M:System.Text.StringBuilder.Append(System.Double)
M:System.Text.StringBuilder.Append(System.Decimal)
M:System.Text.StringBuilder.Append(System.UInt16)
M:System.Text.StringBuilder.Append(System.UInt32)
M:System.Text.StringBuilder.Append(System.UInt64)
M:System.Text.StringBuilder.Append(System.Object)
M:System.Text.StringBuilder.AppendJoin(System.Char,System.String[])
M:System.Text.StringBuilder.AppendJoin(System.Char,System.Object[])
M:System.Text.StringBuilder.AppendJoin`1(System.Char,System.Collections.Generic.IEnumerable{``0})
M:System.Text.StringBuilder.AppendJoin(System.String,System.String[])
M:System.Text.StringBuilder.AppendJoin(System.String,System.Object[])
M:System.Text.StringBuilder.AppendJoin`1(System.String,System.Collections.Generic.IEnumerable{``0})

これらの設定ファイルを各プロジェクトに読み込ませます。
Directory.Build.props または各ライブラリ .csproj に以下を追加します。

<ItemGroup>
  <GlobalAnalyzerConfigFiles Include="BannedApiAnalyzers.globalconfig" />
  <AdditionalFiles Include="BannedSymbols.txt" />
</ItemGroup>

詳しくは後述します。

ライブラリ設定向けの設定は、以上で完了です。

InvariantGlobalization について

グローバリゼーション インバリアント モードの設定 で設定した InvariantGlobalization についてです。

.NET 5 から Windows では NLC (National Language Support) から ICU (International Components for Unicode) が使用されるように変更されました。

各バージョンの既定の動作は、以下のようになります。

RuntimeWindowsLinux備考
.NET Framework 1.0 - 4.8NLS-
NET Core 1.0 - 3.1NLSICUOS で動作が異なっていた
.NET 5.0ICUICUICU に統一された

実際にソートを試してみます。

var strings = new[]
{
    "あ", "ぁ",  // ひらがな
    "ア", "ァ",  // 全角カタカナ
    "ア", "ァ",    // 半角カタカナ
    "A", "a",    // 半角
    "A", "a",  // 全角
};

strings.Order(StringComparer.Ordinal).ToArray();
strings.Order(StringComparer.CurrentCulture).ToArray();
strings.Order(StringComparer.InvariantCulture).ToArray();

IComparer<string> は以下を指定します。

  1. StringComparer.Ordinal
  2. StringComparer.CurrentCulture (ja-JP)
  3. StringComparer.InvariantCulture
  4. NLS を有効にした場合の StringComparer.CurrentCulture (ja-JP)
  5. NLS を有効にした場合の StringComparer.InvariantCulture

4 と 5 は ICU の代わりに NLS を使用するtrue にした場合です。.NET Framework と同じ動作になります。
これに IgnoreCase を加えたものも試してみます。結果が同じ場合は、同じ列にまとめて記載しています。

1. Ordinal2. Current (ICU)3. Invariant (ICU)4. Current (NLS),
5. Invariant (NLS)
2. Current (ICU),
3. Invariant (ICU)
+ IgnoreCase
4. Current (NLS),
5. Invariant (NLS)
+ IgnoreCase
AaaaAA
a (全角) (全角) (全角)aa
AAA (全角) (全角)
(全角) (全角) (全角) (全角) (全角)
(半角カナ) (半角カナ)
(全角) (半角カナ)
(全角) (半角カナ) (半角カナ) (半角カナ) (半角カナ)
(半角カナ)
(半角カナ) (半角カナ) (半角カナ) (半角カナ)

❗ ここで各カルチャで以下の文字は、同じ文字と判定されています。

  • CurrentCulture ja-JP (ICU)
    • = = (大文字の “あ” 系)
    • = = (小文字の “ぁ” 系)
    • A = (大文字の “A” 系)
    • a = (小文字の “a” 系)
  • CurrentCultureIgnoreCase ja-JP (ICU)
    • ⚠️ = (カタカナの判定が消えた…)
    • A = a
    • =
  • InvariantCulture (ICU)
    • ✅ 上記の全ての文字は異なると判定されている
  • InvariantCultureIgnoreCase (ICU)
    • ⚠️ = (InvariantCulture では区別されていたのに IgnoreCase になると、ひらがなだけ判定される…)
    • A = a
    • =

なお、Ordinal と NLS の IgnoreCase は、英字の大小文字のみを正しく同じと判定されています。

  • OrdinalIgnoreCase
  • CurrentCultureIgnoreCase ja-JP (NLS)
  • InvariantCultureIgnoreCase ja-JP (NLS)
    • A = a
    • =

このように ICU 環境では統一感のない結果になっているため、注意が必要です。
Incorrect string comparison for Japanese small letters with IgnoreCase option in .NET 5 ICU · Issue #54987 · dotnet/runtime

つまり、OrdinalOrdinalIgnoreCase で判定するのが確実であり、最も高速です。

特殊な並び替えが必要な場合は、自分で明示的に並び替えた方が良いでしょう。
例えば、大文字小文字を区別をしない自然ソートがあります。

[C#] 文字列を数値の大きさで並び替える自然ソートを行う

Windows エクスプローラーのファイル順と同じように、文字列中の数字を値の大きさでソートします。OS に依存しない実装も対応します。

C# C# Win32API 2022-12-04 (日)

PredefinedCulturesOnly について

グローバリゼーション インバリアント モードの設定 で設定した PredefinedCulturesOnly についてです。

InvariantGlobalization を有効にすることでカルチャ依存問題は解決しますが、次の問題が発生します。
破壊的変更: グローバリゼーション インバリアント モードでのカルチャの作成とケース マッピング - .NET | Microsoft Learn

具体的には、以下のようなコードで CultureInfo が作成された時に CultureNotFoundException が発生します。

// 1041 は "ja-JP"
new CultureInfo("ja-JP");
new CultureInfo(1041);
CultureInfo.GetCultureInfo("ja-jp")
CultureInfo.GetCultureInfo(1041)

.NET 6 で PredefinedCulturesOnly という設定が追加されました。これを false にすることで、例外ではなく InvariantCulture と同等のカルチャ設定が返されるようになります。

<PropertyGroup>
  <InvariantGlobalization>true</InvariantGlobalization>
  <PredefinedCulturesOnly>false or true</PredefinedCulturesOnly>
</PropertyGroup>

自分のコードではなく参照した NuGet ライブラリ内で CultureInfo が作成されても例外が発生するため、PredefinedCulturesOnlyfalse に設定することで回避します。

また、int 指定の CultureInfo の作成時の例外は、.NET 8 で発生しないように修正されました。
PredefinedCulturesOnly=false not respected in new CultureInfo(int) #86878

.NETPredefinedCulturesOnlyCultureInfo(“ja-JP”)CultureInfo(1041)
.NET 5-InvariantCulture を返すCultureNotFoundException 発生
.NET 6, .NET 7true (既定値)CultureNotFoundException 発生CultureNotFoundException 発生
.NET 6, .NET 7falseInvariantCulture を返すCultureNotFoundException 発生
.NET 8 以降true (既定値)CultureNotFoundException 発生CultureNotFoundException 発生
.NET 8 以降falseInvariantCulture を返すInvariantCulture を返す

この設定により、例外が発生することなく、常に既定で InvariantCulture で動作するようになります。

CultureInfo.DefaultThreadCurrentCulture について

インバリアント カルチャの文字列フォーマットの設定 についての説明です。

DateTimeFormat の各設定は、ToString() する際に以下の部分に適用されます。

Init();

var d = new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Unspecified);
d.ToString();          // 2024-01-02 (5) 03:04:05 (2)
d.ToLongDateString();  // 2024-01-02 (1)
d.ToShortDateString(); // 2024-01-02 (5)
d.ToLongTimeString();  // 03:04:05 (2)
d.ToShortTimeString(); // 03:04 (6)
d.ToString("F");       // 2024-01-02 (1) 03:04:05 (2)
d.ToString("G");       // 2024-01-02 (5) 03:04:05 (2)
d.ToString("g");       // 2024-01-02 (5) 03:04:05 (6)
d.ToString("M");       // 01-02 (3)
d.ToString("Y");       // 2024-01 (4)
d.ToString("U");       // 2024-01-01 (1) 18:04:05 (2)

var o = new DateOnly(2024, 1, 2);
o.ToString();          // 2024-01-02 (5)
o.ToLongDateString();  // 2024-01-02 (1)
o.ToShortDateString(); // 2024-01-02 (5)
o.ToString("M");       // 01-02 (3)
o.ToString("Y");       // 2024-01 (4)

var t = new TimeOnly(3,4,5,6,7);
t.ToString();          // 03:04:05 (6)
t.ToLongTimeString();  // 03:04:05 (2)
t.ToShortTimeString(); // 03:04 (6)

static void Init()
{
    var c = (CultureInfo)CultureInfo.InvariantCulture.Clone();
    c.DateTimeFormat.LongDatePattern = "yyyy'-'MM'-'dd (1)";
    c.DateTimeFormat.LongTimePattern = "HH':'mm':'ss (2)";
    c.DateTimeFormat.MonthDayPattern = "MM'-'dd (3)";
    c.DateTimeFormat.YearMonthPattern = "yyyy'-'MM (4)";
    c.DateTimeFormat.ShortDatePattern = "yyyy'-'MM'-'dd (5)";
    c.DateTimeFormat.ShortTimePattern = "HH':'mm (6)";

    CultureInfo.DefaultThreadCurrentCulture = c;
}

このカスタムした設定と標準の InvariantCulture の比較をまとめました。

まず、CultureInfo.DateTimeFormat の設定値の比較です。

DateTimeFormat のプロパティカスタム設定InvariantCultureInvariantCulture 出力例
(1) LongDatePatternyyyy'-'MM'-'dddddd, dd MMMM yyyyTuesday, 02 January 2024
(2) LongTimePatternHH':'mm':'ssHH:mm:ss03:04:05
(3) MonthDayPatternMM'-'ddMMMM ddJanuary 02
(4) YearMonthPatternyyyy'-'MMyyyy MMMM2024 January
(5) ShortDatePatternyyyy'-'MM'-'ddMM/dd/yyyy01/02/2024
(6) ShortTimePatternHH':'mmHH:mm03:04

次に、DateTimeDateOnlyTimeOnlyToString() の比較です。

ToStringカスタム設定InvariantCulture
DateTime.ToString(), ToString("G")2024-01-02 03:04:0501/02/2024 03:04:05
DateTime.ToLongDateString(), ToString("D")2024-01-02Tuesday, 02 January 2024
DateTime.ToShortDateString(), ToString("d")2024-01-0201/02/2024
DateTime.ToLongTimeString(), ToString("T")03:04:0503:04:05
DateTime.ToShortTimeString(), ToString("t")03:0403:04
DateTime.ToString("F")2024-01-02 03:04:05Tuesday, 02 January 2024 03:04:05
DateTime.ToString("G")2024-01-02 03:04:0501/02/2024 03:04:05
DateTime.ToString("g")2024-01-02 03:0401/02/2024 03:04
DateTime.ToString("M")01-02January 02
DateTime.ToString("Y")2024-012024 January
DateTime.ToString("U") (世界時刻 UTC)2024-01-01 18:04:05Monday, 01 January 2024 18:04:05
DateOnly.ToString(), ToString("d")2024-01-0201/02/2024
DateOnly.ToLongDateString(), ToString("D")2024-01-02Tuesday, 02 January 2024
DateOnly.ToShortDateString(), ToString("d")2024-01-0201/02/2024
DateOnly.ToString("M")01-02January 02
DateOnly.ToString("Y")2024-012024 January
TimeOnly.ToString(), ToString("t")03:0403:04
TimeOnly.ToLongTimeString(), ToString("T")03:04:0503:04:05
TimeOnly.ToShortTimeString(), ToString("t")03:0403:04

上記のように ISO 8601 形式に変更しています。

コード分析のルールについて

.NET コード分析の設定Meziantou.Analyzer コード分析の設定BannedApiAnalyzers コード分析の設定 について説明します。

Roslyn コード分析は、以下のようなルールがあります。
グローバリゼーション規則 (コード分析) - .NET | Microsoft Learn

適用ルール備考
CA1304: CultureInfo を指定しますstring.Compare, ToLower, ToUpper, ResourceManager.GetString などが該当
CA1305: IFormatProvider を指定する組み込み型や DateTimexxx.ToString, xxx.ParseConvert クラスメソッドなどが該当
CA1307: 意味を明確にするために StringComparison を指定するContains, Equals, Replace, IndexOf, GetHashCode などが該当
CA1309: Ordinal StringComparison を使用するstring.Compare, EqualsOrdinal or OrdinalIgnoreCase の指定を強制
CA1310: 正確さのために StringComparison を指定するstring.Compare, CompareTo, IndexOf, StartsWith などが該当
CA1311: カルチャの指定またはインバリアント バージョンの使用ToLower, ToUpperToXxxInvariant 版を使用させる

Roslyn のアナライザーだけでは不十分なケースがあるので、Meziantou.Analyzer のルール も適用しています。

適用IdCategoryDescriptionSeverityIs enabledCode fix
MA0001UsageStringComparison is missingℹ️✔️✔️
MA0002UsageIEqualityComparer or IComparer is missing⚠️✔️✔️
MA0011UsageIFormatProvider is missing⚠️✔️
MA0021UsageUse StringComparer.GetHashCode instead of string.GetHashCode⚠️✔️✔️
MA0074UsageAvoid implicit culture-sensitive methods⚠️✔️✔️
MA0076DesignDo not use implicit culture-sensitive ToString in interpolated stringsℹ️✔️
MA0111PerformanceUse string.Create instead of FormattableStringℹ️✔️✔️

上記だけでは CurrentCulture が使用できてしまうため、BannedApiAnalyzers のルール で禁止しています。
下記の表でカバーできない部分を禁止するように設定しています。

適用IdCategoryEnabledSeverityCodeFix
RS0030ApiDesignTrue⚠️False

既定の検索と比較の種類 の一覧表をベースに結果をまとめてみました。いくつかのメソッドは追記しています。
“💡 不要” の表記の意味は、既定の動作が Ordinal なので警告が必要ない箇所を表します。

API既定の動作CA1304CA1305CA1307CA1309CA1310CA1311Meziantou
string.Compare⚠️ CurrentCulture⚠️ 該当⚠️ 該当⚠️ MA0074
string.Compare (ignoreCase)⚠️ CurrentCulture⚠️ 該当⚠️ 該当⚠️ 該当⚠️ MA0011
string.CompareOrdinal✅ Ordinal
string.CompareTo⚠️ CurrentCulture⚠️ 該当
string.Contains✅ Ordinal💡 不要💡 不要:ℹ️ MA0001
string.EndsWith (char)✅ Ordinal
string.EndsWith (string)⚠️ CurrentCulture⚠️ 該当⚠️ MA0074
string.Equals✅ Ordinal💡 不要⚠️ 該当💡 不要:ℹ️ MA0001
string.GetHashCode✅ Ordinal💡 不要💡 不要:ℹ️ MA0001
string.IndexOf (char)✅ Ordinal💡 不要:ℹ️ MA0001
string.IndexOf (string)⚠️ CurrentCulture⚠️ 該当⚠️ 該当⚠️ MA0074
string.IndexOfAny✅ Ordinal
string.LastIndexOf (char)✅ Ordinal
string.LastIndexOf (string)⚠️ CurrentCulture⚠️ 該当⚠️ MA0074
string.LastIndexOfAny✅ Ordinal
string.Replace (char)✅ Ordinal
string.Replace (string)✅ Ordinal💡 不要💡 不要:ℹ️ MA0001
string.Split✅ Ordinal
string.StartsWith (char)✅ Ordinal
string.StartsWith (string)⚠️ CurrentCulture⚠️ 該当⚠️ MA0074
string.ToLower⚠️ CurrentCulture⚠️ 該当⚠️ 該当⚠️ MA0011
string.ToLowerInvariant✅ InvariantCulture
string.ToUpper⚠️ CurrentCulture⚠️ 該当⚠️ 該当⚠️ MA0011
string.ToUpperInvariant✅ InvariantCulture
string.Trim✅ Ordinal
string.TrimEnd✅ Ordinal
string.TrimStart✅ Ordinal
string == string✅ Ordinal
string != string✅ Ordinal
API既定の動作CA1304CA1305CA1307CA1309CA1310CA1311Meziantou
string.Create⚠️ CurrentCulture⚠️ 該当⚠️ MA0011
string.Concat⚠️ CurrentCulture
string.Format⚠️ CurrentCulture⚠️ 該当⚠️ MA0011
string.Join⚠️ CurrentCulture
xxx.ToString⚠️ CurrentCulture⚠️ 該当⚠️ MA0011
xxx.Parse⚠️ CurrentCulture⚠️ 該当⚠️ MA0011
xxx.TryParse⚠️ CurrentCulture⚠️ MA0011
Convert.ToXxx⚠️ CurrentCulture⚠️ 該当⚠️ MA0011
StringBuilder().Append (value)⚠️ CurrentCulture
StringBuilder().Append
(AppendInterpolatedStringHandler)
⚠️ CurrentCulture⚠️ 該当⚠️ MA0011
StringBuilder().AppendLine⚠️ CurrentCulture⚠️ 該当⚠️ MA0011
StringBuilder().AppendFormat⚠️ CurrentCulture⚠️ 該当⚠️ MA0011
StringBuilder().AppendJoin⚠️ CurrentCulture
$"{}" 文字列補間⚠️ CurrentCulture⚠️ MA0076
FormattableString.CurrentCulture⚠️ CurrentCulture⚠️ MA0111
FormattableString.Invariant✅ InvariantCulture⚠️ MA0111
IComparer<string> 引数あり⚠️ Comparer<T>.Default⚠️ MA0002
IEqualityComparer<string> 引数ありEqualityComparer<T>.Default⚠️ MA0002
ResourceManager.GetObject⚠️ CurrentUICulture⚠️ 該当⚠️ MA0011
ResourceManager.GetString⚠️ CurrentUICulture⚠️ 該当⚠️ MA0011

💡 CA1307CA1310 の違い

  • CA1307: Equals, Contains も該当する
  • CA1310: Equals, Contains は既定値が Ordinal のため、該当しない

💡 CA1307MA0001 は不要

  • 既定値が Ordinal なメソッドに対しても警告が出てしまうため
  • 他のルールでカバーできるため

CA1305 の注意点

❗ 必ず InvariantCulture, Ordinal が保証されるわけではない点に注意

  • ToString("N0", (IFormatProvider?)null); のように IFormatProvider 引数に null が指定された場合は、CurrentCulture が適用される (警告なし)。
  • Order((IComparer<string>?)null); のように IComparer<T> 引数に null が指定された場合は、Comparer<T>.Default が適用される (警告なし)。
  • そもそも引数に IFormatProviderIComparer<T> が指定なく、内部で CurrentCulture が適用される (警告なし)。
  • 外部ライブラリのメソッドを使用している場合、外部ライブラリ内部でで CurrentCulture が適用される場合がある (コード分析対象外)。

検証ソースのメモです。

void Main(string x, string y, int n)
{
    string.Compare(x, y); // 1309, 1310, MA0074
    string.Compare(x, y, true); // 1304, 1309, 1310, MA0011
    string.Compare(x, y, true, CultureInfo.CurrentCulture); // 1309, RS0030
    string.Compare(x, y, true, CultureInfo.InvariantCulture); // 1309

    string.CompareOrdinal(x, y);

    x.CompareTo(y); // 1310
    x.CompareTo((object?)null); // 1310

    x.Contains('c'); // 1307, MA0001
    x.Contains('c', StringComparison.CurrentCulture); // RS0030
    x.Contains('c', StringComparison.Ordinal);
    x.Contains(y); // 1307, MA0001
    x.Contains(y, StringComparison.CurrentCulture); // RS0030
    x.Contains(y, StringComparison.Ordinal);

    x.EndsWith('c');
    x.EndsWith(y); // 1310, MA0001
    x.EndsWith(y, StringComparison.CurrentCulture); // RS0030
    x.EndsWith(y, StringComparison.Ordinal);
    x.EndsWith(y, ignoreCase: true, culture: null);

    x.Equals(y); // 1307, 1309, MA0001
    x.Equals((object?)null);
    x.Equals(y, StringComparison.CurrentCulture); // 1309, RS0030
    x.Equals(y, StringComparison.Ordinal);

    x.GetHashCode(); // 1307, MA0001
    x.GetHashCode(StringComparison.CurrentCulture); // RS0030
    x.GetHashCode(StringComparison.Ordinal);

    x.IndexOf('c'); // 1307, MA0001

    x.IndexOf(y); // 1310. MA0074
    x.IndexOf(y, StringComparison.CurrentCulture); // RS0030
    x.IndexOf(y, StringComparison.Ordinal);
    x.IndexOf(y, 0); // 1310. MA0074
    x.IndexOf(y, 0, StringComparison.CurrentCulture); // RS0030
    x.IndexOf(y, 0, StringComparison.Ordinal);
    x.IndexOf(y, 0, 0); // 1310. MA0074

    x.IndexOfAny(['c']);

    x.LastIndexOf('c');
    x.LastIndexOf(y); // 1310. MA0074
    x.LastIndexOf(y, StringComparison.CurrentCulture); // RS0030
    x.LastIndexOf(y, StringComparison.Ordinal);
    x.LastIndexOf(y, 0); // 1310. MA0074
    x.LastIndexOf(y, 0, StringComparison.CurrentCulture); // RS0030
    x.LastIndexOf(y, 0, StringComparison.Ordinal);
    x.LastIndexOf(y, 0, 0); // 1310. MA0074

    x.LastIndexOfAny(['c']);

    x.Replace('c', 'd');
    x.Replace("aa", "bb"); // 1307, MA0001
    x.Replace("aa", "bb", StringComparison.CurrentCulture); // RS0030
    x.Replace("aa", "bb", StringComparison.Ordinal);

    x.Split('c');
    x.Split(y);

    x.StartsWith('c');
    x.StartsWith(y); // 1310, MA0074
    x.StartsWith(y, StringComparison.CurrentCulture); // RS0030
    x.StartsWith(y, StringComparison.Ordinal);
    x.StartsWith(y, ignoreCase: true, culture: null);

    x.ToLower(); // 1304, 1311, MA0011
    x.ToLower(CultureInfo.CurrentCulture);
    x.ToLower(CultureInfo.InvariantCulture);

    x.ToLowerInvariant();

    x.ToUpper(); // 1304, 1311, MA0011
    x.ToUpper(CultureInfo.CurrentCulture);
    x.ToUpper(CultureInfo.InvariantCulture);

    x.ToUpperInvariant();

    x.Trim();
    x.TrimStart();
    x.TrimEnd();

    var eq = x == y;
    var ne = x != y;

    string.Create($"{n}"); // MA0076
    string.Create(null, $"{n}");
    string.Create(CultureInfo.InvariantCulture, $"{n}");

    string.Concat(n);

    string.Format("{0}", n); // 1305, MA0011
    string.Format(default(IFormatProvider), "{0}", n);

    string.Join(", ", new[] { n });

    x.ToString();
    x.ToString(CultureInfo.CurrentCulture); // RS0030
    x.ToString(CultureInfo.InvariantCulture);

    n.ToString(); // 1305, MA0011
    n.ToString("N0"); // 1305, MA0011
    n.ToString("N0", (IFormatProvider?)null);
    n.ToString(CultureInfo.CurrentCulture); // RS0030
    n.ToString(CultureInfo.InvariantCulture);

    (default(int?)).ToString(); // MA0011

    int.Parse("123"); // 1305, MA0011

    int.TryParse("123", out _); // MA0011

    Enum.Parse<DayOfWeek>("abc");

    Convert.ToString(x); // 1305 (string の ToString() なので警告不要)
    Convert.ToInt32(x); // 1305, MA0011
    Convert.ToSingle(x); // 1305, MA0011

    new StringBuilder().Append(n);
    new StringBuilder().Append($"{n}"); // CA1305, MA0011
    new StringBuilder().AppendLine($"{n}"); // CA1305, MA0011
    new StringBuilder().AppendLine(null, $"{n}");
    new StringBuilder().AppendLine(CultureInfo.InvariantCulture, $"{n}");
    new StringBuilder().AppendFormat("{0}", n); // CA1305, MA0011
    new StringBuilder().AppendFormat(CultureInfo.InvariantCulture, "{0}", n);
    new StringBuilder().AppendJoin(", ", new[] { n });

    Console.WriteLine($"{n:N0}"); // MA0076

    FormattableString.CurrentCulture($"{n}"); // MA0111
    FormattableString.Invariant($"{n}"); // MA0111

    new string[] { x, y }.OrderBy(x => x).ToArray(); // MA0002
    new string[] { x, y }.Order().ToArray(); // MA0002
    new int[] { n }.OrderBy(x => x.ToString()).ToArray(); // MA0002

    new HashSet<string>(); // MA0002
    new Dictionary<string, object>(); // MA0002
    new ConcurrentDictionary<string, object>(); // MA0002
    new SortedList<string, object>(); // MA0002

    new ResourceManager(typeof(object)).GetObject("x"); // 1304, MA0011
    new ResourceManager(typeof(object)).GetObject("x", CultureInfo.CurrentCulture); // RS0030
    new ResourceManager(typeof(object)).GetObject("x", CultureInfo.InvariantCulture);
    new ResourceManager(typeof(object)).GetString("x"); // 1304, MA0011
    new ResourceManager(typeof(object)).GetString("x", CultureInfo.CurrentCulture);  // RS0030
    new ResourceManager(typeof(object)).GetString("x", CultureInfo.InvariantCulture);

    Activator.CreateInstance(typeof(string));
    Activator.CreateInstance(typeof(string), BindingFlags.Default, null, null, CultureInfo.CurrentCulture); // RS0030

    x.AsSpan().IndexOf(y.AsSpan(), StringComparison.CurrentCulture); // RS0030
    x.AsSpan().IndexOf(y.AsSpan(), StringComparison.Ordinal);

    object obj = x;
    Console.WriteLine(obj);
}

あとがき

ライブラリ開発向けのコード分析のルール設定は、やりすぎると泥沼ですね。
string.JoinStringBuilder もほぼ使えなくなってしまいます…。
ZString もカルチャ指定できないようなので、使用できません。

文字列の比較、ソートについては Ordinal を指定すべきと思いますが、ToString() する部分はある程度は許容しても良い気がします。
そもそもライブラリ使用者側がインバリアントモードにするかどうか、CurrentCulture を自由に設定してもらえばいいわけなので、ライブラリ開発者が頑張りすぎる必要はないかもしれませんね。

感謝

++C++; // 未確認飛行 C ブログ

ブログ

ドキュメント

破壊的変更について

2024-09-13 (金)