Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .openpublishing.redirection.standard.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@
"source_path_from_root": "/docs/standard/base-types/standard-timespan.md",
"redirect_url": "/dotnet/standard/base-types/standard-timespan-format-strings"
},
{
"source_path_from_root": "/docs/standard/base-types/string-comparison-net-5-plus.md",
"redirect_url": "/dotnet/standard/base-types/best-practices-strings"
},
{
"source_path_from_root": "/docs/standard/base-types/strip-characters.md",
"redirect_url": "/dotnet/standard/base-types/how-to-strip-invalid-characters-from-a-string"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ To fix this code by conducting an ordinal search instead of a culture-sensitive

You can run code analysis rules [CA1307: Specify StringComparison for clarity](../../../../fundamentals/code-analysis/quality-rules/ca1307.md) and [CA1309: Use ordinal StringComparison](../../../../fundamentals/code-analysis/quality-rules/ca1309.md) to find these call sites in your code.

For more information, see [Behavior changes when comparing strings on .NET 5+](../../../../standard/base-types/string-comparison-net-5-plus.md).
For more information, see [Best practices for comparing strings in .NET](../../../../standard/base-types/best-practices-strings.md).

### Currency symbol

Expand Down Expand Up @@ -84,4 +84,5 @@ No action is required on the part of the developer. However, if you wish to cont

## See also

- [Best practices for comparing strings in .NET](../../../../standard/base-types/best-practices-strings.md)
- [Globalization APIs use ICU libraries on Windows Server](../7.0/icu-globalization-api.md)
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ If you wish to continue using NLS globalization APIs, you can set a [runtime swi

## See also

- [Best practices for comparing strings in .NET](../../../../standard/base-types/best-practices-strings.md)
- [Globalization APIs use ICU libraries on Windows 10](../5.0/icu-globalization-api.md)
2 changes: 1 addition & 1 deletion docs/core/extensions/globalization-icu.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ By default, <xref:System.String.IndexOf(System.String)?displayProperty=nameWithT

You can run code analysis rules [CA1307: Specify StringComparison for clarity](../../fundamentals/code-analysis/quality-rules/ca1307.md) and [CA1309: Use ordinal StringComparison](../../fundamentals/code-analysis/quality-rules/ca1309.md) to find call sites in your code where the string comparison isn't specified or it isn't ordinal.

For more information, see [Behavior changes when comparing strings on .NET 5+](../../standard/base-types/string-comparison-net-5-plus.md).
For more information, see [Best practices for comparing strings in .NET](../../standard/base-types/best-practices-strings.md).

#### String.EndsWith

Expand Down
2 changes: 0 additions & 2 deletions docs/fundamentals/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -617,8 +617,6 @@ items:
href: ../standard/base-types/best-practices-strings.md
- name: Displaying and persisting formatted data
href: ../standard/base-types/best-practices-display-data.md
- name: Behavior changes in .NET 5+ (Windows)
href: ../standard/base-types/string-comparison-net-5-plus.md
- name: Basic string operations
items:
- name: Create new strings
Expand Down
103 changes: 103 additions & 0 deletions docs/standard/base-types/best-practices-strings.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ When you develop with .NET, follow these recommendations when you compare string
- Use overloads that explicitly specify the string comparison rules for string operations. Typically, this involves calling a method overload that has a parameter of type <xref:System.StringComparison>.
- Use <xref:System.StringComparison.Ordinal?displayProperty=nameWithType> or <xref:System.StringComparison.OrdinalIgnoreCase?displayProperty=nameWithType> for comparisons as your safe default for culture-agnostic string matching.
- Use comparisons with <xref:System.StringComparison.Ordinal?displayProperty=nameWithType> or <xref:System.StringComparison.OrdinalIgnoreCase?displayProperty=nameWithType> for better performance.
- Enable [code analyzers](../../fundamentals/code-analysis/overview.md) such as [CA1307](../../fundamentals/code-analysis/quality-rules/ca1307.md), [CA1309](../../fundamentals/code-analysis/quality-rules/ca1309.md), and [CA1310](../../fundamentals/code-analysis/quality-rules/ca1310.md) to detect potentially incorrect string comparisons in your code.
- Use string operations that are based on <xref:System.StringComparison.CurrentCulture?displayProperty=nameWithType> when you display output to the user.
- Use the non-linguistic <xref:System.StringComparison.Ordinal?displayProperty=nameWithType> or <xref:System.StringComparison.OrdinalIgnoreCase?displayProperty=nameWithType> values instead of string operations based on <xref:System.Globalization.CultureInfo.InvariantCulture%2A?displayProperty=nameWithType> when the comparison is linguistically irrelevant (symbolic, for example).
- Use the <xref:System.String.ToUpperInvariant%2A?displayProperty=nameWithType> method instead of the <xref:System.String.ToLowerInvariant%2A?displayProperty=nameWithType> method when you normalize strings for comparison.
Expand Down Expand Up @@ -90,6 +91,28 @@ However, evaluating two strings for equality or sort order doesn't yield a singl

In addition, string comparisons using different versions of .NET or using .NET on different operating systems or operating system versions may return different results. For more information, see [Strings and the Unicode Standard](xref:System.String#Unicode).

### Globalization libraries: .NET vs .NET Framework

.NET and .NET Framework use different globalization libraries, which can affect string comparison behavior:

- **.NET** uses the [International Components for Unicode (ICU)](https://icu.unicode.org/) libraries for globalization functionality across all platforms (Windows, Linux, macOS). ICU is an industry-standard Unicode implementation that provides consistent behavior across operating systems.
- **.NET Framework** uses [National Language Support (NLS)](/windows/win32/intl/national-language-support) APIs on Windows, which is a Windows-specific globalization system.
Comment on lines +98 to +99
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section states that .NET uses ICU “across all platforms (Windows, Linux, macOS)”. On Windows, .NET’s default globalization library depends on the OS version and runtime version (for example, ICU on Windows 10 19H1+ starting in .NET 5, ICU on Windows Server 2019 starting in .NET 7). Consider qualifying this statement (and/or linking to the ICU-on-Windows details) to avoid implying ICU is always the default on Windows.

Copilot uses AI. Check for mistakes.

Because ICU and NLS implement different logic in their linguistic comparers, the same string comparison code can produce different results depending on which runtime you're using. Consider the following example that formats a number as currency using a German culture:

:::code language="csharp" source="./snippets/best-practices-strings/csharp/icu-demo/Program.cs":::
:::code language="vb" source="./snippets/best-practices-strings/vb/icu-demo/Program.vb":::

When running on .NET Framework, the output is `"100,00 €"` (using the euro symbol). On .NET, the output is `"100,00 ¤"` (using the international currency symbol). This difference occurs because ICU treats currency as a property of a country or region, not just a language, whereas the language-only German culture (`"de"`) doesn't specify a country.

Comment on lines +106 to +107
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The currency output difference described here is platform- and version-specific (it’s documented as a behavioral change on certain Windows versions when ICU is used). Consider qualifying “On .NET” to the specific conditions (for example, .NET 5+ on Windows 10 19H1+ / .NET 7+ on Windows Server 2019) so readers on other OSes or Windows versions don’t get confused by different output.

Copilot uses AI. Check for mistakes.
If your application requires the older NLS behavior when running on .NET, you can enable it through [runtime configuration](../../core/runtime-config/globalization.md#nls). However, for new applications, we recommend using explicit `StringComparison` parameters to make string comparison behavior clear and consistent.
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To align with the docs writing style guidance (avoid “we” voice), consider rephrasing “we recommend using…” to a direct instruction (for example, “use explicit StringComparison parameters…”).

Suggested change
If your application requires the older NLS behavior when running on .NET, you can enable it through [runtime configuration](../../core/runtime-config/globalization.md#nls). However, for new applications, we recommend using explicit `StringComparison` parameters to make string comparison behavior clear and consistent.
If your application requires the older NLS behavior when running on .NET, you can enable it through [runtime configuration](../../core/runtime-config/globalization.md#nls). For new applications, use explicit `StringComparison` parameters to make string comparison behavior clear and consistent.

Copilot uses AI. Check for mistakes.

For detailed information about behavior changes and migration guidance, see:

- [Globalization APIs use ICU libraries on Windows 10](../../core/compatibility/globalization/5.0/icu-globalization-api.md)
- [Globalization APIs use ICU libraries on Windows Server 2019](../../core/compatibility/globalization/7.0/icu-globalization-api.md)
- [.NET globalization and ICU](../../core/extensions/globalization-icu.md)

### String comparisons that use the current culture

One criterion involves using the conventions of the current culture when comparing strings. Comparisons that are based on the current culture use the thread's current culture or locale. If the culture isn't set by the user, it defaults to the operating system's setting. You should always use comparisons that are based on the current culture when data is linguistically relevant, and when it reflects culture-sensitive user interaction.
Expand Down Expand Up @@ -131,6 +154,13 @@ In this case, because "file:" is meant to be interpreted as a non-linguistic, cu
:::code language="csharp" source="./snippets/best-practices-strings/csharp/turkish/Program.cs" id="ordinal":::
:::code language="vb" source="./snippets/best-practices-strings/vb/turkish/Program.vb" id="ordinal":::

Another security-sensitive scenario involves filtering or validation code. Consider the following example that attempts to detect HTML-sensitive characters:

:::code language="csharp" source="./snippets/best-practices-strings/csharp/security-filtering/Program.cs":::
:::code language="vb" source="./snippets/best-practices-strings/vb/security-filtering/Program.vb":::

The incorrect version uses the default linguistic search, which might not find literal `'<'` or `'&'` characters in all cultures. The corrected version explicitly uses `StringComparison.Ordinal` to ensure the literal characters are matched. For filtering, validation, and security-sensitive comparisons, always use ordinal comparison.

### Ordinal string operations

Specifying the <xref:System.StringComparison.Ordinal?displayProperty=nameWithType> or <xref:System.StringComparison.OrdinalIgnoreCase?displayProperty=nameWithType> value in a method call signifies a non-linguistic comparison in which the features of natural languages are ignored. Methods that are invoked with these <xref:System.StringComparison> values base string operation decisions on simple byte comparisons instead of casing or equivalence tables that are parameterized by culture. In most cases, this approach best fits the intended interpretation of strings while making code faster and more reliable.
Expand Down Expand Up @@ -168,6 +198,30 @@ Both <xref:System.StringComparison.Ordinal?displayProperty=nameWithType> and <xr

Ordinal semantics are the default for <xref:System.String.Equals%2A?displayProperty=nameWithType> overloads that don't include a <xref:System.StringComparison> argument (including the equality operator). In any case, we recommend that you call an overload that has a <xref:System.StringComparison> parameter.

#### Linguistic comparison and collation elements

Unlike ordinal comparisons, *linguistic* comparisons decompose strings into *collation elements* rather than individual characters. A collation element is a linguistic unit that may consist of one or more characters. For example, the accented character "é" can be represented as:

- A single character: `'\u00E9'`
- A base character plus combining accent: `'e'` + `'\u0301'`

When performing linguistic comparisons, these different representations are treated as semantically equivalent. This behavior is important for Unicode normalization but can produce unexpected results if you're expecting exact character-by-character matching.

The following example demonstrates how Unicode normalization affects string searching and comparison:

:::code language="csharp" source="./snippets/best-practices-strings/csharp/collation-elements/Program.cs":::
:::code language="vb" source="./snippets/best-practices-strings/vb/collation-elements/Program.vb":::

As the example shows, ordinal comparison requires an exact byte-for-byte match, while linguistic comparison understands that `"\u00E9"` and `"e\u0301"` represent the same accented character.

#### Culture-aware linguistic comparison

Culture-aware comparisons extend linguistic comparison with culture-specific rules. For example, in the Hungarian alphabet, when "dz" appears as consecutive characters, it's treated as a single letter distinct from "d" or "z". This means that a Hungarian culture-aware comparer treats "dz" as a single collation element.

The example in the preceding code snippet also demonstrates this behavior: when using the Hungarian culture (`"hu-HU"`), the string `"endz"` doesn't end with `"z"` because "dz" is considered a single letter. When using the invariant culture, `"endz"` does end with `"z"` because the characters are treated independently.

Be aware that linguistic and culture-aware comparers can undergo behavioral adjustments over time as languages evolve and Unicode standards are updated. Ordinal comparisons never change because they perform exact binary matching.

### String operations that use the invariant culture

Comparisons with the invariant culture use the <xref:System.Globalization.CultureInfo.CompareInfo%2A> property returned by the static <xref:System.Globalization.CultureInfo.InvariantCulture%2A?displayProperty=nameWithType> property. This behavior is the same on all systems; it translates any characters outside its range into what it believes are equivalent invariant characters. This policy can be useful for maintaining one set of string behavior across cultures, but it often provides unexpected results.
Expand Down Expand Up @@ -278,6 +332,55 @@ if (Commands.Contains(command))

In .NET 9, `SearchValues` was extended to support searching for substrings within a larger string. For an example, see [`SearchValues` expansion](../../core/whats-new/dotnet-9/libraries.md#searchvalues-expansion).

## Default search and comparison types

The following tables list the default search and comparison types for various string and string-like APIs. If the caller provides an explicit `CultureInfo` or `StringComparison` parameter, that parameter overrides the default behavior shown here.

### String methods

| API | Default behavior | Remarks |
|---------------------------|------------------|------------------------------------------|
| `string.Compare` | CurrentCulture | |
| `string.CompareTo` | CurrentCulture | |
| `string.Contains` | Ordinal | |
| `string.EndsWith` | Ordinal | (when the first parameter is a `char`) |
| `string.EndsWith` | CurrentCulture | (when the first parameter is a `string`) |
| `string.Equals` | Ordinal | |
| `string.GetHashCode` | Ordinal | |
| `string.IndexOf` | Ordinal | (when the first parameter is a `char`) |
| `string.IndexOf` | CurrentCulture | (when the first parameter is a `string`) |
| `string.IndexOfAny` | Ordinal | |
| `string.LastIndexOf` | Ordinal | (when the first parameter is a `char`) |
| `string.LastIndexOf` | CurrentCulture | (when the first parameter is a `string`) |
| `string.LastIndexOfAny` | Ordinal | |
| `string.Replace` | Ordinal | |
| `string.Split` | Ordinal | |
| `string.StartsWith` | Ordinal | (when the first parameter is a `char`) |
| `string.StartsWith` | CurrentCulture | (when the first parameter is a `string`) |
| `string.ToLower` | CurrentCulture | |
| `string.ToLowerInvariant` | InvariantCulture | |
| `string.ToUpper` | CurrentCulture | |
| `string.ToUpperInvariant` | InvariantCulture | |
| `string.Trim` | Ordinal | |
| `string.TrimEnd` | Ordinal | |
| `string.TrimStart` | Ordinal | |
| `string == string` | Ordinal | |
| `string != string` | Ordinal | |

### MemoryExtensions methods

Unlike `string` APIs, all `MemoryExtensions` APIs perform *Ordinal* searches and comparisons by default, with the following exceptions:

| API | Default behavior | Remarks |
|-------------------------------------|------------------|---------------------------------------------|
| `MemoryExtensions.ToLower` | CurrentCulture | (when passed a null `CultureInfo` argument) |
| `MemoryExtensions.ToLowerInvariant` | InvariantCulture | |
| `MemoryExtensions.ToUpper` | CurrentCulture | (when passed a null `CultureInfo` argument) |
| `MemoryExtensions.ToUpperInvariant` | InvariantCulture | |

> [!IMPORTANT]
> When converting code from consuming `string` to consuming `ReadOnlySpan<char>`, behavioral changes may be introduced because `MemoryExtensions` methods default to ordinal comparison while some `string` methods default to culture-sensitive comparison. Always specify an explicit `StringComparison` parameter to avoid unexpected behavior changes.

## Methods that perform string comparison indirectly

Some non-string methods that have string comparison as a central operation use the <xref:System.StringComparer> type. The <xref:System.StringComparer> class includes six static properties that return <xref:System.StringComparer> instances whose <xref:System.StringComparer.Compare%2A?displayProperty=nameWithType> methods perform the following types of string comparisons:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Globalization;

// Demonstrate Unicode normalization with résumé
Console.WriteLine("=== Unicode Normalization Example ===");
Console.WriteLine("resume".IndexOf("e", StringComparison.Ordinal)); // prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("e", StringComparison.Ordinal)); // prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf("e", StringComparison.Ordinal)); // prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf("e", StringComparison.Ordinal)); // prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf("e", StringComparison.Ordinal)); // prints '1'

// Linguistic comparison
Console.WriteLine("\n=== Linguistic Comparison Example ===");
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("e")); // prints '-1' (not found)
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("\u00E9")); // prints '1'
Console.WriteLine("\u00E9".IndexOf("e\u0301")); // prints '0'

// Hungarian culture-aware comparison
Console.WriteLine("\n=== Hungarian Culture-Aware Example ===");
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("hu-HU");
Console.WriteLine("endz".EndsWith("z")); // Prints 'False'

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Console.WriteLine("endz".EndsWith("z")); // Prints 'True'
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These snippet projects in this folder consistently target net8.0 and enable common settings (for example, Nullable and ImplicitUsings) (see .../csharp/turkish/turkish.csproj). This new project targets net9.0 and omits those settings, which makes the snippets inconsistent and harder to maintain. Consider matching the existing snippet project conventions in this directory.

Suggested change
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or do you want to upgrade to .NET 10?

<RootNamespace>collation_elements</RootNamespace>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;
using System.Globalization;

System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("de");
string text = string.Format("{0:C}", 100);
Console.WriteLine($"Currency formatted: {text}");

// Output on .NET Framework (NLS): "100,00 €"
// Output on .NET (ICU): "100,00 ¤"
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These snippet projects in this folder consistently target net8.0 and enable common settings (for example, Nullable and ImplicitUsings) (see .../csharp/turkish/turkish.csproj). This new project targets net9.0 and omits those settings, which makes the snippets inconsistent and harder to maintain. Consider matching the existing snippet project conventions in this directory.

Suggested change
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>

Copilot uses AI. Check for mistakes.
<RootNamespace>icu_demo</RootNamespace>
</PropertyGroup>

</Project>
Loading
Loading