I’ve been working on a series of posts and Webcasts in my “spare time” directed at some of the areas in which I am most frequently asked questions. I am currently working on developing topics dealing with IIS configuration. It seems logical that this would bring me a great deal of questions considering that IIS 7.0 is using a completely different configuration system than it did in previous versions. That said, I wanted to follow up to my Webcast (“Extending IIS Configuration“) with an article, and another webcast dealing with accessing custom configuration data at runtime.
In the webcast, I described how to extend an existing IIS configuration system object. I want to talk a little more about that. In this blog post I will take on four topics:
-
Give you some warnings that were not provided in the Webcast regarding extending native IIS configuration
-
Show you how to access your extended configuration data at runtime in a managed-code HTTP module
-
Describe how to improve the example used in the Webcast in a custom configuration section.
-
Show you how to access your custom configuration section at runtime in a managed-code HTTP module
Extending Native Configuration Elements
Introduction
I talked with Carlos today to discuss the validity of extending native configuration elements for our own purposes. While there are certainly valid reasons to do this, it doesn’t come without a risk to your application. Any time that you modify a piece of data that was created by another party — Microsoft or otherwise — you run the risk of having your data overwritten or corrupting the data that the other party needs to use. That said, lets revisit the sample that is frequently given in many articles and was reused in my webcast.
Warnings
In our Webcast, we discussed the idea that you could extend a native configuration element simply by creating a new configuration element schema and placing it in our %windir%sytem32inetsrvconfigschema directory. For our example, we created a file named SiteOwner_schema.xml and put the following code into it:
<sectionSchema name=”system.applicationHost/sites”>
<attribute name=”ownerName” type=”string”/>
<attribute name=”ownerEmail” type=”string”/>
<attribute name=”ownerPhone” type=”string”/>
</sectionSchema>
</configSchema>
All we have done is matched the same section schema path as the existing “system.applicationHost/sites” sectionSchema element in our IIS_schema.xml file. We then added schema information for our three new attributes.
This simple step allows us to immediately start adding attributes to our site configuration, and to access that data programmatically, through PowerShell, and through the AppCmd utility. However, you should be aware of a few dangers before you do something of this nature.
The first problem is that many of our native configuration elements have associated “default” configuration elements. For instance, the <site/> element has an associated <siteDefaults/> element. When modifying the configuration schema for <site/> , you should also modify the <siteDefaults/> configuration schema to match. That said, you might consider changing the above sample configuration schema as follows:
<sectionSchema name=”system.applicationHost/sites”>
<attribute name=”ownerName” type=”string”/>
<attribute name=”ownerEmail” type=”string”/>
<attribute name=”ownerPhone” type=”string”/>
</sectionSchema>
<element name=”siteDefaults”>
<attribute name=”ownerName” defaultValue=”undefined” type=”string”/>
<attribute name=”ownerEmail” defaultValue=”undefined” type=”string”/>
<attribute name=”ownerPhone” defaultValue=”undefined” type=”string”/>
</element>
</configSchema>
As you can see, in the bolded section above we have now modified the <siteDefaults/> configuration element to match our changes to the <site/> element. This isn’t the end of the story, however. While changing this might have seemed simple, modifying other elements become much more complex. Consider the fact that when you modify the schema for <application/> elements, there are several places where <applicationDefaults/> are kept. You will find <applicationDefaults/> under both the sites collection definition, as well as under the associated a site element definition. If you modify the application schema definition, you must be sure to edit the schema for both of the applicationDefaults definitions. This gets even more complex if you try to modify the <virtualDirectoryDefaults/> definition. You’ll find that defined three times in the native configuration: under the sites collection definition, under the application element definition, and under the site element definition itself.
You can see how you can quickly start adding complexity to your changes. Making sure that you make changes everywhere you need to becomes all the more risky.
The second problem you may have is that you may not be able to rely on your third-party to keep their schema definition in line with your own expectations. From one minor revision to the next, a configuration schema you may be depending on can change before your very eyes. This becomes a huge issue. Your future installation may need to make many dependency and version checks to decide what schema document to install, and your module may even need to make checks in case the user upgrades the third-party component without installing the upgraded schema. This can get very tricky, to say the least.
The third problem we can run into when we take a dependency on existing configuration is that external code may not behave the way we want it to. For instance, there is no guarantee that utilities and code written to handle those elements will know what to do if it encounters data that it doesn’t expect. Sure, we can cause IIS to validate our changes through schema, but there is no guarantee that the components which handle those elements can take on the challenge of handling the existing data. There is even a good chance that you could cause an Access Violation (read: crash) if the data is not handled properly. There is also a good chance that the components which normally handle those elements may overwrite your custom data.
So before we go on to show you how to access this extended configuration element data, please be warned that it may not be the best solution to your problem. Later in this article, I will describe how to modify our example to give us the same functionality without altering the schema definition of the existing elements.
Accessing Extended Native Configuration Elements at Runtime
Introduction
With the warnings having been fairly posted, I’ll show you quickly how to access your custom configuration data at runtime from an HTTP module. Once you have saved your schema file to the appropriate directory, its time to write the HTTP module. That module is going to be very simple. We need to create a module that uses the PreSendRequestHeaders event. In this event we will get our “system.applicationHost/sites” configuration section, loop through the configured sites looking for the site on which the current HTTP request is executing, and then write HTTP headers out to the client. Just to make sure that our sample is working, we’ll wrap it up by adding JavaScript to an HTML page that will request the headers from the Web site and display them in a message box.
Getting Started
For this sample, I used Visual Studio 2008, but you can use plain old notepad and command-line compilation if your are feeling particularly froggy.
Visual Studio 2008 Walk-through
The following walk-through will create an HTTP module that can display the site owner details of the configuration system
- Click on the File | New | Project … menu item.
- In the “Project types” tree on the left, drill down to the C# | Windows tree element
- In the “Templates” selector on the right, select the “Class Library” template
- Change the Name of the project to “SiteOwner“
- Change the Location to “c:samples”
- Verify that the “Create directory for solution” checkbox is checked
- Click OK
- Click on the Project | Add Reference … menu item.
- Under the .NET tab, select the component with the Component Name of “System.Web“
- Click OK
- Rename the “Class1.cs” file in the Solution Explorer to “SiteOwnerHeaderModule.cs“
- Paste the following code in “SiteOwnerHeaderModule.cs” file:
using System;
using System.Web;
using System.Web.Hosting;
using Microsoft.Web.Administration;
using System.Collections.Specialized; namespace SiteOwner
{
public class SiteOwnerHeaderModule : IHttpModule {
public void Dispose() {}
public void Init(HttpApplication context) {
context.PreSendRequestHeaders += new EventHandler(OnPreSendRequestHeaders);
}
void OnPreSendRequestHeaders(object sender, EventArgs e){
// Get the site collection
ConfigurationSection section =
WebConfigurationManager.GetSection(“system.applicationHost/sites”);
ConfigurationElementCollection cfgSites = section.GetCollection();
HttpApplication app = (HttpApplication)sender;
HttpContext context = app.Context;
// Loop through the sites to find the site of
// the currently executing request
foreach (ConfigurationElement cfgSite in cfgSites) {
if (HostingEnvironment.SiteName == (string)cfgSite.GetAttributeValue(“name”)) {
// This is the correct site, get the site owner attributes
SiteOwner siteOwner = new SiteOwner(cfgSite);
NameValueCollection headers = context.Response.Headers;
headers.Add(“Site-Owner-Name”, siteOwner.Name);
headers.Add(“Site-Owner-Email”, siteOwner.Email);
headers.Add(“Site-Owner-Phone”, siteOwner.Phone);
break;
}
}
}
}
class SiteOwner {
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public SiteOwner(ConfigurationElement site) {
this.Name = site.GetAttributeValue(“ownerName”) as string ;
this.Email = site.GetAttributeValue(“ownerEmail”) as string ;
this.Phone = site.GetAttributeValue(“ownerPhone”) as string;
}
}
} - Click the File | Save menu item.
- Right-click the SiteOwner project in the Solution Explorer window and select Properties from the menu
- Click the Signing tab
- Check the Sign the assembly checkbox
- Select “<new>” from the Choose a strong name key file dropdown box
- Type “KeyFile” in the Key file name box
- Uncheck the checkbox labeled Protect my key file with a password
- Click OK
- Click the File | Save SiteOwner menu item
- Click the Build | Build SiteOwner menu item.
Notepad.exe and Command-line Walk-through
The following walk-through will create an HTTP module that can display the site owner details of the configuration system. This walk-through assumes that you have the Microosft .NET Framework SDK or Windows SDK installed. These include the C# compilers needed to build. For more help on building with csc.exe, see “Command-line building With csc.exe”
- Paste the code from step 10 of the Visual Studio .NET 2008 walk-through into a new notepad instance
- Click File | Save As …
- Browse to the “C:samples” folder if it exists; create the folder if it does not
- Type “SiteOwnerHeaderModule.cs” in the File name box
- Click the Save button
- Open a new SDK Command Prompt (Windows SDK) or Visual Studio Command Prompt (Visual Studio 2008).
- At the command prompt type:
csc.exe /target:module /out:SiteOwner.netmodule /debug /r:System.Web.dll /r:%windir%system32inetsrvMicrosoft.Web.Administration.dll SiteOwnerHeaderModule.cs - Press ENTER then type:
sn.exe -k keyfile.snk - Press ENTER then type:
al.exe /out:SiteOwner.dll SiteOwner.netmodule /keyfile:keyfile.snk
Deploying Your HTTP Module
Once you have built your assembly using either Visual Studio 2008 or csc.exe, its time to deploy it to your system. This sample assumes that you don’t want all of your customers to have to install this same component to each Web site, so we’ll install it as a default module on the system.
Registering the Assembly in the GAC
To do this we first need to copy the assembly file(s) to the target system, and register them in the Global Assembly Cache. Using your preferred method to copy the files, copy the SiteOwner.dll (and SiteOwner.netmodule if you used command-line compilation) to the %windir%system32inetsrv directory on the target installation machine. Next open up a command prompt and type:
gacutil -i %windir%system32InetSrvSiteOwner.dll
Now you’ll need to get a copy of the “Public Key Token” for the assembly so that you can install this “strong-named assembly” module in IIS. To do this open up your %windir%assembly directory in Windows Explorer. Scroll down to the SiteOwner “file” in that window and copy the “Public Key Token”. You can do this easily by right-clicking the “file” and selecting Properties from the menu. Double click on the Public Key Token to select the entire contents, then right click the highlighted text and select Copy from the menu.
Installing the Module as a Default Module in IIS 7.0
Now that you have your assembly registered and your Public Key Token copied, its time to add this module to all the Web sites on your IIS 7.0 server. To accomplish this, open up your %windir%system32inetsrvconfigapplicationHost.config file in notepad. Scroll down near the bottom of the file to the default location path that looks something like this:
<location path=”” overrideMode=”Allow”>
Inside of that element scroll down to the <modules> element under the <system.webServer> section. You should find a number of modules already listed. To add yours, simply scroll to the bottom of the list and type the following:
<add name=”SiteOwner” type=”SiteOwner.SiteOwnerHeaderModule, SiteOwner, Version=1.0.0.0, Culture=neutral, PublicKeyToken=” />
Once you have typed this line in, you can paste your Public Key Token in right after “PublicKeyToken=” in the line you just typed. Save the applicationHost.config file.
Testing the Module
Choose a Web site on your server to test your application. If you want to run this module for files other than ASP.NET files, you’ll need to choose a Web site running in Integrated Pipeline. For our sample, we are going to make an HTML file to test our code. Our HTML file will contain some JavaScript that will get the headers from the server and display them in a message box. We could use a number of diagnostic tools to do this same work, but this will serve our purposes.
Open up Notepad and paste the following markup into it:
<body>
<script language=”JavaScript” type=”text/javascript”>
<!–
var req = new ActiveXObject(‘Microsoft.XMLHTTP’);
req.open(‘HEAD’, location.href, false);
req.send();
alert(req.getAllResponseHeaders())
//–>
</script>
</body>
</html>
Save the file to the physical root of the Web site you wish to test. Type in the corresponding URL to your test file in your web browser. Your result should be a JavaScript alert that looks something like the following:
You’ll notice in my results, I have already set the Site’s ownerName attribute. (see the Webcast titled “Extending IIS Configuration” to see how).
Improving our Sample
Introduction
As I stated earlier in this post, this demonstrates something that is definitely possible, but somewhat risky to do. We now want to improve our sample to limit our dependencies on native and/or third-party configuration schema definitions, and prevent us from encroaching on another module’s configuration territory. In concept, what we will do is move our configuration data to its own configuration section and use the site’s ID to allow us to map the data from the site collection to our configuration section.
Modifying the Schema
First, you’ll want to remove the custom site owner configuration data from the site nodes. Otherwise, once we modify the schema, the configuration will not validate. Once you’ve removed that data open the SiteOwner_schema.xml file in notepad and change it to the following:
<sectionSchema name=”system.applicationHost/siteOwner”>
<collection addElement=”site”>
<attribute name=”id” type=”uint” required=”true” isUniqueKey=”true” />
<attribute name=”ownerName” type=”string” />
<attribute name=”ownerEmail” type=”string” />
<attribute name=”ownerPhone” type=”string” />
</collection>
</sectionSchema>
</configSchema>
Save the file in the same location as you did previously. Now we need to tell the applicationHost.config file about our new section schema. To do this, we add a new section declaration underneath of the system.applicationHost sectionGroup in the applicationHost.config file.
…
<section name=”siteOwner” allowDefinition=”AppHostOnly” overrideModeDefault=”Deny” />
…
</sectionGroup>
Next we need to modify our HTTP module to read from the new configuration section:
using System.Web;
using System.Web.Hosting;
using Microsoft.Web.Administration;
using System.Collections.Specialized; namespace SiteOwner{
public class SiteOwnerHeaderModule : IHttpModule{
public void Dispose() { }
public void Init(HttpApplication context){
context.PreSendRequestHeaders
+= new EventHandler(OnPreSendRequestHeaders);
}
void OnPreSendRequestHeaders(object sender, EventArgs e){
// Get the site collection
ConfigurationSection section =
WebConfigurationManager.GetSection(“system.applicationHost/sites”);
ConfigurationElementCollection cfgSites = section.GetCollection();
HttpApplication app = (HttpApplication)sender;
HttpContext context = app.Context;
// Loop through the sites to find the site of
// the currently executing request
foreach (ConfigurationElement cfgSite in cfgSites){
if (HostingEnvironment.SiteName == (string)cfgSite.GetAttributeValue(“name”)){
// This is the correct site, find the associated
// Site Owner record in our custom config section
SiteOwner siteOwner = SiteOwner.Find((long)cfgSite.GetAttributeValue(“id”));
if (siteOwner != null) {
// Add the headers if we found a matching
// site owner record configured
NameValueCollection headers = context.Response.Headers;
headers.Add(“Site-Owner-Name”, siteOwner.Name);
headers.Add(“Site-Owner-Email”, siteOwner.Email);
headers.Add(“Site-Owner-Phone”, siteOwner.Phone);
}
break;
}
}
}
}
class SiteOwner{
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public SiteOwner(ConfigurationElement owner){
this.Name = owner.GetAttributeValue(“ownerName”) as string;
this.Email = owner.GetAttributeValue(“ownerEmail”) as string;
this.Phone = owner.GetAttributeValue(“ownerPhone”) as string;
}
public static SiteOwner Find(long siteId){
// Find the matching siteOwner record for this siteId
ConfigurationSection section =
WebConfigurationManager.GetSection(“system.applicationHost/siteOwner”);
ConfigurationElementCollection cfgSiteOwners = section.GetCollection();
foreach (ConfigurationElement cfgOwner in cfgSiteOwners) {
if (siteId == (long)cfgOwner.GetAttributeValue(“id”)){
return new SiteOwner(cfgOwner);
}
}
return null;
}
}
}
There isn’t much changed in this compared to the last. The difference, apart from a few code comments, is that I’ve modified the OnPreRequestHeaders function to get the current site ID, and pass that into the static SiteOwner.Find function. Then I modified the SiteOwner.Find function to read from our new configuration section. Please note that production code would likely set SiteOwner to extend from ConfigurationElement, and a special ConfigurationSection would be developed. Go ahead and compile and deploy this assembly the same way you did with the previous version. Make sure that you run gacutil on the new assembly once it is copied. Also, since we are running against a static HTML file, you might need to run iisreset.exe from a command-line in order to clear the cache from the server.
Once you have deployed your assembly and the schema file, its time to test the application. If we refresh our Web page, we will get a JavaScript alert without any site owner headers listed:
This is because we haven’t created any site owner configuration entries yet. So lets add a record directly to our config. I added the following configuration directly after the <sites> node in the applicationHost.config file:
<site id=”1″ ownerName=”Tobin Titus” ownerEmail=”tobint@microsoft.com” ownerPhone=”555-121-2121″ />
</siteOwner>
Save your file and refresh your Web browser. You should now see your site owner configuration data.
Conclusions
In this blog post, we’ve walked through extending existing IIS configuration objects, and accessing the custom data at runtime through an HTTP module. We’ve also talked about the risks of this approach and have demonstrated a better approach. This approach gives us the same ability to customize our IIS 7.0 configuration system, but gives us a much smaller dependency on the native configuration schema. We have little worry about schema changes, we don’t have to worry about other modules or utilities stomping on our custom data, and most importantly, we are far less likely to cause any Access Violations with the later approach.
Many thanks go to Carlos once again for his help in putting this post together as well as for his CodeColorizer plug-in for Live Writer. High-fives and cookies in his office!