Sharing Configuration in ASP.NET Core SPA Scenarios
This is a guest post from Mike Rousos
To get started, you’ll want to create a new ASP.NET Core Angular project – either by creating a new ASP.NET Core project in Visual Studio and selecting the Angular template, or using the .NET CLI command
dotnet new angular.
At this point, you should be able to restore client packages (
npm install) and launch the application.
In this project template, the ASP.NET Core app’s configuration is loaded from default sources thanks to the
WebHost.CreateDefaultBuilder call in Program.cs. The default configuration providers include:
- User secrets (if in a development environment)
- Environment variables
- Command line arguments
You can see that appsettings.json already has some initial config values related to logging.
For the client-side application, there aren’t any configuration values setup initially. If we were using the Angular CLI to create and manage this application, it would provide environment-specific TypeScript files (environment.ts, environment.prod.ts, etc.) to provide settings specific to different environments. The Angular CLI would pick the right config file to use when building or serving the application, based on the environment specified. In our case, though, we’re not using the Angular CLI to build the client (we’re just using WebPack directly).
Instead of using client-side TypeScript files for configuration, it would be convenient to share portions of our server app’s configuration with the client app. That would enable us to use ASP.NET Core’s rich configuration system which can load from environment-specific config files, as well as from many other sources (environment variables, Azure Key Vault, etc.). We just need to make those config settings available to our client app.
Embedding Client Configuration
Since our goal is to store client and server settings together in the ASP.NET Core app, it’s helpful to define the shape of the client config settings by creating a class modeling the configuration data. This isn’t required (you could just send settings as raw json), but if the structure of your configuration isn’t frequently changing, it’s a little nicer to work with strongly typed objects in C# and TypeScript.
Here’s a simple class for storing sample client configuration data:
Next, we can use configuration Options to easily extract a ClientConfiguration object from the server application’s larger configuration.
Here are the calls to add to Startup.ConfigureServices to make a ClientConfiguration options object available in the web app’s dependency injection container:
Notice that we’ve specified that the ClientConfiguration object comes from the “ClientConfiguration” section of the app’s configuration, so that’s where we need to add config values in appsettings.json (or via environment variables, etc.):
If you want to set these sorts of hierarchical settings using environment variables, the variable name should include all levels of the setting’s hierarchy delimited by colons or double underscores. So, for example, the ClientConfiguration section’s UserMessage setting could be set from an environment variable by setting ClientConfiguration__UserMessage (or ClientConfiguration:UserMessage) equal to some value.
Creating a Client Configuration Endpoint
There are a number of ways that we can make configuration settings from our server application available to the client. One easy option is to create a web API that returns configuration settings.
To do that, let’s create a ClientConfiguration controller (which receives the ClientConfiguration options object as a constructor parameter):
Next, give the controller a single index action which, as you may have guessed, just returns the client configuration object:
At this point, you can launch the application and confirm that navigating to /ClientConfiguration returns configuration settings extracted from those configured for the web app. Now we just have to setup the client app to use those settings.
Creating a Client-Side Model and Configuration Service
Since our client configuration is strongly typed, we can start implementing our client-side config retrieval by making a configuration model that matches the one we made on the server. Create a configuration.ts file like this:
Next, we’ll want to handle app config settings in a service. The service will use Angular’s built-in Http service to request the configuration object from our web API. Both the Http service and our application’s ‘BASE_URL’ (the web app’s root address which we’ll call back to to reach the web API) can be injected into the configuration service’s constructor.
Then, we just create a loadConfiguration function to make the necessary GET request, deserialize into a Configuration object, and store the object in a local field. We convert the http request into a Promise (instead of leaving it as an Observable) so that it works with Angular’s APP_INITIALIZER function (more on this later!).
The finished configuration service should look something like this:
Now that we have a configuration service, we need to register it in app.module.shared.ts to make it available to other components. The ASP.NET Core Angular template puts most module setup for our client app in app.module.shared.ts (instead of app.module.ts) since there are separate modules for server-side rendering and client-side rendering.App.module.shared.ts contains the module pieces common to both scenarios.
To register our service, we need to import it and then add it to a providers array passed to the @NgModule decorator:
There’s one other important change to make before we leave app.module.shared.ts. We need to make sure that config values are loaded from the server before any components are rendered. To do that, we add ConfigurationService.loadConfiguration to our app’s APP_INITIALIZER function (which is called at app-initiazliation time and waits for returned promises to finish prior to any components being rendered).
Import APP_INITIALIZER from @angular/core and then update your providers argument to include a registration for APP_INITIALIZER:
Note that useFactory is a function that must return a function (which, in turn, returns a promise), so we have the double fat-arrow syntax seen above. Also, don’t forget to specify
multi: true since there may be multiple APP_INITIALIZER functions registered.
Now the configuration service is registered with DI and will automatically load configuration from the server when the app starts up.
To make use of it, let’s update the app’s home component. Import
ConfigurationService into the home component and update the component’s constructor to take an instance of the service as a parameter. Make sure to make the parameter public so that it can be used from the home component’s HTML template. Since we will want to loop over the ‘messageCount’ config setting, it’s also useful to create a small helper function to return an array with a length of messageCount for use with *ngFor in the HTML template later.
Here’s my simple home component:
Finally, get rid of everything currently in home.component.html and replace it with an HTML template that takes advantage of the configuration values:
Trying it Out
You should now be able to run the web app and see the server-side configuration values reflected in the client application!
Here’s a screenshot of my sample app running with ASPNETCORE_ENVIRIONMENT set to Development (I set MessageCount to 2 in appsettings.Development.json):
And here’s one with ASPNETCORE_ENVIRONMENT set to Production (where MessageCount is three and the message is appropriately updated):
By exposing (portions of) app configuration from our ASP.NET Core app and making use of Angular’s APP_INITIALIZER function, we can share configuration values between server and client apps. This allows our client apps to take advantage of ASP.NET Core’s rich configuration system. In this sample, the client configuration settings were only used by our Angular app, but if your scenario includes some settings that are needed by both the client and server applications, this sort of solution allows you to set the config values in one place and have them be available to both applications.
Future improvements of this sample could include adding a time-to-live on the client’s cached configuration object to allow automatically reloaded config values to propagate to the client, or perhaps using different configuration providers to show Angular app configuration coming from Azure Key Vault or some other less common source.