dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.15k stars 9.92k forks source link

[Blazor] Provide a mechanism to pass IConfiguration variables from Server to WASM #56772

Open hades200082 opened 1 month ago

hades200082 commented 1 month ago

Is there an existing issue for this?

Is your feature request related to a problem? Please describe the problem.

I have a .NET Aspire AppHost that is passing an environment variable to the Blazor project (Server).

Simplified example below:

// Aspire AppHost Program.cs

var api = builder.AddProject<Projects.Application_Host_Api>("TemplateProject-Api");
builder.AddProject<Projects.Presentation_Blazor>("TemplateProject-Presentation-Blazor")
  .WithReference(api)
  .WithEnvironment("API_BASE_URL", api.Resource.GetEndpoint("https"))
// Presentation.Blazor.Client.Program.cs (the WASM bit)

var apiBaseUrl = builder.Configuration.GetValue<string>("API_BASE_URL"); // This is always null
builder.Services.AddHttpClient("API", client => { client.BaseAddress = new Uri(apiBaseUrl); })
  .AddHttpMessageHandler<AuthenticatedHttpClientHandler>();

But this doesn't work because the Blazor application is split into two parts:

This would also hold true when deploying the application since any environment variables set in the Azure App Service or other deployment environment wouldn't be accessible to the WASM part of the app.

This is in stark contrast to other frontend frameworks. Take NextJS for example:

Using the Aspire.Hosting.NodeJs package in my AppHost I can do something like this:

builder.AddNpmApp("NextJS", "../Presentation/Presentation.NextJs", "run dev")
    .WithReference(api)
    .WithEnvironment("NEXT_PUBLIC_API_BASE_URL", api.Resource.GetEndpoint("http"));

Then I can access the environment variable in my NextJS app, even in client components in the browser since I prefixed the variable name with NEXT_PUBLIC_.

NextJS takes the NEXT_PUBLIC_ prefixed environment variables present at build-time and bundles them into the client JS using WebPack.

Describe the solution you'd like

I think there's a need to ensure that this is an opt-in feature so we can ensure that secrets aren't leaked to the client accidentally. I would foresee using it something like this:

// In the Blazor Server Program.cs
builder.AddClientConfiguration((cfg) => {
  cfg.Add("MY_VARIABLE", "Anything I like here");

  // for things that already exist in the Server configuration this could be shortcut:
  cfg.Add("API_BASE_URL"); // Automatically adds builder.Configuration["API_BASE_URL"] from Server config

  // This could also be used for configuration sections
  cfg.AddSection("Authentication") // assuming no secrets here, but that's for the dev using this to ensure
})

The server would then dynamically merge this config with what is already hard-coded in the client's wwwroot/appsettings.json either rewriting the json file on startup or by passing the new configs in the Blazor.start() JS for webAssembly

Additional context

Discord conversation starting at the link below in the DotNetEvolution #Blazor channel.

https://discord.com/channels/732297728826277939/732297874062311424/1261431446044672032

hades200082 commented 1 month ago

I've created this which seems to work for my use-case - maybe it will help others:

I added the following classes in the Blazor server project...

// ClientConfiguration.cs
internal sealed class ClientConfiguration(IConfiguration configuration)
{
    public Dictionary<string, object> Configurations { get; private set; } = new();

    public void Add(string key)
    {
        Configurations[key] = configuration[key];
    }

    public void Add(string key, object value)
    {
        Configurations[key] = value;
    }

    public void AddSection(string sectionName)
    {
        var section = configuration.GetSection(sectionName);
        foreach (var child in section.GetChildren())
        {
            Add(child.Key, child.Value);
        }
    }
}
// ClientConfigurationExtensions.cs
internal static class ClientConfigurationExtensions
{
    public static void ConfigureClient(this WebApplicationBuilder builder, Action<ClientConfiguration> configure)
    {
        var clientConfig = new ClientConfiguration(builder.Configuration);
        configure(clientConfig);
        builder.Services.AddSingleton(clientConfig);
    }
}

Then, in the Blazor Server project's Program.cs...

var builder = WebApplication.CreateBuilder(args);

// ... snip ...

builder.ConfigureClient(cfg =>
    {
        cfg.AddSection("Authentication"); // copies the "Authentication" section from the Blazor Server config
        cfg.Add("API_BASE_URL"); // copies the API_BASE_URL variable from the Blazor Server config
    });

// ... snip ...

var app = builder.Build();

// Add this anywhere BEFORE `app.UseStaticFiles()`
app.MapGet("appsettings.json", ([FromServices]ClientConfiguration cfg) => Results.Json(cfg.Configurations));

// ... snip ...

I suspect this sort of thing could easily be added to Blazor as a built-in thing.

kratinator commented 1 month ago

Great workaround ! Thank you.