Click here to Skip to main content
13,195,710 members (55,975 online)
Click here to Skip to main content
Add your own
alternative version

Stats

36.4K views
2.1K downloads
63 bookmarked
Posted 20 Feb 2015

Getting All "Special Folders" in .NET

, 21 Mar 2016
Rate this:
Please Sign up or sign in to vote.
Retrieving the path to the new user folders like Downloads, Saved Games or Searches

Introduction

Since many years, back to Windows 98 as far as I remember, users have special folders in their home directory, called "My Documents", "My Music", "My Pictures" or "My Videos". These were kept relatively untouched up to XP, and their path was retrieved easily: Simply calling the System.Environment.GetFolderPath() function and passing an enumeration value of System.Environment.SpecialFolder was enough, since the enumeration contains entries for MyDocumentsMyMusic, MyPictures and so on.

However, the newer folders which exist since Windows Vista, are not listed in the enumeration and cannot be retrieved that way. The reason for this is that the .NET framework was not updated to mirror the changes in the user home directory, and people started to use hacky and even wrong solutions to get the folder paths to the other folders. This article clears up all the bad solutions and presents the correct way to get the new, "special" folder paths.

Background

In the past, most people went with these two and wrong solutions to get the path for - let's say - the download folder.

Wrong Solution #1: Be extremely lazy and get the user home directory root, manually appending the folder name

This is the most horrible solution of the two. Developers started to get the user home root, like this:

string userRoot = System.Environment.GetEnvironmentVariable("USERPROFILE");

This returns something like "C:\Users\Username" in the most cases. In the next step, the folder name was appended, mostly hardcoded, and if you're lucky, it was done with Path.Combine() rather than with a hardcoded back slash:

string downloadFolder = Path.Combine(userRoot, "Downloads");

Good enough, did the developers say, since Windows Vista and newer versions use English folder names on the file system, and the names displayed in Explorer are just virtually localized versions of them, so it should work, should'nt 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 must not be in the user root directory, it can be anywhere - mine for example are all on the D partition.

Wrong Solution #2: Use the "Shell Folders" key in the registry

In the next try of getting the Downloads folder path with unclever functions, developers (including me!) searched through the entire registry for a key containing the redirected folder path. Windows must store this information somewhere, right?

Eventually, they landed at "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders". The key looks really promising actually, 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, oh 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". What the heck does this key want from me? Most .NET developers simply blocked its helpful warning with "I can't even pronounce SHGetFolderWhatever, let me just use this key already, I got so far, I need to get this done today.".

Actually, the 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 some nice articles about why not to use this key here and here.

To cut things a little shorter, using this key was only acceptable in a Windows 95 beta version, and no moment later. Microsoft developers noticed it would not be flexible enough to keep all the information about shell folders, and it did not respect roaming user profiles and so on. A WinAPI function was created instead to retrieve the real paths, and the key was just left in the registry "temporary" to keep literally four programs designed in the beta time of Win95 compatible with the RTM version. It was never deleted afterwards since more developers didn't read the MSDN documentation, discovered this key in the RTM time, and started to make exorbitant use of it, so deleting it later 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 fell into the "Shell Folders"-key trap once and posted this as a "solution" to a StackOverflow answer (which I have updated meanwhile). Now I just feel ashamed.

The Correct Solution

So, if we are good developers, we just follow what the key has told us: P/Invoke to the said WinAPI functions! Sadly, P/Invoke is a little nasty, and even I struggled to implement the correct solution for a long time - until I finally ran into wrong results with the mentioned registry key and got the errors I deserved.

Luckily for you, you found this CodeProject article, and I posted my "wrapper" around these functions here. It's just a simple wrapper to retrieve the current, redirected and correct path to the folders as well as what the systems default path would be, not using the whole power of the SHGet/SetKnownFolder functions to change the paths or impersonating another user to get the path of his special folder.

Please note that my wrapper only makes use of SHGetKnownFolder function, which is a new function introduced in Windows Vista to replace the older SHGetFolderPath in XP and earlier. Thus said, you need at least Vista or newer, this will not work for XP. You can however, after following this article, easily develop up a wrapper using the older SHGetFolderPath function, if you still need to support XP or earlier - I don't.

At first, there's the static class KnownFolders with functions similar to <a href="https://msdn.microsoft.com/en-us/library/system.environment.getfolderpath" target="_blank">System.Environment.GetFolderPath()</a>:

/// <summary>
/// Class containing methods to retrieve specific file system paths.
/// </summary>
public static class KnownFolders
{
    /// <summary>
    /// Gets the current path to the specified known folder as currently configured. This does
    /// not require the folder to be existent.
    /// </summary>
    /// <param name="knownFolder">The known folder which current path will be returned.</param>
    /// <returns>The default path of the known folder.</returns>
    /// <exception cref="System.Runtime.InteropServices.ExternalException">Thrown if the path
    ///     could not be retrieved.</exception>
    public static string GetPath(KnownFolder knownFolder)
    {
        return GetPath(knownFolder, false);
    }

    /// <summary>
    /// Gets the current path to the specified known folder as currently configured. This does
    /// not require the folder to be existent.
    /// </summary>
    /// <param name="knownFolder">The known folder which current path will be returned.</param>
    /// <param name="defaultUser">Specifies if the paths of the default user (user profile
    ///     template) will be used. This requires administrative rights.</param>
    /// <returns>The default path of the known folder.</returns>
    /// <exception cref="System.Runtime.InteropServices.ExternalException">Thrown if the path
    ///     could not be retrieved.</exception>
    public static string GetPath(KnownFolder knownFolder, bool defaultUser)
    {
        return GetPath(knownFolder, KnownFolderFlags.DontVerify, defaultUser);
    }

    /// <summary>
    /// Gets the default path to the specified known folder. This does not require the folder
    /// to be existent.
    /// </summary>
    /// <param name="knownFolder">The known folder which default path will be returned.</param>
    /// <returns>The current (and possibly redirected) path of the known folder.</returns>
    /// <exception cref="System.Runtime.InteropServices.ExternalException">Thrown if the path
    ///     could not be retrieved.</exception>
    public static string GetDefaultPath(KnownFolder knownFolder)
    {
        return GetDefaultPath(knownFolder, false);
    }

    /// <summary>
    /// Gets the default path to the specified known folder. This does not require the folder
    /// to be existent.
    /// </summary>
    /// <param name="knownFolder">The known folder which default path will be returned.</param>
    /// <param name="defaultUser">Specifies if the paths of the default user (user profile
    ///     template) will be used. This requires administrative rights.</param>
    /// <returns>The current (and possibly redirected) path of the known folder.</returns>
    /// <exception cref="System.Runtime.InteropServices.ExternalException">Thrown if the path
    ///     could not be retrieved.</exception>
    public static string GetDefaultPath(KnownFolder knownFolder, bool defaultUser)
    {
        return GetPath(knownFolder, KnownFolderFlags.DefaultPath | KnownFolderFlags.DontVerify,
            defaultUser);
    }

    /// <summary>
    /// Creates and initializes the known folder.
    /// </summary>
    /// <param name="knownFolder">The known folder which will be initialized.</param>
    /// <exception cref="System.Runtime.InteropServices.ExternalException">Thrown if the known
    ///     folder could not be initialized.</exception>
    public static void Initialize(KnownFolder knownFolder)
    {
        Initialize(knownFolder, false);
    }

    /// <summary>
    /// Creates and initializes the known folder.
    /// </summary>
    /// <param name="knownFolder">The known folder which will be initialized.</param>
    /// <param name="defaultUser">Specifies if the paths of the default user (user profile
    ///     template) will be used. This requires administrative rights.</param>
    /// <exception cref="System.Runtime.InteropServices.ExternalException">Thrown if the known
    ///     folder could not be initialized.</exception>
    public static void Initialize(KnownFolder knownFolder, bool defaultUser)
    {
        GetPath(knownFolder, KnownFolderFlags.Create | KnownFolderFlags.Init, defaultUser);
    }
}

The GetPath() functions are pretty self-explaining, especially because I added big summaries to them to explain what they do exactly. Additionally, you can create the special folders if they don't exist yet, with the Initialize() functions.

Of most interest to you now should be the KnownFolder type, which is just an enumeration of special folders you can retrieve with the functions. I removed the summaries here which would describe what the folder is for, since when operating system version it exists, and what the default path would be, because that are 94 special folders in total!

/// <summary>
/// Standard folders registered with the system. These folders are installed with Windows Vista
/// and later operating systems, and a computer will have only folders appropriate to it
/// installed.
/// </summary>
public enum KnownFolder
{
    AccountPictures,
    AdminTools,
    ApplicationShortcuts,
    CameraRoll,
    CDBurning,
    CommonAdminTools,
    CommonOemLinks,
    CommonPrograms,
    CommonStartMenu,
    CommonStartup,
    CommonTemplates,
    Contacts,
    Cookies,
    Desktop,
    DeviceMetadataStore,
    Documents,
    DocumentsLibrary,
    Downloads,
    Favorites,
    Fonts,
    GameTasks,
    History,
    ImplicitAppShortcuts,
    InternetCache,
    Libraries,
    Links,
    LocalAppData,
    LocalAppDataLow,
    LocalizedResourcesDir,
    Music,
    MusicLibrary,
    NetHood,
    OriginalImages,
    PhotoAlbums,
    PicturesLibrary,
    Pictures,
    Playlists,
    PrintHood,
    Profile,
    ProgramData,
    ProgramFiles,
    ProgramFilesX64,
    ProgramFilesX86,
    ProgramFilesCommon,
    ProgramFilesCommonX64,
    ProgramFilesCommonX86,
    Programs,
    Public,
    PublicDesktop,
    PublicDocuments,
    PublicDownloads,
    PublicGameTasks,
    PublicLibraries,
    PublicMusic,
    PublicPictures,
    PublicRingtones,
    PublicUserTiles,
    PublicVideos,
    QuickLaunch,
    Recent,
    RecordedTVLibrary,
    ResourceDir,
    Ringtones,
    RoamingAppData,
    RoamedTileImages,
    RoamingTiles,
    SampleMusic,
    SamplePictures,
    SamplePlaylists,
    SampleVideos,
    SavedGames,
    SavedSearches,
    Screenshots,
    SearchHistory,
    SearchTemplates,
    SendTo,
    SidebarDefaultParts,
    SidebarParts,
    SkyDrive,
    SkyDriveCameraRoll,
    SkyDriveDocuments,
    SkyDrivePictures,
    StartMenu,
    Startup,
    System,
    SystemX86,
    Templates,
    UserPinned,
    UserProfiles,
    UserProgramFiles,
    UserProgramFilesCommon,
    Videos,
    VideosLibrary,
    Windows
}

When implementing this enumeration and writing the documentation for it, I followed the list of special folders published in MSDN here.

Internally, the enumeration is mapped to array indices, which contain the real KNOWNFOLDERID with which the WinAPI is queried. I'm afraid to list 94 rows of GUIDs here, so you just have to download the source code to see them all, but they're actually not really exciting for being the GUIDs they are.

Of last interest could be the private function of the KnownFolder class which actually does the legwork to eventually call the WinAPI:

private static string GetPath(KnownFolder knownFolder, KnownFolderFlags flags,
    bool defaultUser)
{
    IntPtr outPath;
    int result = SHGetKnownFolderPath(new Guid(_knownFolderGuids[(int)knownFolder]),
        (uint)flags, new IntPtr(defaultUser ? -1 : 0), out outPath);
    if (result >= 0)
    {
        return Marshal.PtrToStringUni(outPath);
    }
    else
    {
        throw new ExternalException("Unable to retrieve the known folder path. It may not "
            + "be available on this system.", result);
    }
}

/// <summary>
/// Retrieves the full path of a known folder identified by the folder's KnownFolderID.
/// </summary>
/// <param name="rfid">A KnownFolderID that identifies the folder.</param>
/// <param name="dwFlags">Flags that specify special retrieval options. This value can be
///     0; otherwise, one or more of the KnownFolderFlag values.</param>
/// <param name="hToken">An access token that represents a particular user. If this
///     parameter is NULL, which is the most common usage, the function requests the known
///     folder for the current user. Assigning a value of -1 indicates the Default User.
///     The default user profile is duplicated when any new user account is created.
///     Note that access to the Default User folders requires administrator privileges.
///     </param>
/// <param name="ppszPath">When this method returns, contains the address of a string that
///     specifies the path of the known folder. The returned path does not include a
///     trailing backslash.</param>
/// <returns>Returns S_OK if successful, or an error value otherwise.</returns>
[DllImport("Shell32.dll")]
private static extern int SHGetKnownFolderPath(
    [MarshalAs(UnmanagedType.LPStruct)]Guid rfid, uint dwFlags, IntPtr hToken,
   out IntPtr ppszPath);

[Flags]
private enum KnownFolderFlags : uint
{
    SimpleIDList              = 0x00000100,
    NotParentRelative         = 0x00000200,
    DefaultPath               = 0x00000400,
    Init                      = 0x00000800,
    NoAlias                   = 0x00001000,
    DontUnexpand              = 0x00002000,
    DontVerify                = 0x00004000,
    Create                    = 0x00008000,
    NoAppcontainerRedirection = 0x00010000,
    AliasOnly                 = 0x80000000
}

(Disclaimer: I did not put the imported SHGetKnownFolderPath function into a separate "NativeMethods"-like class here as recommended by Microsoft, this is just for simplicity.)

As you can see, it gets the GUID associated with the enumeration value as the index in the long GUID string array (which I did not list), the correct flags to get either the default path, create the folder or just get the current path (s. the KnownFolderFlags enumeration) and returns the path retrieved as a normal .NET string. Just how you want it!

Using the Code

From your point of view, using the code is straight forward. This small console application prints all the paths it could retrieve - yep, not all paths may be retrieved as they do not exist or are unsupported by your operating system, that's why there's a try catch.

private static void Main()
{
    // Go through each KnownFolder enum entry
    foreach (KnownFolder knownFolder in Enum.GetValues(typeof(KnownFolder)))
    {
        // Write down the current and default path
        Console.WriteLine(knownFolder.ToString());
        try
        {
            Console.Write("Current Path: ");
            Console.WriteLine(KnownFolders.GetPath(knownFolder));
            Console.Write("Default Path: ");
            Console.WriteLine(KnownFolders.GetDefaultPath(knownFolder));
        }
        catch (ExternalException ex)
        {
            // While uncommon with personal folders, other KnownFolders don't exist on every
            // system, and trying to query those returns an error which we catch here
            Console.WriteLine("<Exception> " + ex.ErrorCode);
        }
        Console.WriteLine();
    }
    Console.ReadLine();
}

Normally, you would not need the try catch, as the user folders can be retrieved most of the time, but as you know, most of the time is not all the time. You may just want to rewrite my internal functions to not throw an exception, but that's up to you.

NuGet Package

Due to a request in the comments, I created a NuGet package for this functionality. More information about it on its project site.

It offers additional functionality like modifying the paths with the SHSetKnownFolderPath method, and allows querying the paths of any user (if you have the required rights).

Note that the class design of the NuGet package is slightly different to be more object oriented with all these new goodies. Consult the Usage section on the project site for a guide.

Points of Interest

We learned: P/Invoke is easy - if we do not have to do it and download the source code from CodeProject. Nah, really, it wasn't that hard, was it? And now, we can have fun with any super special folder path as we want it.

As said, you must implement your own wrapper to the older SHGetFolderPath function to get an XP-and-earlier-compatible wrapper. That function is indeed just a wrapper to SHGetKnownFolderPath in versions since Windows Vista, and I personally do not care about decade old, unsupported systems anymore, but you may have due to special customers with special folders. But for XP, the System.Environment.GetFolderPath() function might just be good enough.

If you like my wrapper, feel free to upvote my original answer on StackOverflow!

History

  • 21.03.2016 - Added note about newly created NuGet package
  • 20.02.2015 - First release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Ray Koopa
Germany Germany
No Biography provided

You may also be interested in...

Pro

Comments and Discussions

 
Questionmemory leak Pin
Member 944454221-Mar-17 3:39
memberMember 944454221-Mar-17 3:39 
Question[WIN10] Changing System Folder Path Pin
Member 1260888428-Jun-16 12:49
memberMember 1260888428-Jun-16 12:49 
AnswerRe: [WIN10] Changing System Folder Path Pin
Ray Koopa5-Aug-16 2:19
memberRay Koopa5-Aug-16 2:19 
GeneralMy vote of 5 Pin
David A. Gray29-Jun-15 7:39
memberDavid A. Gray29-Jun-15 7:39 
QuestionNuget Package Pin
Lasse Christiansen - sw_lasse4-May-15 3:32
memberLasse Christiansen - sw_lasse4-May-15 3:32 
AnswerRe: Nuget Package Pin
Ray Koopa5-May-15 11:50
memberRay Koopa5-May-15 11:50 
AnswerRe: Nuget Package Pin
Ray Koopa21-Mar-16 12:47
memberRay Koopa21-Mar-16 12:47 
GeneralMy vote of 5 Pin
Prafulla Hunde6-Mar-15 6:19
memberPrafulla Hunde6-Mar-15 6:19 
QuestionSuggestion Pin
bfrthekid991-Mar-15 4:37
memberbfrthekid991-Mar-15 4:37 
AnswerRe: Suggestion Pin
DebugErr1-Mar-15 5:19
memberDebugErr1-Mar-15 5:19 
QuestionIs Favorites "Folder" in W7, W8 a Special Folder? Pin
wim4you24-Feb-15 3:10
memberwim4you24-Feb-15 3:10 
AnswerRe: Is Favorites "Folder" in W7, W8 a Special Folder? Pin
DebugErr24-Feb-15 3:28
memberDebugErr24-Feb-15 3:28 
GeneralRe: Is Favorites "Folder" in W7, W8 a Special Folder? Pin
David A. Gray29-Jun-15 7:38
memberDavid A. Gray29-Jun-15 7:38 
Questioncool stuff!! Pin
jediYL23-Feb-15 16:35
professionaljediYL23-Feb-15 16:35 
QuestionReturn the special folder for given path Pin
Andrew Truckle21-Feb-15 19:02
memberAndrew Truckle21-Feb-15 19:02 
AnswerRe: Return the special folder for given path Pin
DebugErr21-Feb-15 23:17
memberDebugErr21-Feb-15 23:17 
GeneralRe: Return the special folder for given path Pin
Andrew Truckle22-Feb-15 6:21
memberAndrew Truckle22-Feb-15 6:21 
GeneralRe: Return the special folder for given path Pin
DebugErr22-Feb-15 6:39
memberDebugErr22-Feb-15 6:39 
GeneralRe: Return the special folder for given path Pin
Andrew Truckle22-Feb-15 6:56
memberAndrew Truckle22-Feb-15 6:56 
GeneralRe: Return the special folder for given path Pin
DebugErr22-Feb-15 8:52
memberDebugErr22-Feb-15 8:52 
GeneralRe: Return the special folder for given path Pin
Philippe Mori1-Jun-15 6:27
memberPhilippe Mori1-Jun-15 6:27 
Generalmy vote of 5 Pin
Southmountain21-Feb-15 9:33
memberSouthmountain21-Feb-15 9:33 
GeneralFantastic Pin
Dom Sinclair21-Feb-15 0:46
memberDom Sinclair21-Feb-15 0:46 
GeneralMy vote of 5 Pin
Franc Morales20-Feb-15 22:38
memberFranc Morales20-Feb-15 22:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.171019.1 | Last Updated 21 Mar 2016
Article Copyright 2015 by Ray Koopa
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid