Load libraries from app.config - c#

Say, for example, I have many methods for calculating the square root of a number.
One developer gives me his own .dll (maths1.dll), another one gives me his too (maths2.dll) and maybe a third one (maths3.dll).
All of them contains the same class, implementing the same interface.
Assembly 1 Maths1.dll
public class Maths : IMaths {
public static string Author = "Author1";
public static SquareRoot(int number) {
// Implementation method 1
}
}
Assembly 2 Maths2.dll
public class Maths : IMaths {
public static string Author = "Author2";
public static SquareRoot(int number) {
// Implementation method 2
}
}
etc. etc.
And I have a console application wich must be aware of all the dlls dynamically at runtime.
Looking for .dll files in code is undesirable.
// DON'T WANT THIS
DirectoryInfo di = new DirectoryInfo("bin");
FileInfo[] fi = di.GetFiles("*.dll");
My idea is to manage them from the app.config file with a custom configuration section.
<configuration>
<configSections>
<section name="MathsLibraries" type="MyMathsLibrariesSectionClass, ApplicationAssembly" />
</configSections>
<MathsLibraries>
<Library author="Author1" type="MathsClass, Maths1Assembly" /><!-- Maths1.dll -->
<Library author="Author2" type="MathsClass, Maths2Assembly" /><!-- Maths2.dll -->
<Library author="Author3" type="MathsClass, Maths3Assembly" /><!-- Maths3.dll -->
</MathsLibraries>
</configuration>
Considering I will manually copy the library file Maths1.dll to my application's bin folder.Then, the only thing I would have to do is, add a line to my app.config file in the MathsLibraries section.
I need an example code for the console application's Main, presenting the user all the dynamically linked .dll's and allowing him to calculate the square root of a number with the chosen library.
// NOT WORKING CODE, JUST IDEA OF WHAT IS NEEDED
public static void Main(string[] args) {
// Show the user the linked libraries
MathsLibraries libs = MathsLibraries.GetSection();
Console.WriteLine("Available libraries:");
foreach (MathLibrary lib in libs.Libraries) {
Console.WriteLine(lib.Author);
}
// Ask for the library to use
Console.Write("Which do you want to use?");
selected_option = Console.Read();
IMaths selected_library;
// since we don't know wich class would be,
// declare a variable using the interface we know they al implement.
// Assign the right class to the variable
if (selected_option == '1') {
selected_library = Assembly1.Maths;
} else if (selected_option == '2') {
selected_library = Assembly2.Maths;
}
// other options...
// Invoke the SquareRoot method of the dynamically loaded class
float sqr_result = selected_library.SquareRoot(100);
Console.WriteLine("Result is {0}", sqr_result);
Console.WriteLine("Press Enter key to exit");
Console.Read();
}
Please, can any one help me in this task of loading assemblies from app.config.
Detailed code would be appreciated.
Thanks!

Assuming they all implement the same interface (actually the same one, declared in the same assembly, not just the same definition in individual namespaces), you could use dependency injection like ms unity, which can be managed in config file, to register all implementations of this interface, create concrete implementations of all at run time, and execute them.
EDIT
Wrote a sample app, I'll post the meat here, and will provide a link to git hub or something when I get it uploaded.
I have an interface, IMasterInterface, and 3 implementations in separate assemblies 'UnityContainer.MasterImplementation', 'Satellite1.Implementation1' and 'Satellite2.Implementation2'. UnityConfiguration is a console app, and I have referenced unity using NuGet. For convenience, I have configured the build paths of all 3 assemblies to the same Build directory for Debug, so the 2 satellite assemblies are available to the console app.
IMasterInterface has a single method GetResult(): string.
Edit web config with the following:
<configuration>
<configSections>
<section name="unity"
type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
Microsoft.Practices.Unity.Configuration, Version=3.0.0.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</configSections>
<unity>
<typeAliases>
<typeAlias alias="IMasterInterface" type="UnityInjection.IMasterInterface, UnityInjection" />
<typeAlias alias="MasterImp" type="UnityInjection.MasterImplementation, UnityInjection" />
<typeAlias alias="SatelliteOneImplementation" type="Satellite1.Implementation1, Satellite1" />
<typeAlias alias="SatelliteTwoImplementation" type="Satellite2.Implementation2, Satellite2" />
</typeAliases>
<containers>
<container name="containerOne">
<types>
<type type="IMasterInterface" mapTo="MasterImp" name="Master" />
<type type="IMasterInterface" mapTo="SatelliteOneImplementation" name="One" />
<type type="IMasterInterface" mapTo="SatelliteTwoImplementation" name="Two" />
</types>
</container>
</containers>
</unity>
</configuration>
Configure the container
//Set up the dependency container
IUnityContainer myContainer = new UnityContainer();
var section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
section.Configure(myContainer, "containerOne");
Resolve All implementations
//create all implementations of out interface
var implementations = myContainer.ResolveAll<IMasterInterface>();
//call the method we are interested in for all implementations
foreach (var implementation in implementations)
{
Console.WriteLine(implementation.GetResult());
}
Resolve a specific named implementation
//Get a particular one
var specific = myContainer.Resolve<IMasterInterface>("Master");
Console.WriteLine(specific.GetResult());

You can use reflection to load selected library and create instance of required type.
var assembly = Assembly.LoadFrom("selected_math_library.dll");
var types = assembly.GetTypes();
var mathType = (from type in types
where type.GetInterface("IMath") != null && !type.IsAbstract
select type).ToList();
if (mathType.Count > 0)
{
IMath math = (IMath)Activator.CreateInstance(mathType);
// call methods from math
}

Possible duplicate of C# - Correct Way to Load Assembly, Find Class and Call Run() Method
var asm = Assembly.LoadFile(#"YourMathAssembly.dll");
var type = asm.GetType("Maths");
var sqrRoot = Activator.CreateInstance(Maths) as IMaths;
if (sqrRoot == null)
throw new Exception("broke");
sqrRoot .SquareRoot(100);

Related

Adding Assemblies/Types to be made available to Razor Page at Runtime

I'm trying to build a dynamic Web interface where I can dynamically point at a folder and serve Web content out of that folder with ASP.NET Core. This works fairly easily by using FileProviders in ASP.NET Core to re-route the Web root folder. This works both for StaticFiles and For RazorPages.
However, for RazorPages the problem is that once you do this you can't dynamically add references for additional types. I'd like to be able to optionally add a folder (PrivateBin) which on startup I can loop through, load the assemblies and then have those assemblies visible in Razor.
Unfortunately it doesn't work as Razor does not appear to see the loaded assemblies even when using runtime compilation.
I use the following during startup to load assemblies. Note the folder that these are loaded from are not in the default ContentRoot or WebRoot but in the new redirected WebRoot.
// WebRoot is a user chosen Path here specified via command line --WebRoot c:\temp\web
private void LoadPrivateBinAssemblies()
{
var binPath = Path.Combine(WebRoot, "PrivateBin");
if (Directory.Exists(binPath))
{
var files = Directory.GetFiles(binPath);
foreach (var file in files)
{
if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) &&
!file.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
continue;
try
{
var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
Console.WriteLine("Additional Assembly: " + file);
}
catch (Exception ex)
{
Console.WriteLine("Failed to load private assembly: " + file);
}
}
}
}
The assembly loads into the AssemblyLoadContext() and I can - using Reflection and Type.GetType("namespace.class,assembly") - access the type.
However, when I try to access the type in RazorPages - even with Runtime Compilation enabled - the types are not available. I get the following error:
To make sure that the type is indeed available, I checked that I can do the following inside of Razor:
#{
var md = Type.GetType("Westwind.AspNetCore.Markdown.Markdown,Westwind.AspNetCore.Markdown");
var mdText = md.InvokeMember("Parse", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null,
null, new object[] { "**asdasd**", false, false, false });
}
#mdText
and that works fine. So the assembly is loaded and the type is accessible, but Razor doesn't appear to be aware of it.
So the question is:
Is it possible to load assemblies at runtime and make them available to Razor with Runtime Compilation, and use it like you normally would use a type via direct declarative access?
It turns out the solution to this is via the Razor Runtime Compilation Options which allow adding of extra 'ReferencePaths', and then explicitly loading assemblies.
In ConfigureServices():
services.AddRazorPages(opt => { opt.RootDirectory = "/"; })
.AddRazorRuntimeCompilation(
opt =>
{
opt.FileProviders.Add(new PhysicalFileProvider(WebRoot));
LoadPrivateBinAssemblies(opt);
});
then:
private void LoadPrivateBinAssemblies(MvcRazorRuntimeCompilationOptions opt)
{
var binPath = Path.Combine(WebRoot, "PrivateBin");
if (Directory.Exists(binPath))
{
var files = Directory.GetFiles(binPath);
foreach (var file in files)
{
if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) &&
!file.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
continue;
try
{
var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
opt.AdditionalReferencePaths.Add(file);
}
catch (Exception ex)
{
...
}
}
}
}
The key is:
opt.AdditionalReferencePaths.Add(file);
which makes the assembly visible to Razor, but doesn't actually load it. To load it you then have to explicitly load it with:
AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
which loads the assembly from a path. Note that any dependencies that this assembly has have to be available either in the application's startup path or in the same folder you're loading from.
Note: Load order for dependencies may be important here or a not previously added assembly may not be found as a dependency (untested).
A Quick look into the ASP.NET Core source code reveals:
All Razor view Compilations start at:
RuntimeViewCompiler.CreateCompilation(..)
which uses:
CSharpCompiler.Create(.., .., references: ..)
which uses:
RazorReferenceManager.CompilationReferences
which uses: see code on github
// simplyfied
var referencePaths = ApplicationPartManager.ApplicationParts
.OfType<ICompilationReferencesProvider>()
.SelectMany(_ => _.GetReferencePaths())
which uses:
ApplicationPartManager.ApplicationParts
So we need somehow register our own ICompilationReferencesProvider and this is how..
ApplicationPartManager
While it's search for Application parts does the ApplicationPartManager a few things:
it searchs for hidden Assemblies reading attributes like:
[assembly: ApplicationPartAttribute(assemblyName:"..")] // Specifies an assembly to be added as an ApplicationPart
[assembly: RelatedAssemblyAttribute(assemblyFileName:"..")] // Specifies a assembly to load as part of MVC's assembly discovery mechanism.
// plus `Assembly.GetEntryAssembly()` gets added automaticly behind the scenes.
Then it loops throuth all found Assemblies and uses ApplicationPartFactory.GetApplicationPartFactory(assembly) (as seen in line 69) to find types which extend ApplicationPartFactory.
Then it invokes the method GetApplicationParts(assembly) on all found ApplicationPartFactorys.
All Assemblies without ApplicationPartFactory get the DefaultApplicationPartFactory which returns new AssemblyPart(assembly) in GetApplicationParts.
public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);
GetApplicationPartFactory
GetApplicationPartFactory searches for [assembly: ProvideApplicationPartFactory(typeof(SomeType))] then it uses SomeType as factory.
public abstract class ApplicationPartFactory {
public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);
public static ApplicationPartFactory GetApplicationPartFactory(Assembly assembly)
{
// ...
var provideAttribute = assembly.GetCustomAttribute<ProvideApplicationPartFactoryAttribute>();
if (provideAttribute == null)
{
return DefaultApplicationPartFactory.Instance; // this registers `assembly` as `new AssemblyPart(assembly)`
}
var type = provideAttribute.GetFactoryType();
// ...
return (ApplicationPartFactory)Activator.CreateInstance(type);
}
}
One Solution
This means we can create and register (using ProvideApplicationPartFactoryAttribute) our own ApplicationPartFactory which returns a custom ApplicationPart implementation which implements ICompilationReferencesProvider and then returns our references in GetReferencePaths.
[assembly: ProvideApplicationPartFactory(typeof(MyApplicationPartFactory))]
namespace WebApplication1 {
public class MyApplicationPartFactory : ApplicationPartFactory {
public override IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly)
{
yield return new CompilationReferencesProviderAssemblyPart(assembly);
}
}
public class CompilationReferencesProviderAssemblyPart : AssemblyPart, ICompilationReferencesProvider {
private readonly Assembly _assembly;
public CompilationReferencesProviderAssemblyPart(Assembly assembly) : base(assembly)
{
_assembly = assembly;
}
public IEnumerable<string> GetReferencePaths()
{
// your `LoadPrivateBinAssemblies()` method needs to be called before the next line executes!
// So you should load all private bin's before the first RazorPage gets requested.
return AssemblyLoadContext.GetLoadContext(_assembly).Assemblies
.Where(_ => !_.IsDynamic)
.Select(_ => new Uri(_.CodeBase).LocalPath);
}
}
}
My Working Test Setup:
ASP.NET Core 3 WebApplication
ASP.NET Core 3 ClassLibrary
Both Projects have no reference to each other.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Content Remove="Pages\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.0.0" />
</ItemGroup>
</Project>
services
.AddRazorPages()
.AddRazorRuntimeCompilation();
AssemblyLoadContext.Default.LoadFromAssemblyPath(#"C:\path\to\ClassLibrary1.dll");
// plus the MyApplicationPartFactory and attribute from above.
~/Pages/Index.cshtml
#page
<pre>
output: [
#(
new ClassLibrary1.Class1().Method1()
)
]
</pre>
And it shows the expected output:
output: [
Hallo, World!
]
Have a nice day.

Where do I insert this data into my code?

I want to set a cache limit for my C# program. My program is creating files periodically and saving them to a folder. I want it so if the folder (C:\SysApp) hits this limit (150000KB) it will automatically start deleting the files starting with the oldest ones deleting only a certain amount at a time (149900KB).
So far I have this code:
private void DeleteOldFilesIfOverFolderLimit(string folderPath,
long folderSizeLimit,
long amountToDelete)
{
var folder = new DirectoryInfo(folderPath);
var files = folder.GetFiles();
var folderSize = files.Sum(fi => fi.Length);
if (folderSize > folderSizeLimit)
{
// Sort the list of files with the oldest first.
Array.Sort(files,
(fi1, fi2) => fi1.CreationTime.CompareTo(fi2.CreationTime));
var amountDeleted = 0L;
foreach (var file in files)
{
amountDeleted += file.Length;
file.Delete();
if (amountDeleted >= amountToDelete)
{
break;
}
}
}
}
I'm just trying to figure out where I need to insert the specific data for my program (given in first paragraph in parenthesis).
I'm using Visual Studio Community 2015.
Click on the project options and select settings:
Enter the default values
and your program can read these values (from the .settings file or the defaults) using the following code
static void Main(string[] args)
{
string folder=Properties.Settings.Default.folder;
long limit=Properties.Settings.Default.sizeLimit;
long delete=Properties.Settings.Default.toDelete;
}
If you change the settings value in the program you need to save the new values before exiting the application. This is done with Properties.Settings.Default.Save();. This command creates a .config file with your values. These are read automatically when the program starts.
The contents are an XML file with the settings values clearly visible.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="SO_KeepStettings.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
</sectionGroup>
</configSections>
<userSettings>
<SO_KeepStettings.Properties.Settings>
<setting name="folder" serializeAs="String">
<value>C:\SysApp</value>
</setting>
<setting name="sizeLimit" serializeAs="String">
<value>157286400</value>
</setting>
<setting name="toDelete" serializeAs="String">
<value>156237824</value>
</setting>
</SO_KeepStettings.Properties.Settings>
</userSettings>
</configuration>
Deleting files is sketchy, if you accidentally pass in a malformed string or simply the wrong string then you will be losing data, such as accidentally deleting your only copy of your PhD thesis or destroying your operating system. So, I'm just going to show you how to call a method instead of showing you how to use this code you were given:
Starting with a Hello World! example:
public class Hello1
{
public static void Main()
{
System.Console.WriteLine("Hello, World!");
// This is here only to pause the console window so it stays open.
System.Console.ReadLine();
}
}
Now let's implement our own method to print any string to the console:
public class Hello1
{
public static void Main()
{
PrintToConsole("Hello World!");
// This is here only to pause the console window so it stays open.
System.Console.ReadLine();
}
private static void PrintToConsole(string stringToPrintToConsole)
{
System.Console.WriteLine(stringToPrintToConsole);
}
}
Lastly, let's pass in another parameter to control how many times the line is printed:
public class Hello1
{
public static void Main()
{
PrintToConsole("Hello World!", 5);
// This is here only to pause the console window so it stays open.
System.Console.ReadLine();
}
private static void PrintToConsole(string stringToPrintToConsole, long numberOfTimesToPrint)
{
for (int i = 0; i < numberOfTimesToPrint; i++)
{
System.Console.WriteLine(stringToPrintToConsole);
}
}
}
You were given a method but no where are you calling that method. Note how in my program I have to call PrintToConsole() somewhere, and this being a console application the entire program starts and finishes in Main() so that is where I put the code. You could technically take the body of your method and paste it directly into Main, then everywhere you see the usage of the variable folderPath you replace it with the actual string "C:\\SysApp" and likewise with the other 2 parameters and the program would work the same.

How to load a custom .config file with Configuration

I'm not sure I am going about this right, but I am trying to write a custom configuration file for an asp.NET web project. I want to make it clear this is not a windows form, because half the stuff I find is only for those. I am trying to read and write to this file to change a couple of application settings.
I wrote this huge class using this tutorial. Here's a simplified version:
namespace Tedski.Configuration {
public class TedskiSection : ConfigurationSection {
private static ConfigurationProperty s_propName;
private static ConfigurationPropertyCollection s_properties;
static TedskiSection() {
s_propName = new ConfigurationProperty(
"name",
typeof(string),
null,
ConfigurationPropertyOptions.IsRequired
);
s_properties = new ConfigurationPropertyCollection();
s_properties.Add(s_propName);
}
protected override ConfigurationPropertyCollection Properties {
get { return s_properties; }
}
[ConfigurationProperty("name")]
public string Name {
get {
return (string)base[s_propName];
}
set {
base[s_propName] = value;
}
}
}
}
I am now not sure where to define my configuration. I can put this in my Web.config file like this:
<configuration>
<configSections>
<section name="Tedski" type="Tedski.Configuration.TedskiSection" />
</configSections>
<Tedski name="Ted" />
</configuration>
and everything loads up fine with this:
TedskiSection section = ConfigurationManager.GetSection("Tedski") as TedskiSection;
Console.WriteLine(section.Name); //produces "Ted"
However, I need to be able to load this up with the Configuration object, in order to be able to call Configuration.Save(). I can't seem to load up that specific section and save the Web.config (from what I understand this is dangerous). Another solution I'm trying out is creating a separate .config file (Tedski.config) with the same XML syntax as defined above.
I tried using this answer to load up Tedski.config, but I get an error:
ExeConfigurationFileMap configMap = new ExeConfigurationFileMap();
configMap.ExeConfigFilename = Server.MapPath("~/Tedski.config");
Configuration config = ConfigurationManager.OpenMappedExeConfiguration(configMap, ConfigUserLevel.None);
TedskiSection section = config.GetSection("Tedski") as TedskiSection; //fails
ConfigurationErrorsException "An error occurred creating the
configuration section handler for Tedski: Could not load type
'Tedski.Configuration.TedskiSection' from assembly
'System.Configuration, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a'
If I got this to load, I could then modify section.Name and call config.Save(), but I'm stuck here.
In your type property, you have to tell it which assembly contains your Tedski.Configuration.TedskiSection. For example:
<section name="Tedski" type="Tedski.Configuration.TedskiSection, TedskiAssemblyName" />
Replace "TedskiAssemblyName" there with the name of the assembly that contains the class.

App config for dynamically loaded assemblies

I'm trying to load modules into my application dynamically, but I want to specify separate app.config files for each one.
Say I have following app.config setting for main app:
<appSettings>
<add key="House" value="Stark"/>
<add key="Motto" value="Winter is coming."/>
</appSettings>
And another for library that I load using Assembly.LoadFrom:
<appSettings>
<add key="House" value="Lannister"/>
<add key="Motto" value="Hear me roar!"/>
</appSettings>
Both libraries have a class implementing the same interface, with the following method:
public string Name
{
get { return ConfigurationManager.AppSettings["House"]; }
}
And sure enough calls to Name from both main class and loaded assembly class output Stark.
Is there a way to make main app use its own app.config and each loaded assembly use theirs? Names of config files are different in the output, so that should be possible I think.
Ok, here's the simple solution I ended up with:
Create the follow function in the utility library:
public static Configuration LoadConfig()
{
Assembly currentAssembly = Assembly.GetCallingAssembly();
return ConfigurationManager.OpenExeConfiguration(currentAssembly.Location);
}
Using it in dynamically loaded libraries like this:
private static readonly Configuration Config = ConfigHelpers.LoadConfig();
No matter how that library gets loaded it uses the correct config file.
Edit:
This might be the better solution for loading files into ASP.NET applications:
public static Configuration LoadConfig()
{
Assembly currentAssembly = Assembly.GetCallingAssembly();
string configPath = new Uri(currentAssembly.CodeBase).LocalPath;
return ConfigurationManager.OpenExeConfiguration(configPath);
}
To copy file after build you might want to add the following line to post-build events for asp app (pulling the config from library):
copy "$(SolutionDir)<YourLibProjectName>\$(OutDir)$(Configuration)\<YourLibProjectName>.dll.config" "$(ProjectDir)$(OutDir)"
As far as I know, you need separate application domains for the app.config to work separately. The creation of an AppDomainSetup allows you to specify which config file to use. Here's how I do it:
try
{
//Create the new application domain
AppDomainSetup ads = new AppDomainSetup();
ads.ApplicationBase = Path.GetDirectoryName(config.ExePath) + #"\";
ads.ConfigurationFile =
Path.GetDirectoryName(config.ExePath) + #"\" + config.ExeName + ".config";
ads.ShadowCopyFiles = "false";
ads.ApplicationName = config.ExeName;
AppDomain newDomain = AppDomain.CreateDomain(config.ExeName + " Domain",
AppDomain.CurrentDomain.Evidence, ads);
//Execute the application in the new appdomain
retValue = newDomain.ExecuteAssembly(config.ExePath,
AppDomain.CurrentDomain.Evidence, null);
//Unload the application domain
AppDomain.Unload(newDomain);
}
catch (Exception e)
{
Trace.WriteLine("APPLICATION LOADER: Failed to start application at: " +
config.ExePath);
HandleTerminalError(e);
}
Another way you could go about getting the desired effect would be to implement your configuration values inside a resource file compiled into each of your DLLs. A simple interface over the configuration object would allow you to switch out looking in an app.config versus looking in a resource file.
It may work if you change the code little bit:
public string Name
{
get {
Configuration conf = ConfigurationManager.OpenExeConfiguration("library.dll");
return conf.AppSettings.Settings["House"].Value;
}
}

Loading Properties.Settings from a different file at runtime

Is there any way to load settings from a different file other than the default App.config file at runtime? I'd like to do this after the default config file is loaded.
I use the Settings.Settings GUI in Visual Studio to create my App.config file for me. The config file ends up looking like this:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="SnipetTester.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</sectionGroup>
</configSections>
<applicationSettings>
<SnipetTester.Properties.Settings>
<setting name="SettingSomething" serializeAs="String">
<value>1234</value>
</setting>
</SnipetTester.Properties.Settings>
</applicationSettings>
</configuration>
In code, I'm able to access the settings like this:
Console.WriteLine("Default setting value: " + Properties.Settings.Default.SettingSomething);
The idea is that when the application is run, I should be able to specify a config file at run time and have the application load the config file into the Properties.Settings.Default object instead of using the default app.config file. The formats of the config files would be the same, but the values of the settings would be different.
I know of a way to do this with the ConfigurationManager.OpenExeConfiguration(configFile);. However, in the tests that I've run, it doesn't update the Properties.Settings.Default object to reflect the new values from the config file.
After thinking about this a bit longer, I've been able to come up with a solution that I like a little better. I'm sure it has some pitfalls, but I think it'll work for what I need it to do.
Essentially, the Properties.Settings class is automatically generated by Visual Studio; it generates the code for the class for you. I was able to find where the code was generated and add a few function calls to load a config file on its own. Here's my addition:
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
//Parses a config file and loads its settings
public void Load(string filename)
{
System.Xml.Linq.XElement xml = null;
try
{
string text = System.IO.File.ReadAllText(filename);
xml = System.Xml.Linq.XElement.Parse(text);
}
catch
{
//Pokemon catch statement (gotta catch 'em all)
//If some exception occurs while loading the file,
//assume either the file was unable to be read or
//the config file is not in the right format.
//The xml variable will be null and none of the
//settings will be loaded.
}
if(xml != null)
{
foreach(System.Xml.Linq.XElement currentElement in xml.Elements())
{
switch (currentElement.Name.LocalName)
{
case "userSettings":
case "applicationSettings":
foreach (System.Xml.Linq.XElement settingNamespace in currentElement.Elements())
{
if (settingNamespace.Name.LocalName == "SnipetTester.Properties.Settings")
{
foreach (System.Xml.Linq.XElement setting in settingNamespace.Elements())
{
LoadSetting(setting);
}
}
}
break;
default:
break;
}
}
}
}
//Loads a setting based on it's xml representation in the config file
private void LoadSetting(System.Xml.Linq.XElement setting)
{
string name = null, type = null, value = null;
if (setting.Name.LocalName == "setting")
{
System.Xml.Linq.XAttribute xName = setting.Attribute("name");
if (xName != null)
{
name = xName.Value;
}
System.Xml.Linq.XAttribute xSerialize = setting.Attribute("serializeAs");
if (xSerialize != null)
{
type = xSerialize.Value;
}
System.Xml.Linq.XElement xValue = setting.Element("value");
if (xValue != null)
{
value = xValue.Value;
}
}
if (string.IsNullOrEmpty(name) == false &&
string.IsNullOrEmpty(type) == false &&
string.IsNullOrEmpty(value) == false)
{
switch (name)
{
//One of the pitfalls is that everytime you add a new
//setting to the config file, you will need to add another
//case to the switch statement.
case "SettingSomething":
this[name] = value;
break;
default:
break;
}
}
}
}
The code I added exposes an Properties.Settings.Load(string filename) function. The function accepts a config filename as a parameter. It will parse the file and load up any settings it encounters in the config file. To revert back to the original configuration, simply call Properties.Settings.Reload().
Hope this might help someone else!
Look at using ExeConfigurationFileMap and ConfigurationManager.OpenMappedExeConfiguration.
See Cracking the Mysteries of .Net 2.0 Configuration
The ExeConfigurationFileMap allows you to specifically configure the
exact pathnames to machine, exe, roaming and local configuration
files, all together, or piecemeal, when calling
OpenMappedExeConfiguration(). You are not required to specify all
files, but all files will be identified and merged when the
Configuration object is created. When using
OpenMappedExeConfiguration, it is important to understand that all
levels of configuration up through the level you request will always
be merged. If you specify a custom exe and local configuration file,
but do not specify a machine and roaming file, the default machine and
roaming files will be found and merged with the specified exe and user
files. This can have unexpected consequences if the specified files
have not been kept properly in sync with default files.
It depends on the type of the application:
Web Application & Windows Application - use the configSource xml attribute if you are willing to store the config files in the same folder (or subfolders) as the application
Create a settings provider and also implement IApplicationSettingsProvider. Samples here and here. You might also need to use the IConfigurationManagerInternal interface to replace the default .NET configuration manager. When implementing the provider don't forget to make a difference between user settings and application settings and the roaming profiles.
If you want to get started quickly just decompile the LocalFileSettingsProvider class (the default settings provider) and change it to your needs (you might find some useles code and might need to replicate all of the classes on which it depends).
Good luck
You can include the types so you don't need to manually update the source every time.
`private void LoadSetting(System.Xml.Linq.XElement setting)
{
string name = null, type = null;
string value = null;
if (setting.Name.LocalName == "setting")
{
System.Xml.Linq.XAttribute xName = setting.Attribute("name");
if (xName != null)
{
name = xName.Value;
}
System.Xml.Linq.XAttribute xSerialize = setting.Attribute("serializeAs");
if (xSerialize != null)
{
type = xSerialize.Value;
}
System.Xml.Linq.XElement xValue = setting.Element("value");
if (xValue != null)
{
if (this[name].GetType() == typeof(System.Collections.Specialized.StringCollection))
{
foreach (string s in xValue.Element("ArrayOfString").Elements())
{
if (!((System.Collections.Specialized.StringCollection)this[name]).Contains(s))
((System.Collections.Specialized.StringCollection)this[name]).Add(s);
}
}
else
{
value = xValue.Value;
}
if (this[name].GetType() == typeof(int))
{
this[name] = int.Parse(value);
}
else if (this[name].GetType() == typeof(bool))
{
this[name] = bool.Parse(value);
}
else
{
this[name] = value;
}
}
}`

Categories