Demonstrating Custom Configuration for .NET (Part 5): “Inheritance”
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
- Machine
- Application
- User Roaming
- 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 theAssemblyProductAttribute
. 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:
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:
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.