For the past several years, I’ve worked at a company where we write a support a developer framework. As part of that work, I’ve come to understand some of the challenges of writing a component, as opposed to writing an application.
One main thing that is different, is you have to make your component easy to use. You do that by using XML code comments and publishing component document. You also use those XML code comments to give robust and complete Intellisense. But, what about config files?
If you are writing a framework, at some point <appSettings> simply won’t be enough. Even aside from that, it’s not really a great idea to even use <appSettings> for your framework, because now you are messing up a section specifically intended for the end-developer.
However, you will likely not want to hard-code values in your code – and you’d instead want to get values from a config file – so how do we do that? The answer of course is to create your own configuration sections – just like <system.web> or <system.diagnostic> But how do you do that?
This article will go into excruciating detail on just that topic. We will cover all the typical ways you would structure your configuration, and even talk about how to easily give your developers Intellisense inside Visual Studio when they go use your configuration.
I’ve had to learn, and then re-learn this two full times now, so for my own benefit, I’m including a lot of detail. If you (assuming YOU are not me, Future Robert), ran across this page, this is absolutely everything you need, including sample code, to create complete configuration for your component.
Get Your Framework Project Ready:
I have a complete, working sample application (see attachment at the end). I’ve created a fictitious “SederSoftware.Components” library that I want developers to use. This component requires some fictitious configuration.
When you have a section in your configuration file like <system.web>, that isn’t just some random XML, there is a data structure, a class, that matches up to every one of those elements and attributes that correspond to properties of that class.
So, we need to start by creating data structures for each chunk of data that we want to store. I have found it’s best to start by brainstorming what you want the developer experience to be. If you write a developer framework, you already know you MUST put forethought into every design decision you make. For example, you might not think twice and just decide to put something like this in your configuration:
the problem is what about when you have a user that needs to connect to more than one domain? Uh-oh. So instead, you could use an AddRemoveClearMap for example that supports <add …/>, <remove name=””/> and <clear /> – which is sort of a config-file-way of storing a proper collection:
All my point is here is spend the time to figure out what you want the user-experience to be. If you have to change your config in a future release, it’s a pain for everyone. Developers are going to be confused and annoyed, you are going to get more calls, and have to support 2 different documentation streams – one from “the old way we did it” and “the new way we do it”. So, take the time and get the structure right, if you can.
As a matter of convention, files related to having custom configuration, should go in a “Configuration” folder and matching namespace. In the example above, the convention would have me assume that this is configuration for Contoso.Framework.dll, and that the underlying configuration classes are probably in the Contoso.Framework.Configuration namespace.
|IMPORTANT: You need to add a reference to System.Configuration in order to find the classes that are going to talk about next. This concept and the reference to System.Configuration has been the same since at least 2.0 and still works in 4.0 – so just add whatever the current version is that your project is using.|
Different Kinds of Sections/Collections:
Let’s say that you actually have your configuration. In the sample project attached, I came up with this example for a specific reason:
This contains 3 of the most common types of sections, which should cover a vast majority of needs. These are:
- Section Group – inherits from System.Configuration.ConfigurationSectionGroup, can only contain sections, can’t have any attributes (as far as I know).
- Section – inherits from System.Configuration.ConfigurationSection, the workhorse. This is an XML element that has attributes, but it can’t contain sub-elements (as far as I know)
- Element Collection – inherits from System.Configuration.ConfigurationElementCollection. This let’s you have those AddRemoveClearMaps – like above, that <settings> element.
I don’t know if this will help, but here is that same config, with some comments as to what XML elements in the configuration, correspond to which types of classes:
For most cases (that I can think of), this should really cover what you need. You can use a ConfigurationSectionGroup or ConfigurationElementCollection as a “container” and you can use ConfigurationSection or ConfigurationElement as data structures to hold the actual values that you want to store.
Creating a Chain of Configuration:
OK, so at this point, we have an idea of what kind of config we want to support, and what kind of data structure we need to create (and what to inherit from), so how do we actually create this? Using the sample config above, let me show each piece. I will start at the top, with <sederSoftware.components> – that XML is represented by a class like this:
If we walk down into GeneralSettings (or <generalSettings>), that is it’s own data structure too. That looks like this:
The properties of these translate to attributes in the configuration file. So, the Enabled property of the class corresponds to <generalSettings enabled=””/> attribute.
Now, the other element at the same level of <generalSettings>, is a little trickier. I haven’t found a better way to do this. You need a container first, then you can create that AddRemoveClearMap. So, I have a Section called <contextSettings>, and then I point to the ConfigurationElementCollection below that. So, that container <contextSettings> looks like this:
This points to that ContextSettingsElementCollection, which is the actual AddRemoveClearMap. That class, I think needs this – at it’s most simplest:
and then lastly, the actual ContextSettingElement looks something like this:
understanding that I have an enum defined for PartOfDay, as this:
It’s worth mentioning that I have an extra layer in-between – and that is because I don’t think you can add a ConfigurationElementCollection directly to a Section Group. So, that element collection needs to be a child of a section, which is a child of the section group.
I don’t know why they did that, that just seems to be how it works.
Let’s put all of this together. All we’ve done is create some data structures (classes) that represent the hierarchal data. So, if I want access to that <generalSettings enabled=”true”/> value, I could do something like this in code:
From that top-level section group, I can walk into those properties and collection and get access to all of the configuration values, in a strongly-typed way. There is no XPath to deal with or anything like that, I just reference these data structures.
But wait, we have data structures to hold the settings, we sort of “chained” them together to match the same hierarchy of the config file – but do we ACTUALLY read those value? We’ll cover that below. First, we need to actually create a valid configuration file.
Define <configSections> in the Consuming Application:
I know what you’re saying “Hey Rob, you dum-dum, we already did that, my custom configuration is already in my app.config!”. That’s great, but that’s not all there is to it.
In order for you to adapt these XML settings into strongly-typed objects later, you need to tell the .NET run-time WHICH classes handle which XML elements. You do this with a <configSections> at the top of the configuration file. If you have a <configSections>, it MUST be the first element under <configuration>. Here is how I defined my sample:
As you can see, there are a couple of important things here:
- Use <sectionGroup> for a ConfigurationSectionGroup, use <section> for ConfigurationSection
- The “name” attribute value must match your XML element. In the case above, I have name=”generalSettings” – if that element has a different name, this will fail.
- These are version-specific! More on that below…
If you are developing a framework, this is important: you might think “this is kind of ugly, why don’t I just add my <configSections> to the machine.config and be done with it?”. First, I agree, this <configSections> is kind of ugly, but this is the approach will allow your developers to use multiple versions of your components with multiple projects.
Imagine your developer has version 3.1, and 3.5 of your component framework installed. As part of the install, you modify the machine.config and add in these <configSections>. So, when you installed 3.5, it replaced the 3.1 definition. When the developer has an app open that uses 3.1, the <configSections> defined in the machine.config will point to 3.5 and you will get a run-time error that says that you can’t have two versions of your component loaded at the same time.
For this reasons, you MUST do this in the web.config/app.config, to give the developer that flexibility to use multiple versions of your component on the same machine.
Using / Reading the Config Values:
As a checkpoint, we’ve:
- Created a skeleton of the config that we want to use, imagining how the developer might use it.
- We created data structures to support those configuration elements.
- We’ve defined the <configSections> in the consuming configuration file, so that the run-time knows how to deserialize the section.
Now, we are ready to read the actual values. The technique is slightly different if you have a web app versus non-web, so I’ll cover both below. I’ve found this easiest to just write all this logic in one place. Since you are already working in the Configuration namespace in your component, I typically create a ConfigurationHelper class that does this hard work for me. That looks like this:
As you can see, most of the code is in GetIsWeb(). First, many times people will ask “Shouldn’t that be a property?” – the answer is no. There is a published convention in Framework Design Guidelines that says a property getter should never throw an exception. In this case, the business of finding out if we are in a web context has PLENTY of room of error. So above, I have a basic implementation . In real life, this would have a bunch of error handling. In the end though, since something might slip through the cracks, I am leaving it as a Get type of method.
Next, if case you didn’t read this in the comments above – when you host a WCF service in IIS, the “session” object is not available (unless you run in compatibility mode, which most don’t). So – that is a case where Session will be null, but we DO have a web.config, so I have some additional logic to check the process name. This is u-g-l-y, in my opinion, if you have a better way to detect this – please let me know!
OK, so that class gives us access to the current config (whether it’s an app.config or web.config) – and it even gives us a handle to the top-level of our configuration! I have a sample app, which has sample config – in that console app, I can now do something like this:
As you can see, I can very easily get a handle on the configuration and inspect the values.
In your case, if you writing a framework, you may not want to expose this publicly – but I just wanted to show that you can easily expose your config settings, even to your consumers, if you want.
Intellisense: Create and Apply a Schema for Your Component:
If you stopped right now, this could be considered acceptable. You have your own custom configuration, you have data structures behind it, and you have a way to read those values.
What about the poor developer using this component though? When they are typing in the configuration file, how are they supposed to know the structure? Sure, you can provide templates or you could publish documentation, but there is a better way. There is Intellisense for code and there is Intellisense for config, so why not support that?
There is a decent amount written up on messing with DotNetConfig.xsd and catalog.xml already. But the good news is, there is a far easier way to support version-specific Intellisense in your configuration files and it just requires 2 XML attributes. You make the top of your config file look something like this:
Note that this is not a valid location, it’s just a sample URL. You can host your config schemas on your website though. If everyone just points to the website for the Intellisense schema, then you can easily host new versions or fix bugs in old versions.
So, by simply pointing to this schema, that gives actual Intellisense in the configuration file!
This is great, but what does that schema look like? It’s too long to paste here, so look inside the attached sample project. In short, open up the DotNetConfig.xsd file, and start by getting the format from there – well, at least the first few lines:
But where do you start? Well, there is this project on CodeProject that used to work fine for .NET 2.0, but it was a train-wreck when I tried to get it to work with 4.0.
Instead, I’d recommend doing this: create a fully-fleshed out configuration with all of the options filled out. While that config file is open, open the XML menu and choose “Create Schema…”
This will create a good chunk of the schema for you. You will have to do a fair-amount of customizing, but it’s not unreasonable. The good part is, if you want to reproduce functionality that you see with other Intellisense, you have access to the schemas (again, check out DotNetConfig.xsd or the xsd that is included in the source project below).
A couple of things to note. To add Intellisense to any element (including any element, attribute, or enum value) – use this sort of syntax within that defining element in the schema:
When you generate your schema, it will be very simple. You would typically need to take something like this:
and embellish it to be something like this:
One thing I like to do is have better control over boolean values. So, you can do something like this:
I prefer to be explicit by using a schema “restriction” like this:
One last note on schemas – this issue took me some time to find. I am not create with XML schema, so I didn’t know this off the top of my head. The minOccurs and maxOccurs has some limitations, which aren’t obvious.
If you have a scenario where you want to allow exactly 0 or 1 elements, within an existing element, don’t use an <xs:choice> or <xs:sequence>, use <xs:all>. By itself, it will FORCE you to have at least 1 of everything defined within. However, you can override specific elements and set minOccurs=”0″.
Again, there is a full example in the source provided – or you can look at DotNetSchema.xsd for ideas too.
That is about it. If your component or application needs custom configuration, this article attempts to explain everything that is needed to provide effective configuration to your developers, including Intellisense. I hope that helps – and for Future Robert who is reading this, your welcome! 🙂