I posted this question purely because I figured out a good answer, that doesn't require loading native Win32 DLLs via P/Invoke, doesn't require setting any registry keys, doesn't require falling back to calling any native executables, and appears to work all the way back to Windows Server 2012R2 (I don't have anything older to test on).
The short answer:
Run the following in your current PowerShell session or prepend it in your scripts (this may not work in future versions of PowerShell/NET, as it uses internal and undocumented APIs):
[System.AppContext]::SetSwitch('Switch.System.IO.UseLegacyPathHandling', $false)
[System.AppContext]::SetSwitch('Switch.System.IO.BlockLongPaths', $false)
[System.Type]::GetType('System.AppContextSwitches').GetField('_useLegacyPathHandling', [System.Reflection.BindingFlags]::Static -bor [System.Reflection.BindingFlags]::NonPublic).SetValue($null, 0)
[System.Type]::GetType('System.AppContextSwitches').GetField('_blockLongPaths', [System.Reflection.BindingFlags]::Static -bor [System.Reflection.BindingFlags]::NonPublic).SetValue($null, 0)
The explanation:
After poking around at a decompiled view of "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.dll
" in ILSpy for a couple of hours, I determined that .NET APIs like System.IO.DirectoryInfo
call the internal static method System.IO.Path.NormalizePath
, which performs initial validation of the file path you supply.
This then checks an internal boolean System.AppContextSwitches.UseLegacyPathHandling
, and if this is set to true
, it will simply refuse to accept paths longer than 260 characters, even if you use the documented "\\?\
" prefix!
The hard bit is then disabling "UseLegacyPathHandling".
Normally, you'd add a line like <AppContextSwitchOverrides value="Switch.System.IO.UseLegacyPathHandling=false" />
to your application's "app.config
" (to be honest, this probably also works for PowerShell), but if you want to be able to do this at runtime without modifying the system you're on, it turns out to be a pain.
Ideally, simply running [System.AppContext]::SetSwitch('Switch.System.IO.UseLegacyPathHandling', $false)
should work, except that .NET tries to be clever and cache the values of these switches, such that if they've ever been accessed already, they're essentially stuck at whatever value they were set to when they were first accessed.
This is where it gets ugly: In the snippet above, I reset the cached values back to 0
, which the internal System.AppContextSwitches.GetCachedSwitchValue method (technically actually GetCachedSwitchValueInternal
) uses to signify that the value of the switch hasn't been cached, and should be looked up via the public method AppContext.TryGetSwitch
.
Only then - in my experience - does it actually honour the values you set via System.AppContext.SetSwitch
(the syntax is [System.AppContext]::SetSwitch
in PowerShell).
Enjoy!