Numeric sorting, also often known as natural sorting, organizes strings in a human-logical order, considering numbers atomically. With default sorting algorithms, “v10” would be sorted before “v3” because the comparison happens character by character, while in numeric sorting, “v3” would be sorted before “v10” because “3” is considered smaller than “10”. This type of sorting is known to be more human-friendly and is used in applications like Windows Explorer. Let’s explore three ways to implement it in .NET.
Using .NET 10’s new CompareOptions.NumericOrdering flag
If you’re reading this post from the future (after mid-November 2025), you can use the new CompareOptions.NumericOrdering
flag introduced in .NET 10 through PR #109861. For example, you can create a custom StringComparer
:
var comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
string[] tags = ["v1.0.11", "v1.0.2", "v1.0.9"];
Array.Sort(tags, comparer);
Console.WriteLine(string.Join(", ", tags)); // prints v1.0.2, v1.0.9, v1.0.11
There isn’t a public singleton StringComparer
for numeric sorting, like StringComparer.Ordinal
or StringComparer.InvariantCulture
. This is because beyond numeric handling, you might want to customize how other characters are treated, such as case sensitivity, accents, or culture-specific rules. There are many combinations.
Additionally, it’s worth noting that the CompareOptions.NumericOrdering
flag is incompatible with the CompareOptions.Ordinal
and CompareOptions.OrdinalIgnoreCase
flags. As explained in this issue comment, these flags were meant to be used exclusively, and significant changes would be required to support combining them.
Using the NaturalSort.Extension package
If .NET 10 isn’t available yet, or you’re targeting an older framework, you can use the NaturalSort.Extension package by Tomáš Pažourek. This package provides a NaturalSortComparer
that wraps another comparer, allowing you to combine numeric sorting with other comparison options, including ordinal.
var comparer = new NaturalSortComparer(StringComparer.OrdinalIgnoreCase);
string[] tags = ["v1.0.11", "v1.0.2", "v1.0.9"];
Array.Sort(tags, comparer);
Console.WriteLine(string.Join(", ", tags)); // also prints v1.0.2, v1.0.9, v1.0.11
The test suite ensures confidence in the implementation’s quality. Performance-wise, you’ll need to evaluate its suitability for your use case. The implementation is a single file that can easily be included in your project thanks to its permissive MIT license.
Using the same Windows API as Windows Explorer
If your application targets Windows exclusively and you want to replicate the sorting behavior of Windows Explorer, you can use the StrCmpLogicalW function via P/Invoke. StrCmpLogicalW
is case-insensitive and doesn’t provide the same level of customization as the previous two methods.
internal sealed class WindowsNumericStringComparer : IComparer<string?>
{
public int Compare(string? x, string? y)
{
if (x == y) return 0;
if (x == null) return -1;
if (y == null) return 1;
return NativeMethods.StrCmpLogical(x, y);
}
private static class NativeMethods
{
[DllImport("shlwapi.dll", EntryPoint = "StrCmpLogicalW", ExactSpelling = true, CharSet = CharSet.Unicode)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
public static extern int StrCmpLogical(string psz1, string psz2);
}
}
var comparer = new WindowsNumericStringComparer();
string[] tags = ["v1.0.11", "v1.0.2", "v1.0.9"];
Array.Sort(tags, comparer);
Console.WriteLine(string.Join(", ", tags));
If you’re using this approach, consider leveraging the Microsoft.Windows.CsWin32 package for automatic generation of P/Invoke signatures, or
LibraryImportAttribute
, which is also based on source generators.
Photo by Kaboompics on pexels.com