February 7, 2014

Changing web.config file deployed on Windows Azure WebRole



Some times we might want to change settings in web.config file used by our web application after deploying it to Windows Azure Web Role. This post will take you through the required steps by using an ASP .NET MVC application with Entity Framework as an example.

A less interesting scenario will be changing settings in the 'App.config' file after deploying our application to Windows Azure Worker Role. Once we understand how to work with web.config in Web Role, its very easy to apply the same concept for App.config. At the end of this article I have explained working with App.config. Let us start with the main topic i.e. working with web.config in Web Role.

When we generate Entity Framework Code First model classes from an existing database, we have an option to save the data base connection string along with sensitive data (user name and password) in web.config file.

For example below diagram shows the final screen of  'Entity Model Wizard' asking developer's permission for storing connection string with user id and password as plain text in the web.config:







Once we are done with generating the model classes, we can see connection string get added in web.config - in my case with name 'NorthWindEntities' since I specified this name in the 'Entity Model Wizard' as the name for connection string.







If we open the generated context class, we can see the default constructor calls base class by passing name of the connection string i.e. NorthWindEntities. This means if application create context class using the default constructor, entity framework will use the above connection string.



Once we deploy this application to Windows Azure, it will use the database 'NorthWind-Test' hosted in my SQL Azure server (***.database.windows.net)

Suppose 'NorthWind-Test' is my test data base and once we tested and found application is working fine in Azure, we want to change the database to 'NorthWind-Production' (which is hosted in my production SQL Azure Server). This requires changing the connection string 'NorthWindEntities' in the web.config file deployed on Azure.

To update connection string in web.config programmatic-ally we need to find the location of web.config in Azure deployment. Following screenshot is taken from a web role after RDPing into it, which clearly shows where 'web.config' file is located.



Given below code to get path to web.config (You can map it to above picture):

        public static string RoleRootDir
        {
            get
            {
                return Environment.GetEnvironmentVariable("RdRoleRoot");
            }
        }

        public static string RoleModelFile
        {
            get
            {
                return Path.Combine(RoleRootDir, "RoleModel.xml");
            }
        }
        public static string WebsiteDir
        {
            get
            {
XNamespace _roleModelNs =
  "http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition";
                XDocument roleModelDoc = XDocument.Load(RoleModelFile);
                var siteElements = roleModelDoc.Root.Element(_roleModelNs + "Sites").Elements(_roleModelNs + "Site");

                var result =
                    from siteElement in siteElements
                    where siteElement.Attribute("name") != null
                            && siteElement.Attribute("name").Value == "Web"
                            && siteElement.Attribute("physicalDirectory") != null
                    select Path.Combine(RoleRootDir, siteElement.Attribute("physicalDirectory").Value);

                return result.First();
            }
        }
  

        public static string WebConfigFile
        {
            get
            {
                return Path.Combine(WebsiteDir, "web.config");
            }
        }

Now we know how to get path to web.config, we can easily load web.config to XmlDocument, look for the connection settings with name 'NorthWindEntities' and update it. Below function shows how to do this.

        public static void SetConnectionString(string connectionString)
        {
            var xDoc = XDocument.Load(WebConfigFile);
            var xElement = (from z in xDoc.Root.Element("connectionStrings").Elements()
                            where (z.Attribute("name").Value == "NorthWindEntities") select z).SingleOrDefault();
            xElement.Attribute("connectionString").Value = connectionString;
            xDoc.Save(WebConfigFile);
        }

We need to build the connection string and call the above function to set it in the web.config file. I have declared and defined settings to hold DB server name, DB name, user name and password in Azure service definition and configuration file associated with my cloud project.

ServiceDefinition.csdef

<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="NorthWindMVC"
                   xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition"
                   schemaVersion="2013-10.2.2">
  <WebRole name="MvcWebRoleNorthWind" vmsize="Small">
    <Sites>
      <Site name="Web">
        <Bindings>
          <Binding name="Endpoint1" endpointName="Endpoint1" />
        </Bindings>
      </Site>
    </Sites>
    <Endpoints>
      <InputEndpoint name="Endpoint1" protocol="http" port="80" />
    </Endpoints>
    <Imports>
      <Import moduleName="Diagnostics" />
    </Imports>
    <Runtime executionContext="elevated" />
    <ConfigurationSettings>
      <!-- Entity framework DB configuration -->
      <Setting name="NorthWind.DBConfiguration.Server" />
      <Setting name="NorthWind.DBConfiguration.Host" />
      <Setting name="NorthWind.DBConfiguration.Database" />
      <Setting name="NorthWind.DBConfiguration.User" />
      <Setting name="NorthWind.DBConfiguration.Password" />
    </ConfigurationSettings>
  </WebRole>
</ServiceDefinition>

ServiceConfiguration.[Local|Cloud].cscfg

<?xml version="1.0" encoding="utf-8"?>
<ServiceConfiguration serviceName="NorthWindMVC"
                      xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration"
                      osFamily="3"
                      osVersion="*"
                      schemaVersion="2013-10.2.2">
  <Role name="MvcWebRoleNorthWind">
    <Instances count="1" />
    <ConfigurationSettings>
      <Setting name="Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString" value="****" />
      <!-- Entity framework DB configuration -->
      <Setting name="NorthWind.DBConfiguration.Server" value="<server-name>" />
      <Setting name="NorthWind.DBConfiguration.Host" value="database.windows.net" />
      <Setting name="NorthWind.DBConfiguration.Database" value="<db-name>" />
      <Setting name="NorthWind.DBConfiguration.User" value="<user-name>" />
      <Setting name="NorthWind.DBConfiguration.Password" value="<password>" />
    </ConfigurationSettings>
  </Role>
</ServiceConfiguration>


Now we can read these configurations from RoleEnvironment, build the connection string and call 'SetConnectionString'

        public static void UpdateConnectionString()
        {

            string connectionFormat =    "metadata=res://*/NorthWindModel.csdl|res://*/NorthWindModel.ssdl|res://*/NorthWindModel.msl;provider=System.Data.SqlClient;provider connection string=\"data source={0}.{1};initial catalog={2};user id={3}@{0};password={4};multipleactiveresultsets=True;application name=EntityFramework\"";

            string connectionString = String.Format(
                connectionFormat,
RoleEnvironment.GetConfigurationSettingValue("NorthWind.DBConfiguration.Server"),            RoleEnvironment.GetConfigurationSettingValue("NorthWind.DBConfiguration.Host"),              RoleEnvironment.GetConfigurationSettingValue("NorthWind.DBConfiguration.Database"),          RoleEnvironment.GetConfigurationSettingValue("NorthWind.DBConfiguration.User"),              RoleEnvironment.GetConfigurationSettingValue("NorthWind.DBConfiguration.Password"));

         SetConnectionString(connectionString);

        }

Only remaining question is when to call above function? we need to call this function whenever we changes values for the settings 'NorthWind.DBConfiguration.*' from Windows Azure portal or any other tool after the deployment.



When user update any configuration (after changing settings and clicking 'Save' from portal configuration page), below events will be called by role run-time:

RoleEntryPoint::RoleEnvironment_Changing(object sender, RoleEnvironmentChangingEventArgs e)
RoleEntryPoint::RoleEnvironment_Changed(object sender, RoleEnvironmentChangedEventArgs e)


The RoleEnvironment_Changing event and the RoleEnvironment_Changed event are used together to identify and manage configuration changes to the service model.

We can call 'UpdateConnectionString' function from  'RoleEnvironment_Changed'.

        void RoleEnvironment_Changed(object sender, RoleEnvironmentChangedEventArgs e)
        {
            MyConfiguration.UpdateConnectionString();
        }


Using RoleEnvironment_Changing event, a role instance can respond to a configuration change in one of the following ways:

1. Accept the configuration change while it is running, without going offline.
2. Take the instance offline, apply the configuration change, and then bring the instance back online.

The connection string related changes in web.config file not requires instance to take offline. Role instance can inform role  run-time by setting Cancel property of RoleEnvironmentChangingEventArgs to false that it don't want to take the instance offline. 

        void RoleEnvironment_Changing(object sender, RoleEnvironmentChangingEventArgs e)
        {
            string[] exemptedCategories = {
                                              "NorthWind.DBConfiguration.Server",
                                              "NorthWind.DBConfiguration.Host",
                                              "NorthWind.DBConfiguration.Database",
                                              "NorthWind.DBConfiguration.User",
                                              "NorthWind.DBConfiguration.Password"
                                           };
            var changes = e.Changes.OfType<RoleEnvironmentConfigurationSettingChange>();
            e.Cancel =
              !changes.All(c => exemptedCategories.Contains(c.ConfigurationSettingName));

        }

Now hook the above two events in RoleEntryPoint::OnStart method

        public override bool  OnStart()
        {
            // Other app specific initialization code here..

            RoleEnvironment.Changing +
                new EventHandler<RoleEnvironmentChangingEventArgs>(RoleEnvironment_Changing);

            RoleEnvironment.Changed +
                new EventHandler<RoleEnvironmentChangedEventArgs>(RoleEnvironment_Changed);
        }


Note: Sometime you might want to call MyConfiguration.UpdateConnectionString() from OnStart() to set the DB connection string as a part of role initialization, in this case make sure you have following entry in the csdef file under WebRole node.

<Runtime executionContext="elevated" />

This will ensure OnStart() runs with admin privilege so that it can update web.config file. I have included this entry in the above sample csdef file.

That's all. :)

Working with App.config in Worker Role:

Now we know how to change web.config file in Web Role. If we want to configure App.config in similar line (when your application is in Worker Role), only thing we need to know is how to locate App.config file associated with your application.

One important point is once you build and host your application in Azure Worker Role the name of the app configuration file will not be App.config. Instead of App.config it will be '<assembly-name>.Dll.config. For example if name of your assembly is 'NorthWindWorkerRole' then configuration file name will be "NorthWindWorkerRole.Dll.config"

The application configuration file will be located under 'approot' (which is under role root)

        public static string AppRootDir
        {
            get
            {
                return Path.Combine(RoleRootDir, "approot");
            }

        }

We can use RoleModel.xml to find the name of the assembly, once we know assembly name then we just have to append '.config' to derive the application configuration file name. Now we can combine AppRootDir and configuration file name to get the full path.

        public static string AppConfigFile
        {
            get
            {
  XNamespace _roleModelNs =
      "http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition";
                XDocument roleModelDoc = XDocument.Load(RoleModelFile);
                var netFxEntryElement =
                roleModelDoc.Root.Element(_roleModelNs + "Runtime").Element(_roleModelNs +  
                  "EntryPoint").Element(_roleModelNs + "NetFxEntryPoint");
                return Path.Combine(AppRootDir, netFxEntryElement.Attribute("assemblyName").Value + ".config");
            }
        }

Note: If you are interested in understanding more about configuration files in Web role see this post.

8 comments:

  1. Are you sure that Entitty framework connect with new connection string? COnnection strings are already loaded on start.

    Thanks

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. Each ASP .NET MVC application is hosted inside dedicated container called AppDomain. If you have multiple ASP .NET Application in your IIS instance then each of them reside inside it's own AppDomain.

      IIS watch for each ASP .NET application's web.config file changes, if file gets changed then IIS will recycle the AppDomain in which the same ASP .NET application is hosted. Restarting an AppDomain means starting over the contained ASP .NET application with updated web.config file. So restarting the AppDomain will cause Entity Framework to use the updating connection string.

      Delete
  2. Just a small correction, should be "executionContext

    ReplyDelete
  3. xDoc.Save(configPath);
    this sentence should be xDoc.Save(WebConfigFile);

    ReplyDelete
  4. i was stucked 3 days and you solved my problem. thanks man

    ReplyDelete