In a Windows Azure web role, your web application’s configuration is subtly different from a standard ASP.NET web application. Since the platform is designed for scalability, the web.config file is no longer the primary way of getting and setting configuration. Since an update to web.config intrinsically requires an appPool restart, and there would be a challenge to ensure that each service was responsive before moving onto the next web.config to update in a multi-instance solution. Instead, you should consider using RoleEnvironment.GetConfigurationSettingValue(“key”) rather than ConfigurationManager.AppSettings[“key”].

But what if you are stuck with an ASP.NET website that uses web.config that you cannot or may not modify, and you need some Azure specific runtime variables stored in web.config that aren’t available before deployment, such as InternalEndpoint Port number or DeploymentId? This blog shows you a solution.

It should be noted that I consider this approach potentially dangerous and open to misuse. To be clear, I only consider this robust if used during WebRole Startup (OnStart in RoleEntryPoint or in a Startup Task) which is a point in the Web Role execution lifecycle where it is not capable of servicing requests anyway.

When faced with a challenge like this, it is important to realise why it’s a little difficult to achieve, and why there’s no simple interface for achieving this provided by Microsoft Windows Azure SDK. Every modification and save of a web.config causes the ASP.NET application pool that it is the root of to restart. This is a built in mechanism for ensuring consistency of an application’s configuration settings. If you programmatically modify a web.config every 10 seconds, then the application pool restarts every 10 seconds. Every restart causes the termination of executing processes and threads, meaning your users will get dropped connections and Service Unavailable messages. In a multi-instance environment doing so causes a strange experience where only the instances terminating at that given point in time is servicing their request. Their friend on the computer next to them doing the same thing may be absolutely fine for any given page request, when they may get a service unavailable message. This sort of problem arises when the load balancer on top of the Web Role is bypassed in this way. It is not a robust solution during execution.

That said (if I haven’t put you off enough yet!) I do think there is a safe point in time where the modifications can be made. This is during application startup, where you have the opportunity to “prime” your appSettings, connectionStrings etc and make other changes to your role that were not possible before you deployed. This will still cause an application pool restart, but since no users are yet able to consume the web application, it is of little consequence.

Methodology

In our example we seek to add two settings to AppSettings – the port of an internal endpoint and the deploymentId of the running instance. These are both accessible using the SDK and appsettings isn’t a very useful place for them, but in our example we must assume a static codebase that already somehow requires these settings.

Firstly we need to create a new Cloud Project in Visual Studio.

Blank Cloud Project

Blank Cloud Project

This project then contains the place where we will be doing most of our work, WebRole.cs:

This is where we will do most of our work

This is where we will do most of our work

Firstly we need to add a reference to Microsoft.Web.Administration.dll. This can be found in C:WindowsSysWOW64inetsrvMicrosoft.Web.Administration.dll. Make sure the assembly is set to “CopyLocal” = True in its properties.

Add this reference

Add this reference

Next we should prepare our demonstration by adding an Internal Endpoint to our WebRole by modifying the ServiceDefinition.csdef as such:

<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="ProgrammaticWebConfig" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition">
<WebRole name="WebRole1">
<Sites>
<Site name="Web">
<Bindings>
<Binding name="Endpoint1" endpointName="Endpoint1" />
<Binding name="InternalEndpoint1" endpointName="InternalEndpoint1" />
</Bindings>
</Site>
</Sites>
<Endpoints>
<InputEndpoint name="Endpoint1" protocol="http" port="80" />
<InternalEndpoint name="InternalEndpoint1" protocol="http" />
</Endpoints>
<Imports>
<Import moduleName="Diagnostics" />
</Imports>
</WebRole>
</ServiceDefinition>

Note that we haven’t specified a port number for our InternalEndpoint – this is up to the Azure Fabric to calculate, and so we can’t know this before deployment. This is a good use case for this approach.

Next we modify Default.aspx to output our ApplicationSettings, which to start with have no value:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
CodeBehind="Default.aspx.cs" Inherits="WebRole1._Default" %>
<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
<h2>
Welcome to Web.Config manipulation!
</h2>
<p><%: ConfigurationManager.AppSettings["deploymentId"] %></p>
<p><%: ConfigurationManager.AppSettings["internalEndpointPort"] %></p>
</asp:Content>

Once we have this we need to use the ServiceManager class from Microsoft.Web.Administration in our WebRole.cs OnStart method to add in the new configuration values. Here is the code:

First add the using namespace statement:

using Microsoft.Web.Administration;

Then add the logic to your OnStart method:

public override bool OnStart()
{
    using (var server = new ServerManager())
    {
        // get the site's web configuration
        var siteNameFromServiceModel = "Web"; // TODO: update this site name for your site. 
        var siteName =
            string.Format("{0}_{1}", RoleEnvironment.CurrentRoleInstance.Id, siteNameFromServiceModel);
        var siteConfig = server.Sites[siteName].GetWebConfiguration();

        // get the appSettings section
        var appSettings = siteConfig.GetSection("appSettings").GetCollection();

        AddElement(appSettings, "deploymentId", RoleEnvironment.DeploymentId);
        AddElement(appSettings, "internalEndpointPort", RoleEnvironment.CurrentRoleInstance.InstanceEndpoints
            .First(t=>t.Key=="InternalEndpoint1").Value
            .IPEndpoint.Port.ToString());
                
        server.CommitChanges();
    }
    return base.OnStart();
}

For clarity I provided an AddElement method that made the adding of settings safer, in case they happen to exist. In dev fabric, this is often the case but in Azure the web.config shouldn’t be touched more than one, so the settings are quite unlikely to exist.

private void AddElement(ConfigurationElementCollection appSettings, string key, string value)
{
    if (appSettings.Any(t => t.GetAttributeValue("key").ToString() == key))
    {
        appSettings.Remove(appSettings.First(t => t.GetAttributeValue("key").ToString() == key));
    }

    ConfigurationElement addElement = appSettings.CreateElement("add");
    addElement["key"] = key;
    addElement["value"] = value;
    appSettings.Add(addElement);
}

As you can see, the implementation is quite concise, getting the AppSettings Element from Web.Config and then adding elements to it, before committing changes. When you commit this, you may notice Visual Studio prompting you to reload web.config (if you have it open), this is a good sign!

As the application loads, you can see the result as:

Loading the values from config!

Loading the values from config!

Source code is at the end of this post.

Caution

If you use this solution and don’t use one time deployment provisioning, instead scaling up and down at will, you may find inconsistent application settings if you choose to load from RoleEnvironment.GetConfigurationSetting(“key”). Imagine the scenario with a RoleEnvironment “version” setting at version 1, and a startup task that puts this into appsettings:

  1. Deploy 50 instances, in each ConfigurationManager.AppSettings[“version”] always = 1
  2. Change RoleEnvironment “version” to 2
  3. Increase instance count by an extra 10 instances, in 1/6 of the roles ConfigurationManager.AppSettings[“version”] is 2, in 5/6 ths it is 1.

Source

Source code is here: ProgrammaticWebConfig

Postscript

A special thanks to Neil Mackenzie who pointed me in the right direction. This is derived from the recommended (outdated now) approach to machine key synchronisation http://bit.ly/gLKRCs

About these ads