Configuring A Blazor App

In Blazor, we don’t have anything that looks like an environment (we have limited access to the client computer) or an executable argument that would set the configuration. Let's see how to configure a Blazor app.

How to configure a Blazor application

Most of the applications we build need some kind of configuration - connection string, production or not, API key, etc.  Client-side apps are not different although what you’ll store in the configuration must be less sensitive, as the client will have access to it. In this post, I’ll explain how I set up my Blazor project for accessing different configuration needed in a different environment.

How to define the environment

Environment is a thing that decides which configuration to use - development, test, production, etc. We cannot use build configuration (debug, release) as those two things are different. You can decide to run your application built with debug, with the production configuration because you have a bug on production, or you can decide to deploy your release build on your test environment because you need to do one last check before releasing.

In Blazor, we don’t have anything that looks like an environment (we have limited access to the client computer) or an executable argument that would set the configuration. The only thing that looks like it is URI: a Blazor app is launched by a call to a URI just like a program is launched by a command line. A URI is made of multiple parts.

  • The protocol
  • The domain name
  • The path
  • The query string My solution will use the second and the last part :
  • An environment will be tied to a domain name (“myproject.com” will be the production and “localhost” will be the development environment)
  • This environment can be overridden by a query string parameter so we can switch environments without rebuilding our app.

The task here is to do 2 things,

  • Decide which environment we are working on
  • Load the configuration based on this environment and make it available for the rest of the app.

Find the environment on StartUp

The first step is to find the environment given a URI; here is the class I created for this:

  1. /// <summary>  
  2. /// This class is used for picking the environment given a Uri  
  3. /// </summary>  
  4. public class EnvironmentChooser  
  5. {  
  6.     private const string QueryStringKey = "Environment";  
  7.     private string defaultEnvironment;  
  8.     private Dictionary<string, Tuple<stringbool>> _hostMapping = new Dictionary<string, Tuple<string,bool>>();  
  9.   
  10.     /// <summary>  
  11.     /// Build a chooser  
  12.     /// </summary>  
  13.     /// <param name="defaultEnvironment">If no environment is found on the domain name or query then this will be returned</param>  
  14.     public EnvironmentChooser(string defaultEnvironment)  
  15.     {  
  16.         if (string.IsNullOrWhiteSpace(defaultEnvironment))  
  17.         {  
  18.             throw new ArgumentException("message", nameof(defaultEnvironment));  
  19.         }  
  20.   
  21.         this.defaultEnvironment = defaultEnvironment;  
  22.     }  
  23.     public string DefaultEnvironment => defaultEnvironment;  
  24.     /// <summary>  
  25.     /// Add a new binding between a hostname and an environment  
  26.     /// </summary>  
  27.     /// <param name="hostName">The hostname that must fully match the uri</param>  
  28.     /// <param name="env">The environement that'll be returned</param>  
  29.     /// <param name="queryCanOverride">If false, we can't override the environement with a "Environment" in the GET parameters</param>  
  30.     /// <returns></returns>  
  31.     public EnvironmentChooser Add(string hostName, string env, bool queryCanOverride = false)  
  32.     {  
  33.         this._hostMapping.Add(hostName, new Tuple<string,bool>(env, queryCanOverride));  
  34.           
  35.         return this;  
  36.     }  
  37.     /// <summary>  
  38.     /// Get the current environment givent the uri  
  39.     /// </summary>  
  40.     /// <param name="url"></param>  
  41.     /// <returns></returns>  
  42.     public string GetCurrent(Uri url)  
  43.     {  
  44.         var parsedQueryString = HttpUtility.ParseQueryString(url.Query);  
  45.         bool urlContainsEnvironment = parsedQueryString.AllKeys.Contains(QueryStringKey);  
  46.         if (_hostMapping.ContainsKey(url.Authority))  
  47.         {  
  48.             Tuple<stringbool> hostMapping = _hostMapping[url.Authority];  
  49.             if(hostMapping.Item2 && urlContainsEnvironment)  
  50.             {  
  51.                 return parsedQueryString.GetValues(QueryStringKey).First();  
  52.             }  
  53.             return hostMapping.Item1;  
  54.         }  
  55.         if (urlContainsEnvironment)  
  56.         {  
  57.             return parsedQueryString.GetValues(QueryStringKey).First();  
  58.         }  
  59.         return DefaultEnvironment;  
  60.     }  
  61. }  
  • Sorry for the long code sample
  • This class implements a simple algorithm and tries to find the environment given a configuration and a URI
  • I added the ability to ignore query parameters on the production environment

The configuration

Then, I need to use EnvironmentChooser and inject the IConfiguration into my service collection. I do it like this -

  1. public static void AddEnvironmentConfiguration<TResource>(  
  2.     this IServiceCollection serviceCollection,  
  3.     Func<EnvironmentChooser> environmentChooserFactory)  
  4. {  
  5.     serviceCollection.AddSingleton<IConfiguration>((s) =>  
  6.     {  
  7.         var environementChooser = environmentChooserFactory();  
  8.         var uri = new Uri(s.GetRequiredService<IUriHelper>().GetAbsoluteUri());  
  9.         System.Reflection.Assembly assembly = typeof(TResource).Assembly;  
  10.         string environment = environementChooser.GetCurrent(uri);  
  11.         var ressourceNames = new[]  
  12.         {  
  13.             assembly.GetName().Name + ".Configuration.appsettings.json",  
  14.             assembly.GetName().Name + ".Configuration.appsettings." + environment + ".json"  
  15.         };  
  16.         ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();  
  17.         configurationBuilder.AddInMemoryCollection(new Dictionary<stringstring>()  
  18.         {  
  19.             { "Environment", environment }  
  20.         });  
  21.         Console.WriteLine(string.Join(",", assembly.GetManifestResourceNames()));  
  22.         Console.WriteLine(string.Join(",", ressourceNames));  
  23.         foreach (var resource in ressourceNames)  
  24.         {  
  25.   
  26.             if (assembly.GetManifestResourceNames().Contains(resource))  
  27.             {  
  28.                 configurationBuilder.AddJsonFile(  
  29.                     new InMemoryFileProvider(assembly.GetManifestResourceStream(resource)), resource, falsefalse);  
  30.             }  
  31.         }  
  32.         return configurationBuilder.Build();  
  33.     });  
  34. }  
  • I use IUriHelper for getting the current Uri (it uses JSInterop)
  • The InMemoryFileProvider is mostly taken from here (https://stackoverflow.com/a/52405277/277067), I just changed the class for accepting a stream as input
  • I use the convention for reading the resources (folder Configuration then appsettings.json and appsettings.{Environment}.json)
  • I also add the Environment name, it can be used later for debugging purpose
  • For this to work, you need to add the packages Microsoft.Extensions.Configuration and Microsoft.Extensions.Configuration.Json to your project
  • I could call JSInterop with the configuration so it’ll be available to my js code as well.

Now, I call it like below in Startup.ConfigureServices.

  1. services.AddEnvironmentConfiguration<Startup>(() =>   
  2.             new EnvironmentChooser("Development")  
  3.                 .Add("localhost""Development")  
  4.                 .Add("tossproject.com""Production"false));  
The Startup generic parameter is used for selecting the assembly that embedded the configuration files.

One more thing to do: I need to force the initialization of my singleton at startup. If I don’t do that then the URI query parameter might not be here anymore if the user navigates and the IConfiguration is used for the first time. I chose to do this in Startup:

  1. //In Startup.Configure  
  2. public void Configure(IComponentsApplicationBuilder app)  
  3. {  
  4.     //force config initialization with current URI  
  5.     IConfiguration config = app.Services.GetService<IConfiguration>();  
  6. }  

Create the files

Now I need to create 3 files (none of them are mandatory as I check for their existence),

  • Configuration/appsettings.json
  • Configuration/appsettings.Development.json
  • Configuration/appsettings.Production.json

And set them as embedded resources in my project's csproj.

  1. <ItemGroup>  
  2.     <EmbeddedResource Include="Configuration\appsettings.json" />  
  3.     <EmbeddedResource Include="Configuration\appsettings.*.json" />  
  4. </ItemGroup>  

For instance, here is my appsettings.Development.json

  1. {  
  2.   "title""Welcome to Toss Development"  
  3. }  

Usage

For using it, you simply need to inject IConfiguration where you need it.

  1. @inject Microsoft.Extensions.Configuration.IConfiguration configuration  
  2. <h1>@configuration["title"]</h1>  

Conclusion

This project still shows one of the big advantages of Blazor; i.e., you can use most of the things done in .NET in the browser. When Blazor is shipped, we will not have to wait 1-2 years for all the needed libraries to come up (i18n, configuration, serialization …), they have been there for many years.

You can find most of the code for this blog post on my project (https://github.com/RemiBou/Toss.Blazor).

Reference
  • https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-2.2#file-configuration-provider
  • https://stackoverflow.com/a/52405277/277067