How to correctly retrieve paths to all special Windows folders - like the user's Downloads folder
Introduction
Since Windows 98, users have special folders in their home directory, called "My Documents", "My Music", "My Pictures" or "My Videos", properly called "Known Folders". These barely changed up to XP, and their path is easily retrieved with .NET: Calling the System.Environment.GetFolderPath function and passing in an enum
value of System.Environment.SpecialFolder.
However, newer folders introduced since Windows Vista are not listed in the SpecialFolder enum
and cannot be retrieved that way. .NET was not updated to mirror the additions to the user home directory (and several other "special" folders), and people (including me) made hacky and wrong attempts in finding the paths of the other folders. This article details why these attempts are incorrect and finally presents the correct way to get the new, "special" folder paths.
.NET 8 to Add Support for These Folders
15+ years after introducing these folders, adding support for querying them (cross-platform) by extending the above mentioned SpecialFolder enum
is being considered for the upcoming .NET 8. Feel free to check out my API proposal on the dotnet GitHub repository to show support with a reaction or to see how exactly this will be implemented.
Background
I typically see these two wrong attempts to retrieve the path for - let's say - the Downloads folder.
Wrong Approach #1: Append the Folder Name to the User Home Directory
The seemingly easiest way to retrieve the Downloads folder path consists of appending the special folder name to the user home path (in which the special folders are found by default):
string userRoot = System.Environment.GetEnvironmentVariable("USERPROFILE");
string downloadFolder = Path.Combine(userRoot, "Downloads");
Since Windows Vista and newer versions use English folder names internally and the names displayed in the File Explorer are just virtually localized versions of them, this should work, shouldn't it?
No. This solution does not work if the user redirected the path of the Downloads folder. It is possible to change the path in the Downloads folder properties and simply choose another location for it. It may not be in the user root directory at all, it can be anywhere - mine, for example, are all on the D: partition.
Wrong Approach #2: Use the "Shell Folders" Registry Key
In the next attempt, developers searched through the registry for a key containing the redirected folder path. Windows must store this information somewhere, right?
Eventually, they found "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders". The key looks really promising as it contains all the paths to the user folders, even with redirected paths (mine are all on the D: partition as you can see):
But wait, it also contains an ominous key with the name "!Do not use this registry key" and the value "Use the SHGetFolderPath
or SHGetKnownFolderPath
function instead". This key was put there by Microsoft developer Raymond Chen, who had the right feeling that people would continue to abuse this key to get the folder path in the future. He wrote articles about it here and here.
To summarize, using this key was only acceptable in a Windows 95 beta version, and no moment later. Microsoft noticed it would not be flexible enough to keep all the information about shell folders, it did not respect roaming user profiles, and so on. A WinAPI
function was created instead, but the key was left in the registry "temporary" to not break literally four programs designed in the Win95 beta period in the RTM. It was never removed afterwards since more developers found this key and started to rely on it, and deleting it now would make even more programs incompatible than the original four.
So, don't use the values there. They're not guaranteed to be right, or to even exist, and Raymond Chen will probably hate you for doing so - and you better not get into trouble with "Microsoft's Chuck Norris". Even I ran into the "Shell Folders" key trap once and posted this as a "solution" in a StackOverflow answer (which I have updated meanwhile).
The Correct Solution
Being good developers, we follow the key's advice, and P/Invoke said SHGetKnownFolderPath function. Since P/Invoke can be a little nasty, let's carve it out together.
First, we define how we eventually want to retrieve the path. The simplest solution should be a static
method accepting a parameter of our own, extended "special" folder enum
:
string downloadsFolder = KnownFolders.GetPath(KnownFolder.Downloads);
enum KnownFolder
{
Documents,
Downloads,
Music,
Pictures,
SavedGames,
}
static class KnownFolders
{
public static string GetPath(KnownFolder folder)
{
}
}
Understanding the Native Method
Next, we have to understand the SHGetKnownFolderPath
method, as specified in the Microsoft documentation. I quoted the most important parts below:
HRESULT SHGetKnownFolderPath(
[in] REFKNOWNFOLDERID rfid,
[in] DWORD dwFlags,
[in, optional] HANDLE hToken,
[out] PWSTR *ppszPath
);
[in] REFKNOWNFOLDERID rfid
: "A reference to the KNOWNFOLDERID that identifies the folder."
KNOWNFOLDERID
is actually a GUID. The available GUIDs are found here. We have to map them to our KnownFolder
enum
values. For simplicity, we will only use some of them and a dictionary in our static
class.
[in] DWORD dwFlags
: "Flags that specify special retrieval options. This value can be 0; otherwise, one or more of the KNOWN_FOLDER_FLAG values."
For simplicity, we will indeed use 0
, though you can adjust and optimize the behavior to meet your expectations. KF_FLAG_DONT_VERIFY
may be useful if you don't need to ensure if the folder is created if it does not exist yet, an operation that can be slow if the folder was relocated to network drives.
[in, optional] HANDLE hToken
: "If this parameter is NULL, which is the most common usage, the function requests the known folder for the current user."
For simplicity, we will only bother about the executing user's folder paths. You can pass in the handle of a System.Security.Principal.WindowsIdentity.AccessToken
instead to impersonate another user.
[out] PWSTR *ppszPath
: "When this method returns, contains the address of a pointer to a null-terminated Unicode string that specifies the path of the known folder. The calling process is responsible for freeing this resource once it is no longer needed by calling CoTaskMemFree, whether SHGetKnownFolderPath succeeds or not."
This will eventually return the path we are interested in.
Calling the Native Method in C#
To invoke this method in C#, the following import can be used. The list below explains how this was determined. You can skip it if you are not interested in how P/Invoke works under the hood.
[DllImport("shell32", CharSet = CharSet.Unicode,
ExactSpelling = true, PreserveSig = false)]
private static extern string SHGetKnownFolderPath(
[MarshalAs(UnmanagedType.LPStruct)] Guid rfid,
uint dwFlags, nint hToken = default);
- The documentation states that the method resides in the Shell32.dll module, so we provide this file name to the
DllImport
attribute (you do not have to specify the file extension). - Since the path returned to us as an
[out]
parameter is a unicode (UTF16) string, we ensure to override C#'s default CharSet
to CharSet.Unicode
. This allows us to directly convert PWSTR *ppszPath
to a string
, with the marshaller freeing the native memory allocated for it - note that this only works because the marshaller assumes such memory blocks were previously allocated with CoTaskMemAlloc
(which is what the WinAPI method does), always calling CoTaskMemFree
on them for us. - We provide
ExactSpelling = true
since there are no A
or W
versions of the method, and prevent the runtime from searching for such. - Using
PreserveSig = false
allows us to convert any HRESULT
failure codes returned by the method into .NET exceptions. It also makes the method return void
, but we can further change this to return the last [out]
parameter, in this case, our string
. - We could map
REFKNOWNFOLDERID
GUID
to ref Guid
, but to not have to deal with references, we can instruct the marshaller to do this for us when we provide a Guid
"by value" with [MarshalAs(UnmanagedType.LPStruct)]
. - The next parameter is a
DWORD
which, in this case, could map to a .NET enum
consisting of the available flags, but since we are not interested in them, we just use a raw uint
accordingly. - A
HANDLE
is the size of a native integer, so we use C# 9's new nint
for this - alternatively, you can still use an IntPtr
. The parameter is optional, and even if we do not require to do so, we state the same with = default
.
Putting It All Together
Filling out the implementation of our static
class, we will end up with the following:
using System.Runtime.InteropServices;
static class KnownFolders
{
private static readonly Dictionary<KnownFolder, Guid> _knownFolderGuids = new()
{
[KnownFolder.Documents] = new("FDD39AD0-238F-46AF-ADB4-6C85480369C7"),
[KnownFolder.Downloads] = new("374DE290-123F-4565-9164-39C4925E467B"),
[KnownFolder.Music] = new("4BD8D571-6D19-48D3-BE97-422220080E43"),
[KnownFolder.Pictures] = new("33E28130-4E1E-4676-835A-98395C3BC3BB"),
[KnownFolder.SavedGames] = new("4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4"),
};
public static string GetPath(KnownFolder folder)
{
return SHGetKnownFolderPath(_knownFolderGuids[folder], 0);
}
[DllImport("shell32", CharSet = CharSet.Unicode,
ExactSpelling = true, PreserveSig = false)]
private static extern string SHGetKnownFolderPath(
[MarshalAs(UnmanagedType.LPStruct)]
Guid rfid, uint dwFlags, nint hToken = default);
}
Using the Code
We've made it pretty simple to print all the known folder paths exposed through our enum
:
foreach (KnownFolder knownFolder in Enum.GetValues<KnownFolder>())
{
try
{
Console.Write($"{knownFolder}: ");
Console.WriteLine(KnownFolders.GetPath(knownFolder));
}
catch (Exception ex)
{
Console.WriteLine($"<Exception> {ex.Message}");
}
Console.WriteLine();
}
A very loose try catch
is required as P/Invoke converts HRESULT
s to a wide range of exceptions. You may run into FileNotFoundException
s in case a folder is not available on a system, but that will not typically happen for user folders.
Points of Interest
We have not touched all related known folder functionality, and mentioned some possible optimization potential:
- Add the remaining folders to our
enum
. - Wrapping exceptions in a custom
KnownFolderException
to catch them more specifically. - Using attributes instead of a
dictionary
to assign the GUIDs to each KnownFolder
enum
value and retrieving them via reflection. - Changing the path of a known folder with
SHSetKnownFolderPath
. - Querying the paths of another user by passing in an identity access token handle.
- Retrieving an
IShellItem
COM object instance through SHGetKnownFolderItem
and extracting the user friendly folder name with its GetDisplayName
method. - Adding compatibility for Windows XP operating systems and earlier, which do not have the
SHGetKnownFolderPath
function, creating a wrapper capable of retrieving paths for them as well, possibly just falling back to System.Environment.GetFolderPath()
. - Use CsWin32 to automatically generate P/Invoke signatures from WinAPI metadata. Note that its signatures use raw
unsafe
semantics, differing from the signature determined in this article.
If you liked this article, feel free to upvote my answer on StackOverflow.
NuGet Package
I created a NuGet package providing most of the functionality discussed in the previous paragraph. More information about it on its project site. Note that due to the additional features, the API is slightly different, check the README for more details.
History
- 19th April, 2022 - Rewrite of code samples and most paragraphs
- 18th December, 2018 - Updated NuGet project site link
- 11th June, 2018 - Free string memory, updated sample, reworded some sentences
- 21st March, 2016 - Added note about newly created NuGet package
- 20th February, 2015 - First release