0

I have a static C# library project which relies on configuration. This library may be used in two scenarios:

  1. From managed C# applications. Easy - the application's app.config file will be used, via ConfigurationManager
  2. From unmanaged C++ applications (using COM)

In case 2. there is no app.config. I would like the library to still be able to use ConfigurationManager but it should explicitly load an XML config file with the same structure as app.config.

This question talks about how to manually load a config file: Loading custom configuration files

But how can the library detect which case 1/2 its in? I would be happy with an InitLib method which passes a config name, or a static initializer, but I can't see how to put the pieces together.

The config file name to use in case 2 could either be passed in directly, or hardcoded as MyAssembly.Lib.config or similar.

Mr. Boy
  • 60,845
  • 93
  • 320
  • 589
  • You could always include an XML file as an embedded resource. – mm8 Jun 24 '20 at 15:45
  • I suppose two questions on that: 1. can this be used through `ConfigurationManager` - I want the library to work in a regular c# application too. 2. Can this file be edited on a target system? I'm happy supplying a separate config file just not sure how to load it – Mr. Boy Jun 24 '20 at 15:54
  • How would the library work in a regular app? The `CongifurationManager` will always read the config file of the executable rather than the one you provide in your library anyway so this won't work. – mm8 Jun 24 '20 at 15:55
  • I mean I would like to use regular configuration classes to read this file, not roll some custom XML reading - how can I load a bespoke XML file and use it from `ConfigurationManager`? – Mr. Boy Jun 24 '20 at 16:03
  • https://stackoverflow.com/questions/505566/loading-custom-configuration-files – mm8 Jun 24 '20 at 16:05
  • @mm8 I literally found that just as you were posting the link. Thanks – Mr. Boy Jun 24 '20 at 16:07
  • It's still not at all clear what you're after and why the linked question doesn't answer it. Is there or isn't there managed code involved in your scenario? If there is, why not have the managed code load it and pass it to the unmanaged code (or have the unmanaged code call back)? If there isn't, why would `ConfigurationManager` even be relevant as opposed to just reading the XML file with whatever unmanaged library you prefer? – Jeroen Mostert Jun 24 '20 at 16:53
  • @JeroenMostert the C# library needs to access configuration from an app.config style file. When this library is used from a maanged C# application, it should use the application's app.config. When the library is used from unmanaged code, it should explicitly load a specified file. The unmanaged code has no part to play in loading config. Let me see if I can re-word the question a bit. – Mr. Boy Jun 25 '20 at 09:37
  • @JeroenMostert basically rewritten the whole question. I hope this is clearer. # – Mr. Boy Jun 25 '20 at 09:45

1 Answers1

1

So to clarify: you have a class in a class library, the library references System.Configuration.ConfigurationManager.dll, and the class looks does something like this:

using System.Configuration;

namespace FooLibrary
{
    public class Foo
    {
        public Foo()
        {
            var bar = ConfigurationManager.AppSettings("FooLibrary.Foo.Bar");
        }
    }
}

Now when you call this class from a console application, say, Baz.exe, there will exist a Baz.exe.config:

<?xml version="1.0"?>
<configuration>
    <appSettings>
        <add key="FooLibrary.Foo.Bar" value="Baz" />
    </appSettings>
</configuration>

And all works.

It doesn't work when the library runs in a context without a configuration file. While I think it's possible to give an unmanaged application, say, Qux.exe a configuration file Qux.exe.config which .NET will then read for assemblies loaded from that executable, that situation isn't ideal. Even if that'd work (I think, but am not sure, that it's just a file name convention, not something the runtime does for executables on startup). It's possible that the executable running the assembly is not under your control anyway (e.g. somewhere in System32).

While you could let the library load a configuration file relative to its own location, and to answer your question:

But how can the library detect which case 1/2 its in?

You could just test for the AppSettings key anyway, and if not found, assume you've got no configuration file, and open that of the DLL instead:

public class Foo
{
    private readonly string _bar;
    public Foo()
    {   
        // Read from .exe.config or Web.config
        _bar = ConfigurationManager.AppSettings("FooLibrary.Foo.Bar");
        if (string.IsNullOrEmpty(_bar))
        {
            // Assume that if not present, own config must be loaded:
            var dllConfigPath = Assembly.GetExecutingAssembly().Location;
            
            // Will just append ".config" despite its name
            Configuration config = ConfigurationManager.OpenExeConfiguration(dllConfigPath);
            _bar = config.AppSettings.Settings["test"]?.Value;
            
            if (string.IsNullOrEmpty(_bar))
            {
                // Not found in both configs, now what?
            }
        }
    }
}

And you should refactor it a bit to read multiple variables. And now you want to unit test this code, so you'll have to provide a configuration file there as well.

These problems are solved by not reading the configuration in class libraries, but in application startup, and passing the appropriate values to the constructor of the classes from the library:

public Foo(string bar) { ... }

// And then from your executable:
var foo = new Foo(ConfigurationManager.AppSettings["FooLibrary.Foo.Bar"));

// And your unit test
var foo = new Foo("FromTest");

But you can't pass constructor parameters through COM, which the compiler will tell you as well. So you'll have to provide a parameterless constructor, and something like an InitLib(string bar) method for COM only.

And if you, despite all the above, still insist on reading the config there, then it would look something like this:

public class Foo
{
    private string _bar;
    
    public Foo()
    {   
        // Read from .exe.config or Web.config
        _bar = ConfigurationManager.AppSettings["FooLibrary.Foo.Bar"];
        
        // Can't throw here if _bar is empty, maybe we're called from COM
    }
    
    public void ReadDllConfig()
    {
        // Assume that if not present, own config must be loaded:
        var dllConfigPath = Assembly.GetExecutingAssembly().Location;
        
        // Will just append ".config" despite its name
        Configuration config = ConfigurationManager.OpenExeConfiguration(dllConfigPath);
        _bar = config.AppSettings.Settings["test"]?.Value;
        
        // Can throw here if _bar is empty
    }
    
    public void FooForCom()
    {
        // TODO: test _bar again, it can still be null.
    }
}
CodeCaster
  • 147,647
  • 23
  • 218
  • 272