///////////////////////////////////////////////////////
// Code author: Martin Lapierre, http://devinstinct.com
///////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Security;
using System.Web.Configuration;
namespace SubSonic
{
/// <summary>
/// Resolves access to configuration files for ASP.NET, EXEs and DLLs.
/// </summary>
public class ConfigurationResolver
{
#region Fields
/// <summary>
/// The assembly configuration file name.
/// </summary>
private string _assemblyConfigFile = null;
/// <summary>
/// The configuration file overrides.
/// </summary>
private string[] _configFileOverrides = null;
/// <summary>
/// The excluded files from mapped lookup.
/// </summary>
/// <remarks>
/// Some files may be unavailable due to security settings (trust, permissions).
/// The resolver ignores these files and falls back to the next accessible file.
/// </remarks>
private Dictionary<string, Exception> _excludedFiles = new Dictionary<string, Exception>();
#endregion Fields
#region Constructors
/// <summary>
/// Constructor.
/// </summary>
/// <remarks>
/// This constructor is not intended to be used directly:
/// use the ConfigurationProvider class instead.
/// Note that an assembly configuration file is not supported for Web application projects;
/// it is supported however for DLLs referenced by web applications.
/// </remarks>
internal ConfigurationResolver(Assembly assembly)
{
_assemblyConfigFile = GetAssemblyConfigFileName(assembly);
}
#endregion Constructors
#region Properties
/// <summary>
/// Returns the assembly configuration file that may be used by this instance.
/// </summary>
public string AssemblyConfigurationFile
{
get { return _assemblyConfigFile; }
}
/// <summary>
/// Returns the default configuration file that may be used by this instance.
/// </summary>
public string DefaultConfigurationFile
{
get { return AppDomain.CurrentDomain.SetupInformation.ConfigurationFile; }
}
/// <summary>
/// Gets or sets overrides for configuration files.
/// </summary>
/// <remarks>
/// Can be set at runtime to provide configuration files to
/// be used instead than the assembly and default config files.
/// </remarks>
public string[] ConfigFileOverrides
{
get { return _configFileOverrides; }
set
{
if (value == null)
_configFileOverrides = null;
else
InitConfigFileOverrides(value);
}
}
/// <summary>
/// Returns true if the current instance has configuration file overrides, false otherwise.
/// </summary>
public bool HasOverrides
{
get { return _configFileOverrides != null; }
}
/// <summary>
/// Returns true if the currently running application is a Web application, false otherwise.
/// </summary>
/// <remarks>
/// An application is set as a Web application if its configuration file is Web.config.
/// </remarks>
public bool IsRunningWebApp
{
get
{
return Path.GetFileName(DefaultConfigurationFile).ToLower().CompareTo("web.config") == 0;
}
}
/// <summary>
/// Gets the ConnectionStringsSection data for the current application's default configuration.
/// </summary>
/// <remarks>
/// The method uses the configuration file overrides, if set.
/// Otherwise, it tries to use the assembly configuration file and
/// falls back to app.config or web.config if the configuration information is not found.
/// </remarks>
public ConnectionStringSettingsCollection ConnectionStrings
{
get
{
ConnectionStringSettingsCollection connectionStrings = null;
try
{
Configuration cfg;
if (_configFileOverrides != null) // Use configuration overrides.
foreach (string fileName in _configFileOverrides)
{
cfg = OpenMappedConfiguration(fileName);
if (cfg != null)
{
connectionStrings = cfg.ConnectionStrings.ConnectionStrings;
if (cfg.ConnectionStrings.ElementInformation.IsPresent)
break;
}
}
else // Use runtime configuration files.
{
if (_assemblyConfigFile != null)
{
cfg = OpenMappedConfiguration(_assemblyConfigFile);
if (cfg != null)
{
if (cfg.ConnectionStrings.ElementInformation.IsPresent)
connectionStrings = cfg.ConnectionStrings.ConnectionStrings;
}
}
if (connectionStrings == null) // Fall back to default config file.
{
if (IsRunningWebApp)
connectionStrings = WebConfigurationManager.ConnectionStrings; // Web.config
else
connectionStrings = ConfigurationManager.ConnectionStrings; // App.config (Assembly.exe.config)
}
}
}
catch (Exception exception)
{
throw new ConfigurationErrorsException("Configuration error.", exception);
}
if (connectionStrings == null)
throw new ConfigurationErrorsException("Connection strings not found.");
return connectionStrings;
}
}
/// <summary>
/// Gets the AppSettingsSection data for the current application's default configuration.
/// </summary>
/// <remarks>
/// The method uses the configuration file overrides, if set.
/// Otherwise, it tries to use the assembly configuration file and
/// falls back to app.config or web.config if the configuration information is not found.
/// </remarks>
public NameValueCollection AppSettings
{
get
{
NameValueCollection appSettings = null;
try
{
Configuration cfg;
if (_configFileOverrides != null) // Use configuration overrides.
foreach (string fileName in _configFileOverrides)
{
cfg = OpenMappedConfiguration(fileName);
if (cfg != null)
{
// TODO: perf improvement; use caching until cfg changed.
appSettings = new NameValueCollection();
foreach (KeyValueConfigurationElement element in cfg.AppSettings.Settings)
appSettings.Add(element.Key, element.Value);
if (cfg.AppSettings.ElementInformation.IsPresent)
break;
}
}
else // Use runtime configuration files.
{
if (_assemblyConfigFile != null)
{
cfg = OpenMappedConfiguration(_assemblyConfigFile);
if (cfg != null)
{
if (cfg.AppSettings.ElementInformation.IsPresent)
{
// TODO: perf improvement; use caching until cfg changed.
appSettings = new NameValueCollection();
foreach (KeyValueConfigurationElement element in cfg.AppSettings.Settings)
appSettings.Add(element.Key, element.Value);
}
}
}
if (appSettings == null) // Fall back to default config file.
{
if (IsRunningWebApp)
appSettings = WebConfigurationManager.AppSettings; // Web.config
else
appSettings = ConfigurationManager.AppSettings; // App.config (Assembly.exe.config)
}
}
}
catch (Exception exception)
{
throw new ConfigurationErrorsException("Configuration error.", exception);
}
if (appSettings == null)
return new NameValueCollection();
return appSettings;
}
}
#endregion Properties
#region Methods
/// <summary>
/// Retrieves a specified configuration section for the current application's default configuration.
/// </summary>
/// <remarks>
/// The method uses the configuration file overrides, if set.
/// Otherwise, it tries to use the assembly configuration file and
/// falls back to app.config or web.config if the configuration information is not found.
/// </remarks>
/// <typeparam name="TSection">The type of the section to retrieve.</typeparam>
/// <param name="sectionName">The configuration section path and name.</param>
/// <returns>
/// The specified ConfigurationSection object.
/// Throws a ConfigurationErrorsException if the section does not exist.
/// </returns>
public TSection GetSection<TSection>(string sectionName) where TSection : ConfigurationSection
{
// Validate parameters.
if (string.IsNullOrEmpty(sectionName))
throw new ArgumentNullException("sectionName");
TSection section = null;
try
{
Configuration cfg;
if (_configFileOverrides != null) // Use configuration overrides.
foreach (string fileName in _configFileOverrides)
{
cfg = OpenMappedConfiguration(fileName);
if (cfg != null)
{
section = (TSection)cfg.GetSection(sectionName); // Throws if invalid section type.
if (section != null && section.ElementInformation.IsPresent)
break;
}
}
else // Use runtime configuration files.
{
if (_assemblyConfigFile != null)
{
cfg = OpenMappedConfiguration(_assemblyConfigFile);
if (cfg != null)
{
TSection candidateSection = (TSection)cfg.GetSection(sectionName); // Throws if invalid section type.
if (candidateSection != null && candidateSection.ElementInformation.IsPresent)
section = candidateSection;
}
}
if (section == null) // Fall back to default config file.
{
// Throws if invalid section type.
// Use (Web)ConfigurationManager.GetSection and not Configuration.GetSection
// (more performant as the first ones use caching - see help).
if (IsRunningWebApp)
section = (TSection)WebConfigurationManager.GetSection(sectionName); // Web.config
else
section = (TSection)ConfigurationManager.GetSection(sectionName); // App.config (Assembly.exe.config)
}
}
}
catch (Exception exception)
{
throw new ConfigurationErrorsException("Configuration error.", exception);
}
// TODO: maybe should return null; this is the default .NET behavior. But here it's safer.
if (section == null)
throw new ConfigurationErrorsException(string.Format("Section '{0}' not found.", sectionName));
return section;
}
/// <summary>
/// Finds a project configuration files from a specified path.
/// </summary>
/// <remarks>
/// The method starts by searching the given path, then goes up the directory hierarchy until
/// it finds the configuration files. It stops looking if a project, solution or the root
/// directory is reached.
/// </remarks>
/// <param name="path">
/// A file path or a directory path.
/// A directory path must end with Path.DirectorySeparatorChar.
/// </param>
/// <returns>The configuration files found.</returns>
public string[] FindProjectConfigFiles(string path)
{
List<string> configFiles = new List<string>();
string targetFile = null;
string[] filesFound = null;
string currentDirectory = Path.GetDirectoryName(path);
// Format directory correctly.
if (currentDirectory != string.Empty && !currentDirectory.EndsWith(Path.DirectorySeparatorChar.ToString()))
currentDirectory = string.Format("{0}{1}", currentDirectory, Path.DirectorySeparatorChar);
do
{
// First look for the assembly's configuration file,
// which has priority over Web.config and App.config
// (that is, must be the first in the list).
if (_assemblyConfigFile != null)
{
targetFile = string.Format("{0}{1}", currentDirectory, Path.GetFileName(_assemblyConfigFile));
if (File.Exists(targetFile))
configFiles.Add(targetFile);
}
// Then look for Web.config
targetFile = string.Format("{0}{1}", currentDirectory, "Web.config");
if (File.Exists(targetFile))
configFiles.Add(targetFile);
// Then look for App.config
targetFile = string.Format("{0}{1}", currentDirectory, "App.config");
if (File.Exists(targetFile))
configFiles.Add(targetFile);
// If at least one config file has been found, stop.
if (configFiles.Count != 0)
break;
// If we find a project file, stop.
filesFound = Directory.GetFiles(currentDirectory, "*.*proj", SearchOption.TopDirectoryOnly);
if (filesFound.Length != 0)
break;
// If we find a solution file, stop.
filesFound = Directory.GetFiles(currentDirectory, "*.sln", SearchOption.TopDirectoryOnly);
if (filesFound.Length != 0)
break;
// If we're at the root, stop.
if (currentDirectory == string.Empty || currentDirectory == Path.GetPathRoot(path))
break;
// Else we move one directory up.
currentDirectory = currentDirectory.Remove(currentDirectory.Length - 1); // Remove last Path.DirectorySeparatorChar
currentDirectory = currentDirectory.Substring(0, currentDirectory.LastIndexOf(Path.DirectorySeparatorChar) + 1);
}
while (true);
if (configFiles.Count == 0)
throw new Exception(string.Format("No project configuration found from '{0}'.", path));
return configFiles.ToArray();
}
/// <summary>
/// Opens the specified configuration file as a Configuration object.
/// </summary>
/// <param name="fileName">The configuration file name.</param>
/// <returns>
/// Returns null if the file is not found or not accessible because of security.
/// </returns>
protected System.Configuration.Configuration OpenMappedConfiguration(string fileName)
{
// Validate parameters.
if (string.IsNullOrEmpty(fileName))
throw new ArgumentNullException("fileName");
try
{
if (_excludedFiles.ContainsKey(fileName))
return null;
ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
fileMap.ExeConfigFilename = fileName; // Assumes already set to full path.
System.Configuration.Configuration cfg = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
return cfg.HasFile ? cfg : null;
}
catch (SecurityException exception)
{
_excludedFiles.Add(fileName, exception);
Trace.WriteLine(string.Format("Lookup for file '{0}' failed. The process will try to locate another valid configuration file. The reason for the error was: '{1}'.", fileName, exception.Message), "SubSonic.ConfigurationResolver");
return null;
}
}
/// <summary>
/// Gets the assembly configuration file name for the given assembly.
/// </summary>
/// <param name="assembly">The assembly to get the configuration file name for.</param>
/// <returns>The assembly configuration file.</returns>
protected string GetAssemblyConfigFileName(Assembly assembly)
{
// Validate parameters.
if (assembly == null)
throw new ArgumentNullException("assembly");
string fileName = string.Format("{0}.config", Path.GetFileNameWithoutExtension(assembly.CodeBase));
return GetFullPath(fileName);
}
/// <summary>
/// Initializes the configuration file overrides.
/// </summary>
/// <param name="fileNames">The list of configuration file overrides.</param>
protected void InitConfigFileOverrides(string[] fileNames)
{
// Validate parameters.
if (fileNames == null)
throw new ArgumentNullException("fileNames");
if (fileNames.Length == 0)
throw new ArgumentException("Array empty.", "fileNames");
_configFileOverrides = new string[fileNames.Length];
for (int iFileName = 0; iFileName < fileNames.Length; iFileName++)
{
if (string.IsNullOrEmpty(Path.GetFileName(fileNames[iFileName])))
throw new ArgumentOutOfRangeException("fileNames");
_configFileOverrides[iFileName] = GetFullPath(fileNames[iFileName]);
}
}
/// <summary>
/// Gets the full path for a given file name.
/// </summary>
/// <param name="fileName">The file name to get the full path for.</param>
/// <returns>The full path of the file.</returns>
protected string GetFullPath(string fileName)
{
if (string.IsNullOrEmpty(Path.GetDirectoryName(fileName)))
{
string appBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
if (!appBase.EndsWith(Path.DirectorySeparatorChar.ToString()))
appBase += Path.DirectorySeparatorChar;
return string.Format("{0}{1}", appBase, fileName);
}
else
return fileName;
}
#endregion Methods
}
}