Configuring ASP.NET Core Applications in Azure App Service

The .NET Core framework has a nice robust configuration framework. You will normally see the following (or something akin to it) in the constructor of the Startup.cs file:

        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

This version gets its settings from JSON files and then overrides these settings with things from environment variables. It seems ideal for Azure App Service because all the settings – from the app settings to the connection strings – are provided as environment variables. Unfortunately, they don’t necessarily get placed in the right place in the configuration. For example, Azure App Service has a Data Connections facility to define the various other resources that the app service uses. You can reach it from the menu of your App Service via the Data Connections menu. The common thing to do for Azure Mobile Apps is to define a connection to a SQL Azure instance called MS_TableConnectionString. However, when this is turned into an environment variable, the environment variable is called SQLAZURECONNSTR_MS_TableConnectionString, which is hardly obvious.

Fortunately, the ASP.NET Core configuration library is extensible. What I am going to do is develop an extension to the configuration library for handling these data connections. When developing locally, you will be able to add an appsettings.Development.json file with the following contents:

{
    "ConnectionStrings": {
        "MS_TableConnectionString": "my-connection-string"
    },
    "Data": {
        "MS_TableConnectionString": {
            "Type": "SQLAZURE",
            "ConnectionString": "my-connection-string"
        }
    }
}

When in production, this configuration is produced by the Azure App Service. When in development (and running locally), you will want to produce this JSON file yourself. Alternatively, you can adjust the launchsettings.json file to add the appropriate environment variable for local running.

To start, here is what our Startup.cs constructor will look like now:

        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddAzureAppServiceDataConnections()
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

Configuration Builder Extensions

In order to wire our configuration provider into the configuration framework, I need to provide an extension method to the configuration builder. This is located in Extensions\AzureAppServiceConfigurationBuilderExtensions.cs:

using Microsoft.Extensions.Configuration;

namespace ExampleServer.Extensions
{
    public static class AzureAppServiceConfigurationBuilderExtensions
    {
        public static IConfigurationBuilder AddAzureAppServiceDataConnections(this IConfigurationBuilder builder)
        {
            return builder.Add(new AzureAppServiceDataConnectionsSource());
        }
    }
}

This is a fairly standard method of hooking extensions into extensible builder-type objects.

The Configuration Source and Provider

To actually provide the configuration source, I need to write two classes – a source, which is relatively simple, and a provider, which does most of the actual work. Fortunately, I can extend other classes within the configuration framework to ease the code I have to write. Let’s start with the easy one – the source. This is the object that is added to the configuration builder (located in Extensions\AzureAppServiceDataConnectionsSource.cs:

using Microsoft.Extensions.Configuration;
using System;

namespace ExampleServer.Extensions
{
    public class AzureAppServiceDataConnectionsSource : IConfigurationSource
    {
        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return new AzureAppServiceDataConnectionsProvider(Environment.GetEnvironmentVariables());
        }
    }
}

I’m passing in the current environment into my provider because I want to be able to mock an environment later on for testing purposes. The configuration source (which is linked into the configuration builder) returns a configuration provider. The configuration provider is where the work is done (located in Extensions\AzureAppServiceDataConnectionsProvider.cs):

using Microsoft.Extensions.Configuration;
using System.Collections;
using System.Text.RegularExpressions;

namespace ExampleServer.Extensions
{
    internal class AzureAppServiceDataConnectionsProvider : ConfigurationProvider
    {
        /// <summary>
        /// The environment (key-value pairs of strings) that we are using to generate the configuration
        /// </summary>
        private IDictionary environment;

        /// <summary>
        /// The regular expression used to match the key in the environment for Data Connections.
        /// </summary>
        private Regex MagicRegex = new Regex(@"^([A-Z]+)CONNSTR_(.+)$");

        public AzureAppServiceDataConnectionsProvider(IDictionary environment)
        {
            this.environment = environment;
        }

        /// <summary>
        /// Loads the appropriate settings into the configuration.  The Data object is provided for us
        /// by the ConfigurationProvider
        /// </summary>
        /// <seealso cref="Microsoft.Extensions.Configuration.ConfigurationProvider"/>
        public override void Load()
        {
            foreach (string key in environment.Keys)
            {
                Match m = MagicRegex.Match(key);
                if (m.Success)
                {
                    var conntype = m.Groups[1].Value;
                    var connname = m.Groups[2].Value;
                    var connstr = environment[key] as string;

                    Data[$"Data:{connname}:Type"] = conntype;
                    Data[$"Data:{connname}:ConnectionString"] = connstr;
                    Data[$"ConnectionStrings:{connname}"] = connstr;
                }
            }
        }
    }
}

The bulk of the work is done within the Load() method. This cycles through each environment variable. If it matches the pattern
we are looking for, it extracts the pieces of data we need and puts them in the Data object. The Data object is provided by the
ConfigurationProvider and all the other lifecycle methods are handled for me by the ConfigurationProvider class.

Integrating with Dependency Injection

I want to provide a singleton service to my application that provides access to the data configuration. This service will be injected
into any code I want via dependency injection. To do this, I need an interface:

using System.Collections.Generic;

namespace ExampleServer.Services
{
    public interface IDataConfiguration
    {
        IEnumerable<DataConnectionSettings> GetDataConnections();
    }
}

This refers to a model (and I produce a list of these models later):

namespace ExampleServer.Services
{
    public class DataConnectionSettings
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public string ConnectionString { get; set; }
    }
}

I also need a concrete implementation of the IDataConfiguration interface to act as the service:

using Microsoft.Extensions.Configuration;
using System.Collections.Generic;

namespace ExampleServer.Services
{
    public class DataConfiguration : IDataConfiguration
    {
        List<DataConnectionSettings> providerSettings;

        public DataConfiguration(IConfiguration configuration)
        {
            providerSettings = new List<DataConnectionSettings>();
            foreach (var section in configuration.GetChildren())
            {
                providerSettings.Add(new DataConnectionSettings
                {
                    Name = section.Key,
                    Type = section["Type"],
                    ConnectionString = section["ConnectionString"]
                });
            }
        }

        public IEnumerable<DataConnectionSettings> GetDataConnections()
        {
            return providerSettings;
        }
    }
}

Now that I have defined the interface and concrete implementation of the service, I can wire it into the dependency injection system in the ConfigureServices() method within Startup.cs:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IDataConfiguration>(new DataConfiguration(Configuration.GetSection("Data")));

            // Add framework services.
            services.AddMvc();
        }

I can now write my test page. The main work is in the HomeController.cs:

        public IActionResult DataSettings([FromServices] IDataConfiguration dataConfiguration)
        {
            ViewBag.Data = dataConfiguration.GetDataConnections();
            return View();
        }

The parameter to the DataSettings method will be provided from my service via dependency injection. Now all I have to do is write the view for it:

<h1>Data Settings</h1>

<div class="row">
    <table class="table table-striped">
        <tr>
            <th>Provider Name</th>
            <th>Type</th>
            <th>Connection String</th>
        </tr>
        <tbody>
            @foreach (var item in ViewBag.Data)
            {
                <tr>
                    <td>@item.Name</td>
                    <td>@item.Type</td>
                    <td>@item.ConnectionString</td>
                </tr>
            }
        </tbody>
    </table>
</div>

Publish this to your Azure App Service. Don’t forget to link a SQL Azure instance via the Data Connections menu. Then browse to https://yoursite.azurewebsites.net/Home/DataSettings – you should see your SQL Azure instance listed on the page.

Now for the good part…

ASP.NET Core configuration already does this for you. I didn’t find this out until AFTER I had written all this code. You can just add the .UseEnvironmentVariables() to your configuration builder and the rename comes along for free. Specifically:

  • ConfigurationStrings:MS_TableConnectionString will point to your connection string
  • ConfigurationStrings:MS_TableConnectionString_Provider will point to the provider class name

So you don’t really need to do anything at all. However, this is a great reference guide for me for producing the next configuration provider as the sample code is really easy to follow.

The code for this blog post is in my GitHub Repository at tag p3. In the next post, I’ll tackle authentication by linking Azure App Service Authentication with ASP.NET Core Identity.

One thought

  1. Pingback: Azure Weekly: Feb 27, 2017 – Build Azure

Comments are closed.