2
votes

I have a .NET Web solution with an Azure Cloud Service project and a single webrole. I deploy it to the East coast West coast data/compute centers for failover purposes and have been asked to automate the deployment using Powershell MSBuild and Jenkins.

The problem is i need to change the Sql Azure database connectionString in the Web.config prior to packaging and publishing to each deployment. Seems simple enough.

I understand that the webrole properties Settings tab allows you to add custom configuration properties to each deployment with a type of either "string" or "Connection String" but it looks like the "Connection String" option applies to only Blob, Table or Queue storage. If I use the "String" and give it an Sql Azure connection string type it writes it out as an key/value pair and Entity Framework and the Membership Provider do not find it.

Is there a way to add a per-deployment connection string setting that points to Sql Azure?

Thanks,

David

2

2 Answers

2
votes

Erick's solution is completely valid and I found an additional way to solve the problem so I thought I'd come back and put it up here since I had such trouble finding an answer.

The trick is getting the Entity Framework and any providers like asp.net Membership/profile/session etc... to read the connection string directly from the Azure service configuration rather than the sites web.config file.

For the providers I was able to create classes that inherit the System.Web.Providers.DefaultMembershipProvider class and override the Initialize() method where I then used a helper class I wrote to retrieve the connection string using the RoleEnvironment.GetConfigurationSettingValue(settingName); call, which reads from the Azure service config.

I then tell the Membership provider to use my class rather than the DefaultMembershipProvider. Here is the code:

Web.config:

<membership defaultProvider="AzureMembershipProvider">
  <providers>
    <add name="AzureMembershipProvider" type="clientspace.ServiceConfig.AzureMembershipProvider" connectionStringName="ClientspaceDbContext" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" />
  </providers>

Note the custom provider "AzuremembershipProvider"

AzuremembershipProvider class:

public class AzureMembershipProvider : System.Web.Providers.DefaultMembershipProvider
{
    public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
    {         
        string connectionStringName = config["connectionStringName"];

        AzureProvidersHelper.UpdateConnectionString(connectionStringName, AzureProvidersHelper.GetRoleEnvironmentSetting(connectionStringName), 
            AzureProvidersHelper.GetRoleEnvironmentSetting(connectionStringName + "ProviderName"));

        base.Initialize(name, config);
    }
}

And here's the helper class AzureProvidersHelper.cs:

public static class AzureProvidersHelper
{
    internal static string GetRoleEnvironmentSetting(string settingName)
    {
        try
        {
            return RoleEnvironment.GetConfigurationSettingValue(settingName);
        }
        catch
        {
            throw new ConfigurationErrorsException(String.Format("Unable to find setting in ServiceConfiguration.cscfg: {0}", settingName));
        }
    }

    private static void SetConnectionStringsReadOnly(bool isReadOnly)
    {
        var fieldInfo = typeof (ConfigurationElementCollection).GetField("bReadOnly", BindingFlags.Instance | BindingFlags.NonPublic);
        if (
            fieldInfo != null)
            fieldInfo.SetValue(ConfigurationManager.ConnectionStrings, isReadOnly);
    }

    private static readonly object ConnectionStringLock = new object();

    internal static void UpdateConnectionString(string name, string connectionString, string providerName)
    {
        SetConnectionStringsReadOnly(false);

        lock (ConnectionStringLock)
        {
            ConnectionStringSettings connectionStringSettings = ConfigurationManager.ConnectionStrings["name"];
            if (connectionStringSettings != null)
            {
                connectionStringSettings.ConnectionString = connectionString;
                connectionStringSettings.ProviderName = providerName;
            }
            else
            {
                ConfigurationManager.ConnectionStrings.Add(new ConnectionStringSettings(name, connectionString, providerName));
            }
        }

        SetConnectionStringsReadOnly(true);
    }
}

The key here is that the RoleEnvironment.GetConfigurationSettingValue reads from the Azure service configuration and not the web.config.

For the Entity Framework that does not specify a provider I had to add this call to the Global.asax once again using the GetRoleEnvironmentSetting() method from the helper class:

var connString = AzureProvidersHelper.GetRoleEnvironmentSetting("ClientspaceDbContext");

        Database.DefaultConnectionFactory = new SqlConnectionFactory(connString);

The nice thing about this solution is that you do not end up having to deal with the Azure role onstart event.

Enjoy

dnash

0
votes

David,

A good option is to use the Azure configurations. If you right click on the Azure project, you can add an additional configuration. Put your connection string(s) in the correct configuration (e.g., ServiceConfiguration.WestCoast.cscfg, ServiceConfiguration.EastCoast.cscfg, etc).

In your build script, pass the TargetProfile property to MSBuild with the name of the configuration, and those settings will be built into the final cscfg.

Let me know if you run into any problems. I did the approach, and it took a few tries to get it working right. Some details that might help.

Erick