Updated on

The Windows Registry is a hierarchical database of settings that Windows and the software installed on it read and write at runtime. In C# we reach it through two types in the Microsoft.Win32 namespace: Registry for a single value, and RegistryKey for everything else.

Registry.GetValue() and Registry.SetValue() take a full key path and do one read or one write. RegistryKey represents an open key, so it can enumerate, delete, inspect value types, and set permissions, and it has to be disposed.

The registry exists only on Windows, so every call is guarded. OperatingSystem.IsWindows() is the guard, and without it the compiler raises CA1416 on code that would throw on any other platform.

To download the source code for this article, you can visit our GitHub repository.

What Is the Windows Registry?

The Windows Registry is a hierarchical database that Windows, its device drivers, and installed applications use to store settings as name/value pairs.

Its shape is a tree. A node in that tree is a key, a key holds subkeys, and it also holds values, each a name paired with data and a declared type. The comparison that lands fastest is a filesystem: keys behave like directories and values like the files inside them.

The tree has a fixed set of roots. Windows keeps them open for us, which is why they are called predefined keys, root keys, or base keys.

Two of those roots do almost all the work. HKEY_CURRENT_USER holds settings belonging to the account our process is running under. HKEY_LOCAL_MACHINE holds settings shared by every account on the computer.

That split is the practical part. By default, writing under HKEY_CURRENT_USER needs no special rights, while writing under HKEY_LOCAL_MACHINE needs an elevated process. It is the first thing that breaks.

When those writes fail, the fix is to run the application as an administrator.

Windows Registry Structure

The Windows Registry utilizes a tree structure for its data. A node in that tree is a key, and Microsoft’s Win32 registry reference states the rest plainly: “Each key can contain both subkeys and data entries called values.” Furthermore, we can consider Windows Registry a name/value database. An additional view of the Windows Registry is a directory view. Basically, the key represents the directory, and the directory’s content is subdirectories/subkeys or files/name-value pairs.

The Windows Registry comes with a set of predefined keys. An application can use handles to access those keys. The predefined keys are always open and serve as entry points to the registry. The different versions of Windows can have a different set of predefined keys. We also call those keys root elements, root keys, or base keys. Here are the ones the Registry class exposes:

Base keyRegistry propertyRegistryHive memberWhat it holds
HKEY_CLASSES_ROOTRegistry.ClassesRootRegistryHive.ClassesRootFile name extension associations and COM class registration data.
HKEY_CURRENT_USERRegistry.CurrentUserRegistryHive.CurrentUserSettings for the account running our process. Writable without elevation by default.
HKEY_LOCAL_MACHINERegistry.LocalMachineRegistryHive.LocalMachineMachine-wide settings. Writing needs an elevated process by default.
HKEY_USERSRegistry.UsersRegistryHive.UsersThe loaded profiles of every user on the machine.
HKEY_CURRENT_CONFIGRegistry.CurrentConfigRegistryHive.CurrentConfigDifferences between the current and the standard hardware profile.
HKEY_PERFORMANCE_DATARegistry.PerformanceDataRegistryHive.PerformanceDataPerformance counter data, assembled on request rather than stored on disk.

Win32 defines a few more predefined keys that the Registry class does not expose, HKEY_CURRENT_USER_LOCAL_SETTINGS among them, and reaching those means calling the Win32 API directly.

To learn more about the predefined keys, please read about predefined keys.

Windows Registry Key Values

The values of the Windows registry keys can be of different types:

Registry typeRegistryValueKind memberReturned by GetValue() asNotes
REG_SZStringstringA null-terminated string. The default for SetValue() with a string.
REG_EXPAND_SZExpandStringstringHolds unexpanded references such as %PATH%. GetValue() expands them unless we pass RegistryValueOptions.DoNotExpandEnvironmentNames.
REG_BINARYBinarybyte[]Any data in binary form.
REG_DWORDDWordintA 32-bit number. REG_DWORD_LITTLE_ENDIAN is the same type under a second name.
REG_QWORDQWordlongA 64-bit number.
REG_MULTI_SZMultiStringstring[]An array of null-terminated strings.
REG_NONENonebyte[]The value type is not defined.
REG_DWORD_BIG_ENDIAN, REG_LINK, REG_RESOURCE_LISTUnknownbyte[]RegistryValueKind has no member for these, so GetValueKind() reports Unknown.

When the operating system starts or a new user logs in, the new set of registry keys is loaded into memory. This set depends on the user profile, configuration, and settings. The first key in this set is called a hive or a base key. A hive is not something an application creates by adding a key: the set of hives is fixed by the system.

Open the Windows Registry

DISCLAIMER! We’ll demonstrate how to open and look at the Windows Registry. Later on, we’ll also make some programmatic changes to the Windows Registry. The reader accepts sole responsibility for all inadvertent changes made to their system and possible problems caused by modifying or deleting registry keys manually or running the demo application. It is strongly recommended to create a Windows Registry backup before making any changes.

We can open the Windows Registry in two ways:

  • Type regedit in the search box on the taskbar, and select the Registry Editor application.
  • Right-click the Windows Start button, select Run, type regedit in the box, and press OK.

The Windows Registry on the user’s computer may look slightly different, but the following picture shows its basic structure:

Windows Registry Structure

How Do We Access the Windows Registry in .NET?

Everything we need is in the Microsoft.Win32 namespace, and it ships inside the base class library. There is no NuGet package to install and no reference to add.

Two types carry the whole API. Registry is a static class that exposes the six base keys as properties and adds exactly two methods, GetValue() and SetValue(), each of which takes a full key path.

RegistryKey is an instance type representing one open key, and it holds everything else: opening, creating, deleting, enumerating, value types, and access control. It implements IDisposable, because an instance owns an open handle to a native key.

The registry is a Windows feature, so the entire API is annotated as Windows-only. On a target framework that is not Windows-specific, the compiler raises CA1416 on every call site it cannot prove runs on Windows.

Guarding the call with OperatingSystem.IsWindows() satisfies the analyzer and keeps the rest of the application cross-platform.

The Registry and RegistryKey classes are the main classes for interacting with the Windows Registry.

The Registry Class

The Registry class provides access to the root keys. Besides that, it implements static methods to read or write key/value pairs. For example, we can print the name of the HKEY_CURRENT_USER. This first version is deliberately missing its platform check, so we can see what the compiler says about it:

public static string GetCurrentUserRootKeyName()
{
    var registryKey = Registry.CurrentUser;
    return registryKey.Name;
}

In the above method, we notice compiler warnings:

warning CA1416: This call site is reachable on all platforms. 'RegistryKey.Name' is only supported on: 'windows'.
warning CA1416: This call site is reachable on all platforms. 'Registry.CurrentUser' is only supported on: 'windows'.

The Windows Registry is a Windows-specific feature that doesn’t exist on other operating systems.

However, .NET is a cross-platform framework. Because of this, the compiler warns us that this code won’t execute outside of a Windows environment. It is a good practice to check which operating system our code is running on and execute this part of the code accordingly:

public static string GetCurrentUserRootKeyNameWithPlatformCheck()
{
    if (!OperatingSystem.IsWindows())
    {
        return "Unsupported platform";
    }

    return Registry.CurrentUser.Name;
}

The RegistryKey Class

The RegistryKey class encapsulates the key-level node in the Windows Registry. It provides the methods for working with the key and its name/value pairs, inspecting certain properties of the registry key, and getting or setting access control over it. For example, we can count the number of subkeys in the specific key:

public static int GetCurrentUserRootKeySubkeyCount()
{
    if (!OperatingSystem.IsWindows())
    {
        return -1;
    }

    return Registry.CurrentUser.SubKeyCount;
}

We’ll explore other methods and properties of the RegistryKey class in the following sections.

The Windows Registry’s Typical Use Cases

The most common use cases of the Windows Registry include the following:

  • System settings – configuration and behavior of Windows itself
  • Hardware configuration – device drivers and hardware settings
  • Software configuration – preferences, license information, window size and position, and other application-specific data
  • User profiles – user-specific configurations and settings that allow users to have personalized interfaces
  • Security settings – security and group policies, user account settings
  • File associations – file type-specific settings that allow applications to open files with particular extensions
  • Startup configuration – information about programs that should start automatically with Windows

Application preferences are the one item on that list with a portable alternative: the .NET configuration system stores the same settings without tying our application to Windows. The registry still earns its place where the setting belongs to the machine rather than to one deployment, which is often the case for an application running as a Windows service.

How Do We Set and Get a Registry Value in C#?

Writing a value takes one call. Registry.SetValue() takes the full path of the key, the name of the value, and the data, and it creates the key if it is not there yet.

Reading it back takes the matching call. Registry.GetValue() takes the same path, the same value name, and a default to return when that name is missing. It hands the result back as object, so we convert it ourselves.

The second parameter is the one that trips people up. It names a value inside the key, not another step in the key path. In our sample HKEY_CURRENT_USER\CodeMazeRegistryDemoSubKey is the key and CodeMazeRegistryDemoName is a value in it.

Two different misses look alike. GetValue() returns null when it cannot find the key at all, and returns the default we passed when the key exists but has no value under that name.

Every key also has one unnamed default value, read and written by passing null or an empty string as the name.

In the following code snippets, we’ll use a few constants:

private const string CodeMazeRegistryDemoSubKey = "CodeMazeRegistryDemoSubKey";
private const string CodeMazeRegistryDemoName = "CodeMazeRegistryDemoName";
public const string CodeMazeRegistryDemoValue = "CodeMazeRegistryDemoValue";

The CodeMazeRegistryDemoSubKey is the name of the subkey we’ll work with and the CodeMazeRegistryDemoName is the name of the value in the key. Finally, the CodeMazeRegistryDemoValue is the value we’ll write to the name/value pair. As an illustration, we will use Registry.CurrentUser (HKEY_CURRENT_USER) base key.

Read and Write Windows Registry Using Registry Class

The Registry class implements GetValue() and SetValue() static methods. Let’s see how we can use them:

public static string ReadAndWriteRegistryValueUsingRegistryClass()
{
    if (!OperatingSystem.IsWindows())
    {
        return string.Empty;
    }

    var subKeyToWrite = $@"{Registry.CurrentUser.Name}\{CodeMazeRegistryDemoSubKey}";

    Registry.SetValue(subKeyToWrite, CodeMazeRegistryDemoName, CodeMazeRegistryDemoValue);
    var writtenValue = Registry.GetValue(subKeyToWrite, CodeMazeRegistryDemoName, string.Empty);

    Registry.CurrentUser.DeleteSubKey(CodeMazeRegistryDemoSubKey);

    return writtenValue?.ToString() ?? string.Empty;
}

The SetValue() method has three parameters: the full path to the subkey, the name, and the value it will write to the registry. If the subkey doesn’t exist, it will be created. If the key doesn’t exist, it will be created. Otherwise, the method writes the name and value to the specified key.

The SetValue() method has one additional overload, allowing to specify the value’s data type. The method opens and closes the key automatically when writing the value to it.

Identically to the SetValue()method, the GetValue() also has three parameters: the full path to the subkey, the name, and the default value that will be returned if the name doesn’t exist. The method will return null if it can’t find the subkey and return the default value if the name doesn’t exist. The return value is of the object type, and we must cast it or convert it to the appropriate type.

Each registry key contains one default, unnamed value, which the GetValue() method returns if the name doesn’t exist in this key. We can use null or string.Empty in the SetValue() method for a name argument if we want to set the value to the default name.

Default value for Windows Registry Key

Finally, we call the RegistryKey class’s DeleteSubKey() method to delete our created key:

Registry.CurrentUser.DeleteSubKey(CodeMazeRegistryDemoSubKey);

Read and Write Windows Registry Using RegistryKey Class

As mentioned previously, the RegistryKey class represents a node in the Windows Registry. It implements various methods for working with the registry keys:

public static string ReadAndWriteRegistryValueUsingRegistryKeyClass()
{
    if (!OperatingSystem.IsWindows())
    {
        return string.Empty;
    }

    var baseKey = Registry.CurrentUser;

    using var subKey = baseKey.OpenSubKey(CodeMazeRegistryDemoSubKey, true) ??
                       baseKey.CreateSubKey(CodeMazeRegistryDemoSubKey);

    subKey.SetValue(CodeMazeRegistryDemoName, CodeMazeRegistryDemoValue);
    var writtenValue = subKey.GetValue(CodeMazeRegistryDemoName);
    subKey.DeleteValue(CodeMazeRegistryDemoName);

    baseKey.DeleteSubKey(CodeMazeRegistryDemoSubKey);

    return writtenValue?.ToString() ?? string.Empty;
}

We open the subkey with the OpenSubKey() method. This method has five overloads, dealing with access control in different ways. For our example, we use the overload OpenSubKey(string, bool), with which we can open the subkey with write access. If we omit this parameter, the key opens only with read access, and the SetValue() method throws an exception. If the specified key doesn’t exist, the method returns null.

OpenSubKey(name) opens the key read-only, so any write through it throws an UnauthorizedAccessException; OpenSubKey(name, true) and CreateSubKey(name) are the two ways to get a key we can write to.

In our case, the key will always be null, as a result of deleting it with the DeleteSubKey() method at the end of the ReadAndWriteRegistryValueUsingRegistryKeyClass() method. On the other hand, if we comment out the call to the DeleteSubKey() method and rerun the application, the key will exist.

In the following line, we create the key with the call to the CreateSubKey() method. This method will open the key with write access. It has seven overloads that allow us to create the key with different access controls and options.

With the call to the RegistryKey class’s SetValue() method, we write the name/value pair to the key. If the name already exists, the value is updated. The method’s parameters are the name and the values, as the registry key path is already encapsulated in the subKey variable.

The GetValue() method reads the value under the specified name. In addition to that, we call the DeleteValue() method next. It deletes the value with the specified name. However, this step is technically unnecessary, as we later delete the entire subkey by calling the DeleteSubKey() method.

Finally, we return the value as a string. It is worth noting that the RegistryKey class also has a ToString() method that will return the name of the key as a string.

Disposing of the RegistryKey Class

The RegistryKey class implements the IDisposable interface, so everything we already know about how to manage IDisposable objects applies here too. Here, we wrap it in the using statement, implying that the Dispose() method will be called automatically.

Alternatively, if we don’t use the using statement, we can call the Dispose() method directly, which consequently closes the key, the same as a direct call to the Close() method. The call to the Close() method will write the key to the disk, and no further writes are possible.

Similarly, we can call the Flush() method if we want to be sure that key changes are written on the disk, but in typical cases, this is unnecessary, as the registry changes are stored automatically.

Registry or RegistryKey: Which Class Should We Use?

Both classes read and write values. The difference is how many times we touch the key.

Registry is for one-off access. Each GetValue() or SetValue() call opens the key, does the work, and closes it again, so reading a single setting is one line with nothing left to dispose.

RegistryKey is for everything else. We open or create the key once, hold the handle, and call as many methods on it as we need. It is also the only route to enumeration, value types, deletion, access control, the 32-bit and 64-bit views, and remote machines.

That handle is why the choice matters beyond convenience. RegistryKey implements IDisposable, so it belongs in a using declaration, and Registry has nothing to dispose.

One rule settles most cases. Writing through RegistryKey needs a key opened for writing, so we call OpenSubKey(name, true) or CreateSubKey(name). The single-argument OpenSubKey(name) opens read-only, and SetValue() on it throws.

TaskRegistry (static)RegistryKey (instance)
Read one valueRegistry.GetValue(path, name, default)key.GetValue(name)
Write one valueRegistry.SetValue(path, name, value)key.SetValue(name, value)
Open an existing keynot availablekey.OpenSubKey(name) or OpenSubKey(name, writable)
Create a keyhappens implicitly inside SetValue()key.CreateSubKey(name)
Delete a valuenot availablekey.DeleteValue(name)
Delete a keynot availablekey.DeleteSubKey(name), key.DeleteSubKeyTree(name)
List subkeys or value namesnot availablekey.GetSubKeyNames(), key.GetValueNames()
Read a value's typenot availablekey.GetValueKind(name)
Read or set access controlnot availablekey.GetAccessControl(), key.SetAccessControl()
Choose the 32-bit or 64-bit viewnot availableRegistryKey.OpenBaseKey(hive, view)
Reach another machinenot availableRegistryKey.OpenRemoteBaseKey(hive, machine)
Needs disposingnoyes, it implements IDisposable

The same rule covers the rest of the write surface: CreateSubKey(), DeleteSubKey(), DeleteSubKeyTree() and DeleteValue() all go through the same writability check that SetValue() does.

What Else Can We Do With the Windows Registry in C#?

The RegistryKey class implements methods for more advanced work with the Windows Registry. Let’s look at some of them.

Enumerating Keys and Values

We can get an array of all subkeys for the particular key by calling the GetSubKeyNames() method:

public static string[] GetSubKeyNames()
{
    if (!OperatingSystem.IsWindows())
    {
        return [];
    }

    using var subKey = Registry.CurrentUser.CreateSubKey(CodeMazeRegistryDemoSubKey);
    subKey.CreateSubKey("SubKey1");
    subKey.CreateSubKey("SubKey2");

    var subKeyNames = subKey.GetSubKeyNames();

    Registry.CurrentUser.DeleteSubKeyTree(CodeMazeRegistryDemoSubKey);

    return subKeyNames;
}

Apart from this, we can get an array of all value names by calling the GetValueNames() method:

public static string[] GetValueNames()
{
    if (!OperatingSystem.IsWindows())
    {
        return [];
    }

    using var subKey = Registry.CurrentUser.CreateSubKey(CodeMazeRegistryDemoSubKey);
    using var subKey1 = subKey.CreateSubKey("SubKey1");
    subKey1.SetValue("Name1", "Value1");
    subKey1.SetValue("Name2", "Value2");

    var subKeyNames = subKey1.GetValueNames();

    Registry.CurrentUser.DeleteSubKeyTree(CodeMazeRegistryDemoSubKey);

    return subKeyNames;
}

CreateSubKey() returns a key, never null, so there is no null-conditional operator and no fallback array in either method.

Get the Data Type of the Registry Key Value

The RegistryKey class implements the GetValueKind() method. It returns the data type of the specified value name:

public static string GetValueKind()
{
    if (!OperatingSystem.IsWindows())
    {
        return string.Empty;
    }

    using var subKey = Registry.CurrentUser.CreateSubKey(CodeMazeRegistryDemoSubKey);
    using var subKey1 = subKey.CreateSubKey("SubKey1");
    subKey1.SetValue("Name1", "Value1");

    var valueKind = subKey1.GetValueKind("Name1");

    Registry.CurrentUser.DeleteSubKeyTree(CodeMazeRegistryDemoSubKey);

    return valueKind.ToString();
}

The GetValueKind() method will return the member of the RegistryValueKind enumeration defined in the Microsoft.Win32 namespace.

The Registry Key Access Permissions

The CreateSubKey() and OpenSubKey() methods implement various overloads that accept parameters to finely control the access rights to the registry key.

With the RegistryKeyPermissionCheck enumeration, we can enforce the security checks when opening the key and accessing its values. Furthermore, with the RegistryRights enumeration, we can achieve fine-grained access control of the registry key:

public static bool SetRegistryKeyAccessPermissions()
{
    if (!OperatingSystem.IsWindows())
    {
        return false;
    }

    var user = $@"{Environment.UserDomainName}\{Environment.UserName}";
    var registrySecurity = new RegistrySecurity();

    var accessRule = new RegistryAccessRule(user,
        RegistryRights.ReadKey | RegistryRights.WriteKey,
        InheritanceFlags.None,
        PropagationFlags.None,
        AccessControlType.Allow);

    registrySecurity.AddAccessRule(accessRule);

    using var subKey = Registry.CurrentUser.CreateSubKey(CodeMazeRegistryDemoSubKey,
        RegistryKeyPermissionCheck.Default, registrySecurity);

    if (subKey == null)
    {
        return false;
    }

    var isAdded = false;
    var accessControl = subKey.GetAccessControl();
    var accessRules = accessControl.GetAccessRules(true, true, typeof(NTAccount));
    foreach (RegistryAccessRule rule in accessRules)
    {
        if (rule.IdentityReference.Value == user)
        {
            isAdded = true;
            break;
        }
    }
    Registry.CurrentUser.DeleteSubKeyTree(CodeMazeRegistryDemoSubKey);

    return isAdded;
}

In our example, we create the subkey by supplying the permission check and the registry security arguments. The permission check has a default value, meaning the security check is inherited from the parent. Besides the permission check, the RegistrySecurity class allows us to add registry access rules. As an illustration, we define that the user can read and write the key by specifying the desired RegistryRights values. RegistryRights is a flags enum, so the values combine with the bitwise OR operator (|).

The GetAccessControl() method returns the registry security definition of the key, and through it, we can enumerate all defined rules. After that, we check if our rule is applied to the key.

Using the related SetAccessControl() method, we can set and/or modify the security definition of the key (assuming we have the appropriate security access to change security rights).

Read and Write a Remote Windows Registry

The RegistryKey class implements the static OpenRemoteBaseKey() method. With it, we open the base key on a machine to which we have a remote network connection:

public static bool OpenRemoteBaseKey(string machineName)
{
    if (!OperatingSystem.IsWindows())
    {
        return false;
    }

    try
    {
        using var remoteBaseKey = RegistryKey.OpenRemoteBaseKey(RegistryHive.CurrentUser, machineName);

        return true;
    }
    catch (ArgumentException)
    {
        // An unreachable machine and a stopped Remote Registry service both surface here.
        return false;
    }
    catch (IOException)
    {
        return false;
    }
}

The returned key is an open handle like any other, so we wrap it in a using declaration. The catch is narrow on purpose: an unreachable machine and a stopped Remote Registry service both arrive as an ArgumentException rather than as a network or security exception, which is easy to get wrong from the outside.

The Remote Registry Windows Service on the remote machine must be enabled to access the remote registry, and, of course, we must have administrative rights on the remote machine.

Conclusion

The Windows Registry is a central, structured place for applications, the operating system, and hardware to store different types of values necessary for their proper functioning. As a result, it represents the very welcome evolution of .INI files and enables much richer capabilities and features.

The Windows Registry is specific to the Windows operating system, and therefore we should verify in our code that it exists and we can access it.

The Registry and RegistryKey classes provide code-level access which allows us to read and write Windows Registry entries. They also provide a means for more advanced operations such as fine-control of access rights, along with accessing the registry on remote machines.

Tested with .NET 10.0.302.