I actually started writing this post back at the beginning of May, but I never finished it. In the time since I started working on this, I noticed that Eric Anderson tackled the very same problem on his blog. His solution is similar to mine, especially on the client-side of things (nice job, Eric!). But I tackled things a bit differently on the server-side, and I decided to finish out my post just to elaborate on a few of the concepts a bit further.
The Challenge
This post is partially in response to a question from Nigel over at my “Build Your Own App Framework” Pluralsight Course. One of the techniques I show in that course is how to pass data from ASP.NET MVC to Angular. I also show that same technique here on my blog, so feel free to check it out if you’re curious.
The pattern was more applicable in the AngularJS/ASP.NET MVC (non-Core) days, because AngularJS could be used to create “Silo SPAs” quite easily, and it had no built-in configuration system to speak of.
Neither of those things are true with ASP.NET Core and Angular 2+. Angular no longer lends itself to the “Silo SPA” approach, and it does have a configuration system built in.
But that doesn’t mean the idea of passing configuration between the two systems is irrelevant.
There’s still some configuration data that might make sense to have shared between both Angular and your ASP.NET Core application. Examples of things that might make sense to share both server and client-side are things like public keys, or non-sensitive tokens, or, in a multi-tenant application, information about the current tenant.
So how can we share config? How can we specify config settings exactly once, and leverage them in both places?
The Solution
The simplest solution I’ve come up with is this: expose configuration data from ASP.NET Core via an API endpoint, and have Angular make a request to this endpoint during application startup.
The endpoint could return whatever makes sense. A simple JSON object will probably suffice in many cases:
{
"setting1": "value1",
"setting2": "value2",
//And so on...
}
That sounds trivial, but there are a couple of gotchas, as we’ll soon see!
Defining the API Endpoint
Let’s go ahead and start by making an API endpoint to return config data:
[ApiController, Route("api/config")]
public class ConfigController : Controller
{
[HttpGet("")]
public IActionResult Get()
{
}
}
Easy enough so far. Now we want to grab specific config settings from our applications configuration data, and pass those down. I emphasize specific, because your configuration probably has lots of things you don’t want to expose like this. This is doubly true in ASP.NET Core, because by default, your config data will also contain all of your environment variables!
Remember that anything you return from your API is visible to the end user. It’s effectively in the hands of the enemy! If you return everything, you are more than likely passing sensitive information, including connection strings, and that would be a Bad ThingTM. Be selective, and only return the minimum!
We could manually extract and pass config data, which would look like this:
public class ConfigController : Controller
{
private readonly IConfiguration _config;
public ConfigController(IConfiguration config)
{
_config = config;
}
[HttpGet("")]
public IActionResult Get()
{
return Ok(new
{
//Grab whatever settings your client app needs, and pass them back!
clientName = _config.GetSection("CustomerSettings:ClientName").Value,
defaultPage = _config.GetSection("CustomerSettings:DefaultPage").Value
});
}
}
Or, if we choose to keep all of our shared config in a single section, and just pass the entire section:
public class ConfigController : Controller
{
private readonly IConfiguration _config;
public ConfigController(IConfiguration config)
{
_config = config;
}
[HttpGet("")]
public IActionResult Get()
{
var targetSection = "customerSettings";
//The magic happens here!
return Ok(_config.GetSection(targetSection)
.AsEnumerable()
.Where(x => x.Key != targetSection)
// Since we are enumerating the root,
// each key will be prefixed with our
// target section, so we need to strip
// that prefix off.
.ToDictionary(x => x.Key.RemoveStart($"{targetSection}:"), x => x.Value));
}
}
This approach has one nice benefit: if you add a new config setting, it will automatically be passed down. Of course, it’s also a bit dangerous, since if you accidentally add a sensitive setting in there, you might not realize it is being exposed to clients via the API.
There’s a bit more we could do here if we wanted. We could grab information about the current user, or in a multi-tenant application we could grab information about the current tenant. We could add caching, too, if we wanted.
Also note that while I’m using .NET Core’s
IConfiguration
provider, you could actually pull “config” settings from another source, too. If you store some config data in a database, you could retrieve that here, and pass it back as well.
That takes care of the ASP.NET Core side of things, so let’s take a look at the Angular-side.
Loading Configuration
So we have our config endpoint, we have our client app, it should be trivial to pull that data in… right??
Not so fast.
This is where things get a tad tricky.
Calling the endpoint is easy, but that’s an asynchronous call! Our app probably needs this data before it does anything else!
It turns out that Angular actually supports application initializers that are a perfect fit for what we’re doing here.
Let’s start by building a service that will allow us to retrieve configuration data from our API.
Creating the Configuration Provider
Let’s define a service to wrap our config data. I’m going to keep things loosely-typed, but you could define strongly-typed wrappers instead of exposing the configuration data directly:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'
// This represents our raw config from the server.
// We could define a strongly-typed config class
// that wraps the values from the server, but for
// this simple demo, we won't bother with that.
export declare type AppConfig = { [key: string]: string };
@Injectable()
export class AppConfigProvider {
private config: AppConfig;
constructor(
private httpClient: HttpClient
) { }
loadConfig() {
return this.httpClient.get<AppConfig>('api/config').toPromise().then(x => {
this.config = x;
});
}
getConfig() {
return this.config;
}
}
We also need to add our config provider to our application module’s providers:
// SNIP
import { AppConfigProvider } from './config/app-config-provider.service';
@NgModule({
// SNIP
providers: [
AppConfigProvider
],
// SNIP
})
export class AppModule { }
Now our components (and other services!) can inject
AppConfigProvider
in order to have access to our AppConfig
data.
Or they would be able to do that, if only someone was actually calling
loadConfig
first!Hooking in to Angular’s Application Initialization
The challenge now is to make sure our
loadConfig
function is called before anything else happens in our app. It turns out that Angular has built-in support for scenarios just like this in the form of application initializers.Note that app initializers are considered experimental still, so their exact implementation and behavior may change in future releases.
Over in our
app.module
, we can register a new app initializer like so:// SNIP
import { NgModule, APP_INITIALIZER } from '@angular/core';
// SNIP
@NgModule({
// SNIP
providers: [
AppConfigProvider,
{
provide: APP_INITIALIZER,
useFactory: (appConfigProvider: AppConfigProvider) => {
return () => appConfigProvider.loadConfig();
},
multi: true,
deps: [AppConfigProvider]
}
],
// SNIP
})
export class AppModule { }
There’s actually a lot going on here, so let’s focus in a bit:
{
provide: APP_INITIALIZER,
useFactory: (appConfigProvider: AppConfigProvider) => {
return () => appConfigProvider.loadConfig();
},
multi: true,
deps: [AppConfigProvider]
}
See that
provide
? Statement? That tells Angular that we’re providing a service with APP_INITIALIZER
as its injection token. This particular token is used by Angular during application initialization (surprising, I know!) Angular will take care of instantiating our service and executing it automatically.
And that
useFactory
part? That registers the factory that Angular will grab during app initialization. It’s a bit meta, but Angular expects our factory to be a function that returns a function. And that function we return is calling our loadConfig
function.
Since that function returns a promise, Angular will pause app initialization until the promise is resolved. And that’s exactly what we want: Angular won’t finish initializing until our
AppConfigProvider
has loaded its configuration data.
The final two parts of our provider definition are fairly simple:
multi
tells Angular that it’s perfectly fine if there are multiple app initializer providers registered. The deps
part tells Angular that our factory has a single dependency, AppConfigProvider
, which will be passed to our factory function.Putting It All Together…
Now that we have all the pieces in place, we should be able to inject our
AppConfigProvider
anywhere we need it, like in our home
component:import { Component } from '@angular/core';
import { AppConfigProvider, AppConfig } from '../config/app-config-provider.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
})
export class HomeComponent {
appConfig: AppConfig;
constructor(
private appConfigProvider: AppConfigProvider
) {
this.appConfig = appConfigProvider.getConfig();
}
}
And at runtime, our component will receive the provider after it has already loaded the configuration data:
Wrapping Up
So that’s one approach to sharing data between ASP.NET Core and Angular. If you know of others, please feel free to share them in the comments!
Oh, and if you want the code, it is available on GitHub.
No comments:
Post a Comment