Configuration Inheritance: There Are Multiple Configuration Files

Introduction

Previous Parts of This Series.

Note, the contents of the post covers non-ASP.NET applications. ASP.NET uses its own approach to configuration inheritance, and this article already contains more than enough information.

Also note, I call this configuration inheritance because it expresses the idea of one thing overriding another. Given the limited conceptual documentation about .NET’s configuration system I don’t think there is an official term. Until there is I will stick with “inheritance”.

When one uses the .NET configuration APIs to load settings into an application there is a rich structure supporting not just the kind of data that can be held, but also the scope of that data. Different applications can share data at the machine level, different AppDomains in the same process can have different configuration, and users can have their own settings either in the roaming part of their profile or purely local, or both.

Where this data is stored, and what influences where is the subject of this posting, but on the way there is a little coverage of how to get it and manage it but mostly that will be for the future.

The Four Levels

For non-ASP.NET there are four files, with each able—if the section allows it—to override the previous level. Some times of collection can also be modified by subsequence levels: clearing the collection, adding or removing members. The four levels are

  1. Machine
  2. Application
  3. User Roaming
  4. User Local

The Machine Configuration Level

This is the global file, normally stored in %SystemRoot%\\Microsoft.NET\\Framework64\\‹Version›\\Config\\machine.config for 64bit .NET or %SystemRoot%\\Microsoft.NET\\Framework\\‹Version›\\Config\\machine.config for 32bit. Where ‹Version› is the full version, e.g. v4.0.30319 for .NET 4.

It is instructive to read through this file—if you have enabled local active content in Internet Explorer the ability to collapse elements makes this easier—because you will see all the standard sections and section groups defined with the classes that implement them. For instance <appSettings> is implemented by System.Configuration.AppSettingsSection in assembly System.Configuration. It is also in the <connectionStrings> section at the machine level that connection “LocalSqlServer” is defined to be SQL Express using data file …\\App_Data\\aspnetdb.mdf which is then used as the store for ASP.NET profiles in their default configuration. (ASP.NET also has a global configuration “web.config” in the same folder with yet more global settings.)

As this file is read, by the configuration system, first any of the other levels can override it, unless the section is defined with allowDefinition or allowExeDefinition attribute is set to “MachineOnly” to ensure it can only be used at the machine level.

The Application Level

This is the most familiar level, creating a file called “app.config” in a Visual Studio project will create a file “‹assembly-name›.config” in the project output directory.

User Roaming and Local Levels

If you set things up correctly, per user files can also be used. The roaming file will be part of the user’s roaming profile, shared across machines if roaming is enabled. The local file will stay local. But you can override the filenames (see below) and as the content of a user’s roaming profile is based on a set of configurable folders, these names are strictly speaking indicative rather than absolute.

User levels are most useful when the ability to save configuration content is enabled. Rather than storing user settings and options in the registry use these configuration files.

The only problem with these files is the default naming, it is designed to ensure different versions of the same thing running side by side have separate configuration. If the location, assembly version or signing key changes then the filenames will be changed and any previous content (e.g. from before an upgrade) will be lost. This is bad news. The good news is that it is easy to specify your own filenames—and I would suggest doing that in most cases.

Using The User Levels

If you just use the static properties of ConfigurationManager (or one of the other configuration entry points) then machine and application configuration files are included, but user files are not.

To access either user-roaming, or user-roaming and user-local levels (you cannot use the local level without the roaming level—the roaming level could be left empty) one just uses one of the static ConfigurationManager methods that take a ConfigurationUserLevel parameter. The ConfigurationUserLevel enumeration allows selection of no user levels: ConfigurationUserLevel.None, just roaming: ConfigurationUserLevel.PerUserRaoming, or all levels: ConfigurationUserLevel.PerUserRaomingAndLocal.

If the configuration is opened with the ConfigurationManager.OpenExeConfiguration method then the default filenames, created by the configuration system, are used. Alternately using ConfigurationManager.OpenMappedExeConfiguration allows the filenames to be specified (more on this below, after covering the default filenames).

What Are The Default Filenames?

Disclaimer: none of this, as far as I can determine, is formally documented. Treat this information as subject to change in a future .NET version.

Default Filenames For The Default AppDomain of a Unsigned Executable

This is the simplest, and likely starting, case. The application configuration is the starting assembly’s path with “.config” appended. The user files will be:

  %USERPROFILE%\\‹folder›\\‹companyName›\\‹AssemblyBase›_Url_‹hash›\\‹version›\\user.config

where

  • folder: Is either “Roaming” or “Local” depending on the level.
  • companyName: Is the value of the AssemblyCompanyAttribute if set. If not then the value of the AssemblyProductAttribute. If neither is set then this path fragment is not included (one less directory in the path).
  • AssemblyBase: is the first part of the assembly’s filename (including the first period and two further characters).
  • hash: is an alphanumeric string created by hashing the full path of the assembly in some implementation determined way.
  • version: is the value of the AssemblyVersionAttrinbute.

With Code Signing

If the assembly is signed then there is a small change. Rather than ‹AssemblyBase›_Url_‹hash› we have ‹AssemblyBase›_StrongName_‹hash› with the hash being based on the signing key (the rest of the path is the same). This means by signing the assembly the location of the user configuration files becomes independent of the location of the assembly. If you want xcopy deployment then signing would seem to be a good idea to allow the user to relocate the application while preserving their configuration.

With Other AppDomains

If the configuration is loaded from a different AppDomain the machine and application configuration files remain the same. However the user levels file names change. In the above path AssemblyBase is replaced by the AppDomain name. Another case of trying to ensure uniqueness.

One of the options on creating an AppDomain is to specify a different configuration file:

var adConfigPath = Path.GetDirectoryName(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile);
adConfigPath = Path.Combine(adConfigPath, "AppDomain.config");
var adSetup = new AppDomainSetup {
  ConfigurationFile = adConfigPath
};
otherDomain = AppDomain.CreateDomain("WithDifferentConfig", null, adSetup);

In this case replacing the AssemblyName.config with AppDomain.config. This results in the assembly level configuration file being the filename specified, but no other levels are changed. Specifically the user level filenames remain the same whatever application level configuration is specified.

Note, if you don’t give an AppDomain a name on creation, .NET will: there are no unnamed AppDomains.

Defining You Own Filenames

As noted above the default naming is not ideal (if you want to be able to revise the application) except in being confident of its uniqueness. You can however define all the filenames, except at machine level, yourself.

To specify your own filenames you need to specify all the levels you are going to use, except the machine level. This includes the application level (to use the default just use AppDomain.CurrentDomain.SetupInformation.ConfigurationFile (this seems to be always populated whether not specified on AppDomain creation or in the default AppDomain) or Assembly.GetEntryAssembly().Location + \".config\".

The real benefit is being able to specify you own path, to be independent of assembly/AppDomain name, assembly version, signing key and assembly location. Just construct the path you need, and remember with a new version you need to ensure that structural changes are either backwards compatible, or you include conversion code. Something like:

private static string GetProfileConfigPath(Assembly targetAssembly, Environment.SpecialFolder pathId) {
  var profile = Environment.GetFolderPath(pathId,
                                          Environment.SpecialFolderOption.None);
  var company = targetAssembly.GetAttribute<AssemblyCompanyAttribute>().Company;
  if (String.IsNullOrWhiteSpace(company)) {
    throw new InvalidOperationException("AssemblyCompanyAttribute must be set");
  }
  var configPath = Path.Combine(profile, company);
  configPath = Path.Combine(configPath, targetAssembly.GetName().Name);
  configPath = Path.Combine(configPath, "user.config");
  return configPath;
}

This can be called with each of Environment.SpecialFolder.ApplicationData and Environment.SpecialFolder.LocalApplicationData to get the current users roaming and local profile root folders (respectively). (The GetAttribute<T>() method is a helper extension method over Assembly I have which gets the single instance of attribute T from the assembly.) Normally this would be passed the entry assembly, but for an extension could be passed the extension‘s assembly to keep configuration separate (and thus avoid conflicts) from the containing application.

Once you have the filenames you want to use, populate an instance of ExeConfigurationFileMap and pass to ConfigurationManager.OpenMappedExeConfiguration to get a Configuration instance, which is used in exactly the same way as an instance obtained any other way.

Discovering the Names Being Used At Runtime

There are two approaches: defined and laborious, or using reflection to peer inside the Configuration instance. The safe method is based on Configuration.FilePath being the path of the lowest level of configuration in use. So open with different ConfigurationUserLevel values and get each path in turn.

The reflection method relies on Configuration having private member _configRecord of private type System.Configuration.MgmtConfigurationRecord. This is a linked list of MgmtConfigurationRecord instances, one per level. Each has a _configName field which gives the level as a string (“MACHINE”, “EXE”, …) giving the level, and a ConfigurationFilePath property with the path. Given an instance of Configuration and a few helpers a populated ExeConfigurationFileMap can be returned.

Reference

In the posting: “Client Settings FAQ” Raghavendra Prabhu covers some details of the user-roaming and user-local configuration levels. Including that the hash following the _Url_ or _StrongName_ is based on the evidence—i.e. of the URL or signing key.

Previous Part of This Series on .NET Custom Configuration

  1. Demonstrating Custom Configuration Sections for .NET (Part 1)
  2. Demonstrating Custom Configuration for .NET (Part 2): Custom Elements
  3. Demonstrating Custom Configuration for .NET (Part 3): Validation
  4. Custom Configuration for .NET (Part 4): Backgrounder on Configuration Sections