Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Getting All Special Folders in .NET

4.91/5 (122 votes)
27 Sep 2022MIT8 min read 205.1K   6.5K  
Retrieving the path to the new user folders like Downloads, Saved Games or Searches
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):

C#
// This returns something like C:\Users\Username:
string userRoot = System.Environment.GetEnvironmentVariable("USERPROFILE");
// Now let's get C:\Users\Username\Downloads:
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):

Image 1

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:

C#
string downloadsFolder = KnownFolders.GetPath(KnownFolder.Downloads);

enum KnownFolder
{
    Documents,
    Downloads,
    Music,
    Pictures,
    SavedGames,
    // ...
}

static class KnownFolders
{
    public static string GetPath(KnownFolder folder)
    {
        // TODO: Implement
    }
}

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:

C
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.

C#
[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:

C#
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:

C#
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 HRESULTs to a wide range of exceptions. You may run into FileNotFoundExceptions 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

License

This article, along with any associated source code and files, is licensed under The MIT License