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".