ASP.NET Core Fundamental – Configuration

I started my learning journey with this ASP.NET Core. From my point of view, there are 3 major concepts I have to delve into: Configuration, Service, and Middleware. For each concept, I must learn the fundamentals; learn the things that will stick around with me; allow me to work on advanced features if I want to (of course, I will soon).

Let’s start with Configuration. If you are an advanced developer, you might want to read the full document at Microsoft docs. For me, I want to take a different approach. I want to start with my needs. I, then, extract the information I need, build the fundamental knowledge that is mine.

Goals

I want to know

  1. What kind of configuration options I have
  2. How to build a configuration file
  3. How to consume a configuration file
  4. How to deal with environments such as development, staging, and production
  5. How about deploying to Azure, how I change my configuration values on the fly.

Along the way, I might want to explore the flexibilities that I might have with .NET Core.

Code and Magic

Basic

I want to say “hello world” in a configurable way. Open VS2017, I add a new json file, named it appSettings.json

{
  "Message" :  "Hello word. This is Mr. JSON speaking." 
}

And consume the setting file

    public class Startup
    {
        private readonly IConfigurationRoot _configuration;
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appSettings.json");

            _configuration = builder.Build();
        }
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            //app.UseMvc();
            app.Run(async (context) =>
            {
                await context.Response.WriteAsync($"{_configuration["Message"]}. And you are? {context.Request.Path}");
            });
        }
    }

And it just works.

A Bit Deeper

There is more cool stuff that you could do with the JSON configuration. Should you need them all, refer back to the full documentation from Microsoft. I am only interested in the things that make more sense for common scenarios.

In a real application, the settings are more complex than single key-value pairs. Take a look at this

{
  "Message": "Override the simple message",
  "ConnectionStrings": {
    "ef": "Connection string to SQL database for EF"
  },
  "Author": {
    "Name": "Thai Anh Duc",    
    "Job": "Software Developer" 
  } 
}

How could I get a connection string for EF? Super easy with the support of accessing the child element with “:” separator

   public class Startup
    {
        private readonly IConfigurationRoot _configuration;
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appSettings.json")
                .AddJsonFile("appComplexSettings.json");

            _configuration = builder.Build();
        }
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            //app.UseMvc();
            app.Run(async (context) =>
            {
                await context.Response.WriteAsync($"{_configuration["Message"]}; " +
                                                  $"EF Connection String: {_configuration["ConnectionStrings:ef"]}; " +
                                                  $"Author: {_configuration["Author:Name"]}");
            });
        }
    }

I have added AddJsonFile(“appComplexSettings.json”) after the call to the first appSettings.json file. Let’s take a look at the output before continue

Override the simple message; EF Connection String: Connection string to SQL database for EF; Author: Thai Anh Duc
  1. The “Message” key has been overridden by the latest config file (appComplexSettings.json). The order matters.
  2. ConnectionString:ef“: navigate to the “ConnectionString” node, then get value from its child node: ef.

It is flexible and easy to code. But, I am in VS2017 with C# code. I want OOP style. For many reasons, the settings should be modeled with OOP fashion. Let model it

    public class ConnectionStrings
    {
        public string Ef { get; set; }
    }

    public class Author
    {
        public string Name { get; set; }
        public string Job { get; set; }
    }
    public class AppSettings
    {
        public string Message { get; set; }
        public ConnectionStrings ConnectionStrings { get; set; }
        public Author Author { get; set; }
    }

With the magic of .NET Core, let’s try to create a connection between the configuration and the model. Turn out it is pretty easy.

    public class Startup
    {
        private readonly IConfigurationRoot _configuration;
        private readonly AppSettings _appSettings = new AppSettings();
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appSettings.json")
                .AddJsonFile("appComplexSettings.json");

            _configuration = builder.Build();
            _configuration.Bind(_appSettings);
        }
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            //app.UseMvc();
            app.Run(async (context) =>
            {
                await context.Response.WriteAsync($"{_appSettings.Message}; " +
                                                  $"EF Connection String: {_appSettings.ConnectionStrings.Ef}; " +
                                                  $"Author: {_appSettings.Author.Name}");
            });
        }
    }

All I need to do is instantiate a new AppSettings instance, and then Bind to Configuration.

However, the Bind approach is manual. And in the production code, we might not use it. .NET Core supports the dependency injection with IOption. You can register it at the ConfigureServices step, and the use it everywhere. Let’s give it a try.

    public class AppSettingsController
    {
        private readonly AppSettings _appSettings;
        public AppSettingsController(IOptions<AppSettings> appSettingsOptions)
        {
            _appSettings = appSettingsOptions.Value;
        }

        public Task GetAppSettings(HttpContext context)
        {
            return context.Response.WriteAsync($"Thie is AppSettingsController: {_appSettings.Message}; " +
                                               $"EF Connection String: {_appSettings.ConnectionStrings.Ef}; " +
                                               $"Author: {_appSettings.Author.Name}");
        }
    }
    public class Startup
    {
        private readonly IConfigurationRoot _configuration;
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appSettings.json")
                .AddJsonFile("appComplexSettings.json");

            _configuration = builder.Build();
        }
        public void ConfigureServices(IServiceCollection services)
        {
            // Register the ability to read options in configuration
            services.AddOptions();
            services.Configure<AppSettings>(_configuration);

            // Register a controller with a lifestyle of httprequest scoped
            services.AddScoped<AppSettingsController>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            //app.UseMvc();
            app.Run(HandleRequest);
        }

        public Task HandleRequest(HttpContext context)
        {
           var controller = context.RequestServices.GetRequiredService<AppSettingsController>();
            return controller.GetAppSettings(context);
        }
    }

There are a couple of things here

  1. Remove the appSettings variable, and instead use the services.AddOptions and services.Configure to register the AppSettings binding.
  2. Create AppSettingsController class. I just want to have a feeling of Controller here. And then register it with the services.
  3. In the HandleRequest method, resolve the Controller and delegate the work.

It works perfectly. Things are much easier than I did with Docker and deployment stuff 🙁

When we look at the HandleRequest again, it looks like the MVC framework started. Basically what it does is take a request (via HttpContext), process it, and prepare the Response object.

Deployment

Application settings are environment-dependent. Depending on either Development, Staging, or Production, keys have different values. Because .NET Core allows us to add multiple files. Order matters. Therefore, we can accomplish the goal with this code

        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appSettings.json")
                .AddJsonFile($"appSettings.{env.EnvironmentName}.json")
                .AddJsonFile("appComplexSettings.json");

            _configuration = builder.Build();
        }

Whatever keys defined in the second appSettings file will override the default one (the appSettings.json).

The last question remains. How do I change values from Azure Portal?

First, let build and deploy it to the Azure portal using VSTS.

Welcome back to VSTS. Click Build. Build succeeded. Good. Click Release. Oops! Failed with “ERROR_FILE_IN_USE”. There is a long story discussed here in github. The quickest solution is to stop the App Service. Release completed. The result is the same as running locally. Cool.

Open the Azure Portal, look at the configuration of my App Service, seems there is no place to change values in JSON configuration. Let’s ask Google.

I have to make some modification to meet the convention structure.

{
  "AppSettings": {
    "Message": "Setting from app settings"
  }
}
    public class AppSettingsController
    {
        private readonly AppSettings _appSettings;
        private readonly GlobalSettings _globalSettings;
        public AppSettingsController(IOptions<AppSettings> appSettingsOptions, IOptions<GlobalSettings> globalSettingsOptions)
        {
            _appSettings = appSettingsOptions.Value;
            _globalSettings = globalSettingsOptions.Value;
        }

        public Task GetAppSettings(HttpContext context)
        {
            return context.Response.WriteAsync($"Thie is AppSettingsController: {_appSettings.Message}; " +
                                               $"EF Connection String: {_globalSettings.ConnectionStrings.Ef}; " +
                                               $"Author: {_globalSettings.Author.Name}");
        }
    }
    public class Startup
    {
        private readonly IConfigurationRoot _configuration;
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json")
                .AddJsonFile("appComplexSettings.json");

            _configuration = builder.Build();
        }
        public void ConfigureServices(IServiceCollection services)
        {
            // Register the ability to read options in configuration
            services.AddOptions();
            services.Configure<AppSettings>(_configuration.GetSection("AppSettings"));
            services.Configure<GlobalSettings>(_configuration);

            // Register a controller with a lifestyle of httprequest scoped
            services.AddScoped<AppSettingsController>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            //app.UseMvc();
            app.Run(HandleRequest);
        }

        public Task HandleRequest(HttpContext context)
        {
           var controller = context.RequestServices.GetRequiredService<AppSettingsController>();
            return controller.GetAppSettings(context);
        }
    }

The AppSettings section will match the way web.config structured. Deploy to Azure portal again and see what happens.

Still cannot update the AppSettings directly in Azure Portal. This is a long post already. And I should call it a day (it is Friday night).

Recap

Even just a small step in the whole application development, when I get my hands on it, there are so many things I can learn from the design and implementation. The new configuration design gives us a lot of flexibilities. The system can access the configuration by using index access (by key with “:” separator), or build a configuration object (call as Option) and inject into many places. Whenever we want to use it, we can inject it. We get the dependency injection for free.

Because I have setup the deployment, I have a chance to use it. In short, I have a full lifecycle of development ready.

Happy Weekend!

 

Write a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.