Build-time configuration for Blazor WebAssembly Apps using MS Build properties
.NET Core in general has a nice configuration API that I've been fond of since .NET Core 1.x. But since Blazor WebAssembly apps run completely on the client, the configuration works a little differently that one might expect.
The way configuration using appsettings.json
works is that, a HTTP request is sent to a appsettings.json
file. So if the app is available at https://xyz.com
, the request goes to https://xyz.com/appsettings.json
. You can also use another file-name or path if you want to.
But if you're just trying to do something like set the Web API's Base URL, a HTTP request on startup can feel like an overkill. This post shall try and address that.
Source Code: https://github.com/gldraphael/blazor-build-time-configuration
First, we create a custom Attribute
to hold our configuration:
[AttributeUsage(AttributeTargets.Assembly, Inherited = false)]
sealed class BuildConfigurationAttribute : Attribute
{
public string? BaseUrl { get; }
public string BuildDate { get; }
public BuildConfigurationAttribute(string baseUrl, string buildDate)
{
BaseUrl = string.IsNullOrWhiteSpace(baseUrl) ? null : baseUrl;
BuildDate = buildDate ?? "n/a";
}
}
Next, we apply this Attribute to the Assembly by passing MS Build properties in the .csproj
file:
<ItemGroup>
<AssemblyAttribute Include="WasmApp.BuildConfigurationAttribute">
<_Parameter1>$(BaseUrl)</_Parameter1>
<_Parameter2>$(BuildDate)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
We could, optionally, set default values for MS Build properties in the same file, like so:
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
<BaseUrl>https://example.org</BaseUrl>
<BuildDate>$([System.DateTime]::UtcNow.ToString("dd MMM, yyyy"))</BuildDate>
</PropertyGroup>
Next, we access the ApiConfigurationAttribute
from Main()
to configure it with DI, so that it's accessible to the rest of the application in the usual way:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
// Get an instance of the attribute applied to the assembly
var apiConfig = Assembly.GetAssembly(typeof(Program)).GetCustomAttribute<BuildConfigurationAttribute>();
// Build a config options and register it with DI
var baseUrl = new Uri(apiConfig.BaseUrl ?? builder.HostEnvironment.BaseAddress);
builder.Services.Configure<AppOptions>(o =>
{
o.BaseUrl = baseUrl;
o.BuildDate = apiConfig.BuildDate;
});
// Set the BaseUrl on the HttpClient
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = baseUrl });
await builder.Build().RunAsync();
}
// AppOptions is defined as
internal class AppOptions
{
public Uri BaseUrl { get; set; }
public string BuildDate { get; set; }
}
And now we can use it as usual:
@page "/"
@inject IOptions<AppOptions> options
<pre>
<strong>Base URL:</strong> @options.Value.BaseUrl
<strong>Last Build:</strong> @options.Value.BuildDate
</pre>
That's it. dotnet run
or running it within Visual Studio will use the dev-environment-friendly default values:
Now all you need to do to set (or override) the MS Build properties (BaseUrl
in this case) in the other environments is to use the -p:
or /p:
switch:
dotnet build -p:BaseUrl="https://prod.example.com"
dotnet run --no-build
We now have a nice mechanism to use different API Base URLs for different environments, and have environment specific build-time configuration without the need to make a HTTP request.
I think the decision of making a HTTP request to the appsettings.json
file was the right one. Because to me a "configurable" app is one that reads configuration at runtime. Build time configuration is like hardcoding the strings — well, different strings — for each environment in our case. So if I were to use docker, I'd now need to maintain an image for each environment if I choose to use "build time configuration".