Windows has supported case-sensitive file operations for a long time, but it was not easily accessible or enabled by default. Since Windows 10 (version 1803), it is possible to enable case sensitivity on a per-directory basis. This feature was introduced primarily to support the Windows Subsystem for Linux (WSL), but it can be used by any application.
In this post, we'll see how to enable case sensitivity for a directory using C#.
#Per-directory case sensitivity
When case sensitivity is enabled for a directory, all files and folders created within that directory are treated as case-sensitive. This means you can have files named test.txt and TEST.TXT in the same folder.
Note that this setting is not inherited by subdirectories created before the flag was set, but it is inherited by new subdirectories.
#Using fsutil
Before writing the C# code, let's see how to do it using the command line. Windows provides the fsutil tool to manage this setting.
To check the current status of a directory:
PowerShell
fsutil.exe file queryCaseSensitiveInfo <path>
To enable case sensitivity:
PowerShell
fsutil.exe file setCaseSensitiveInfo <path> enable
To disable case sensitivity:
PowerShell
fsutil.exe file setCaseSensitiveInfo <path> disable
#Using C#
To enable case sensitivity programmatically, we need to use the NtSetInformationFile function from ntdll.dll. This is a native API, so we need to use P/Invoke.
First, let's define the necessary constants and structures.
C#
using System.ComponentModel;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
internal static class NativeMethods
{
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew?WT.mc_id=DT-MVP-5003978
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern SafeFileHandle CreateFile(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile);
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntsetinformationfile?WT.mc_id=DT-MVP-5003978
[DllImport("ntdll.dll", ExactSpelling = true)]
public static extern int NtSetInformationFile(
SafeFileHandle FileHandle,
out IO_STATUS_BLOCK IoStatusBlock,
in FILE_CASE_SENSITIVE_INFORMATION FileInformation,
int Length,
FILE_INFORMATION_CLASS FileInformationClass);
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntqueryinformationfile?WT.mc_id=DT-MVP-5003978
[DllImport("ntdll.dll", ExactSpelling = true)]
public static extern int NtQueryInformationFile(
SafeFileHandle FileHandle,
out IO_STATUS_BLOCK IoStatusBlock,
out FILE_CASE_SENSITIVE_INFORMATION FileInformation,
int Length,
FILE_INFORMATION_CLASS FileInformationClass);
[StructLayout(LayoutKind.Sequential)]
public struct IO_STATUS_BLOCK
{
public IntPtr Status;
public IntPtr Information;
}
[StructLayout(LayoutKind.Sequential)]
public struct FILE_CASE_SENSITIVE_INFORMATION
{
public uint Flags;
}
public enum FILE_INFORMATION_CLASS
{
FileCaseSensitiveInformation = 71,
}
public const uint FILE_CS_FLAG_CASE_SENSITIVE_DIR = 0x00000001;
public const int STATUS_SUCCESS = 0;
public const uint FILE_READ_ATTRIBUTES = 0x0080;
public const uint FILE_WRITE_ATTRIBUTES = 0x0100;
public const uint FILE_SHARE_READ = 0x00000001;
public const uint FILE_SHARE_WRITE = 0x00000002;
public const uint FILE_SHARE_DELETE = 0x00000004;
public const uint OPEN_EXISTING = 3;
public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
}
Now we can write a method to enable case sensitivity for a directory.
C#
public static void SetCaseSensitivity(string directoryPath, bool enable)
{
// Open the directory with the required access rights
// We need WriteAttributes access to change the case sensitivity flag
// FILE_FLAG_BACKUP_SEMANTICS is required to open a directory
using var handle = NativeMethods.CreateFile(
directoryPath,
NativeMethods.FILE_WRITE_ATTRIBUTES,
NativeMethods.FILE_SHARE_READ | NativeMethods.FILE_SHARE_WRITE | NativeMethods.FILE_SHARE_DELETE,
IntPtr.Zero,
NativeMethods.OPEN_EXISTING,
NativeMethods.FILE_FLAG_BACKUP_SEMANTICS,
IntPtr.Zero);
if (handle.IsInvalid)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
var info = new NativeMethods.FILE_CASE_SENSITIVE_INFORMATION
{
Flags = enable ? NativeMethods.FILE_CS_FLAG_CASE_SENSITIVE_DIR : 0
};
var status = NativeMethods.NtSetInformationFile(
handle,
out _,
in info,
Marshal.SizeOf<NativeMethods.FILE_CASE_SENSITIVE_INFORMATION>(),
NativeMethods.FILE_INFORMATION_CLASS.FileCaseSensitiveInformation);
if (status != NativeMethods.STATUS_SUCCESS)
{
throw new InvalidOperationException($"Failed to set case sensitivity. NTSTATUS: {status}");
}
}
public static bool IsCaseSensitive(string directoryPath)
{
// Open the directory with the required access rights
// We need ReadAttributes access to query the case sensitivity flag
// FILE_FLAG_BACKUP_SEMANTICS is required to open a directory
using var handle = NativeMethods.CreateFile(
directoryPath,
NativeMethods.FILE_READ_ATTRIBUTES,
NativeMethods.FILE_SHARE_READ | NativeMethods.FILE_SHARE_WRITE | NativeMethods.FILE_SHARE_DELETE,
IntPtr.Zero,
NativeMethods.OPEN_EXISTING,
NativeMethods.FILE_FLAG_BACKUP_SEMANTICS,
IntPtr.Zero);
if (handle.IsInvalid)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
var status = NativeMethods.NtQueryInformationFile(
handle,
out _,
out var info,
Marshal.SizeOf<NativeMethods.FILE_CASE_SENSITIVE_INFORMATION>(),
NativeMethods.FILE_INFORMATION_CLASS.FileCaseSensitiveInformation);
if (status != NativeMethods.STATUS_SUCCESS)
{
throw new InvalidOperationException($"Failed to query case sensitivity. NTSTATUS: {status}");
}
return (info.Flags & NativeMethods.FILE_CS_FLAG_CASE_SENSITIVE_DIR) != 0;
}
Note that we use CreateFile instead of File.OpenHandle because we need to specify the FILE_FLAG_BACKUP_SEMANTICS flag to open a directory handle, which is not exposed in the FileOptions enum in .NET.
#Testing the code
Let's verify that it works.
C#
var directory = Path.Combine(Path.GetTempPath(), "CaseSensitiveTest_" + Guid.NewGuid());
Directory.CreateDirectory(directory);
try
{
Console.WriteLine($"Is case sensitive: {IsCaseSensitive(directory)}");
SetCaseSensitivity(directory, enable: true);
Console.WriteLine($"Is case sensitive: {IsCaseSensitive(directory)}");
// Create two files with the same name but different case
File.WriteAllText(Path.Combine(directory, "test.txt"), "lowercase");
File.WriteAllText(Path.Combine(directory, "TEST.TXT"), "uppercase");
Console.WriteLine($"File 1: {File.ReadAllText(Path.Combine(directory, "test.txt"))}");
Console.WriteLine($"File 2: {File.ReadAllText(Path.Combine(directory, "TEST.TXT"))}");
}
finally
{
Directory.Delete(directory, recursive: true);
}
Output:
Is case sensitive: False
Is case sensitive: True
File 1: lowercase
File 2: uppercase
#Additional resources
Do you have a question or a suggestion about this post? Contact me!