← Back to overview
August 31, 2012 · ASP.NET Universal Providers Microsoft Azure

ASP.NET Universal Providers and the Windows Azure Service Configuration

Since Scott Hanselman introduced the ASP.NET Universal Providers last year welearned that theseproviders should be used when working with SQL Azure. Version 1.2 of the providers were released just a few days ago, but this version still assumes you store your settings in the web.config.

When you're building Cloud Services in Windows Azure you probably want to store this information in the ServiceConfiguration.cscfg. Since Visual Studio allows you to manage multiple configurations, you could store a connection string for local development, for a ‘beta' environment, production, … But the ASP.NET Universal Providers don't support this out of the box.

Getting around this limitation is pretty simply. We can intercept the initialization of these providers and to the necessary work so that they get the connection string configured in the ServiceConfiguration.cscfg.

Default configuration

Let's take a look at how you would configure the ASP.NET Universal Providers with the web.config. First you need to add the package to your project:

After adding the package you can simply add the providers to the web.config:

  <connectionStrings>
     <add name="DefaultConnection" providerName="System.Data.SqlClient" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=aspnet-MvcApplication3-20120901000818;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\aspnet-MvcApplication3-20120901000818.mdf" />
  </connectionStrings>
  ...
  <system.web>
    ...
    <profile defaultProvider="DefaultProfileProvider">
      <providers>
        <add name="DefaultProfileProvider" type="System.Web.Providers.DefaultProfileProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
             connectionStringName="DefaultConnection" applicationName="/" />
      </providers>
    </profile>
    <membership defaultProvider="DefaultMembershipProvider">
      <providers>
        <add name="DefaultMembershipProvider" type="System.Web.Providers.DefaultMembershipProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
             connectionStringName="DefaultConnection" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" />
      </providers>
    </membership>
    <roleManager defaultProvider="DefaultRoleProvider">
      <providers>
        <add name="DefaultRoleProvider" type="System.Web.Providers.DefaultRoleProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
             connectionStringName="DefaultConnection" applicationName="/" />
      </providers>
    </roleManager>
    <sessionState mode="InProc" customProvider="DefaultSessionProvider">
      <providers>
        <add name="DefaultSessionProvider" type="System.Web.Providers.DefaultSessionStateProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
             connectionStringName="DefaultConnection" />
      </providers>
    </sessionState>
  </system.web>

Each provider has an attribute connectionStringName which specifies the name of the connection string that should be used. And that's it. You providers are now working with a connection string specified in the web.config.

Using the ServiceConfiguration.cscfg

Instead of reading the connection string from the web.config we want to be able to use a connection string stored in the ServiceConfiguration.cscfg. After double clicking the Web Role in the Cloud project, theSettings tab makes it possible to add theconnection string (and the ProviderName)to the Service Configuration:

I also added the Provider Name which we'll need later (this is required by Entity Framework). In order to use this setting instead of the connection string in the web.config we simply override the Initialize method of the ProviderBase base class. Take the DefaultMembershipProvider for example, this is how we would override its Initialization method:

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

        // Do something here...

        base.Initialize(name, config);
    }
}

The config collection allows us to read the attributes which were specified in the web.config. The following example configures the AzureMembershipProvider as default membership provider with the connectionStringName set to UserDbConnectionString:

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

During the initialization of our custom membership provider we'll have access to the connectionStringName configured for that provider (in this case UserDbConnectionString). The interception point is ready and the only thing left to do is read the connection string from the Service Configuration and make sure this connection string is used by the AzureMembershipProvider. I created a small helper class to make things easier:

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)
    {
        typeof(ConfigurationElementCollection).GetField("bReadOnly", BindingFlags.Instance | BindingFlags.NonPublic).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 first method, GetRoleEnvironmentSetting, makes it easy to read the ServiceConfiguration and will throw a clear exception if the setting is not found. This will be used to read the connection string and the provider name.

The ASP.NET Universal Providers are tightly coupled to the ConfigurationManager.ConnectionStrings property, so there's no way around this. The only solution would be to add the connection string at runtime whenever we initialize the provider. But if you ever tried to update the connection strings in a running application you'll know that this collection is read-only. The SetConnectionStringsReadOnly method allows us to change the ConnectionStrings property from read-only to writable, and this is exactly what we need to inject our connection string coming from the Service Configuration.

Finally the UpdateConnectionString method makes sure theConnectionStrings property is writeable, adds or updates the connection string and changes the ConnectionStringsproperty back to read-only. This helper class is used when overriding the Initialize method of the provider, which makes that method very light:

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);
    }
}

public class AzureProfileProvider : System.Web.Providers.DefaultProfileProvider  
{
    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);
    }
}

public class AzureRoleProvider : System.Web.Providers.DefaultRoleProvider  
{
    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);
    }
}

In order to use these providers you can simply delete the connection string from the web.config, add it to the Service Configuration and update the provider types in the web.config. Note that, when adding the connection string in the Service Configuration, you'll need to add a setting with the same name and a ProviderName suffix to define the provider name (like System.Data.SqlClient).

<profile defaultProvider="AzureProfileProvider">  
  <providers>
    <add name="AzureProfileProvider" type="UniversalProvidersWebRole.Security.AzureProfileProvider" 
          connectionStringName="UserDbConnectionString" applicationName="/" />
  </providers>
</profile>  
<membership defaultProvider="AzureMembershipProvider">  
  <providers>
    <add name="AzureMembershipProvider" type="UniversalProvidersWebRole.Security.AzureMembershipProvider"
          connectionStringName="UserDbConnectionString" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" />
  </providers>
</membership>  
<roleManager enabled="true" defaultProvider="AzureRoleProvider">  
  <providers>
    <add name="AzureRoleProvider" type="UniversalProvidersWebRole.Security.AzureRoleProvider" 
          connectionStringName="UserDbConnectionString" applicationName="/" />
  </providers>
</roleManager>  

The source for this sample application is available on GitHub.

Enjoy!

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket
Comments powered by Disqus