Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Aspire service discovery to work in a hosted Blazor WebAssembly project #7524

Open
BenjaminCharlton opened this issue Feb 11, 2025 · 0 comments
Labels
area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication
Milestone

Comments

@BenjaminCharlton
Copy link

BenjaminCharlton commented Feb 11, 2025

This is related to my pull request here: #7162

Background and Motivation

.NET Aspire doesn't currently (as of early 2025) facilitate a Blazor WebAssembly (client) app discovering Aspire resources, even if the app has been added to the distributed application, because Blazor WebAssembly apps run in the browser and are "standalone". This has been commented on here:

I suppose Microsoft's expectation was that client apps (including Blazor WASM clients and any SPAs) will need to be aware of the web APIs they're supposed to call and that they will store these in appsettings.json or appsettings.{environmentName}.json without relying on Aspire. This works fine, but if the endpoint changes, or if it differs in your development and production environments, you have to manage those changes in your client app as well as your other resources. This is the kind of problem Aspire is intended to solve (and does so exceedingly well, apart from Blazor). These changes to Aspire will achieve that.

This proposal would integrate my Nuget package Aspire4Wasm (source code here) into Aspire (with the addition of only 5 new classes and 2 interfaces, and changing none of the existing ones).

The changes allow an Aspire AppHost to define web APIs that a Blazor app can access, and pass that service discovery information to the app by writing to its appsettings.{environment}.json files.

Proposed API

You can see the diffs I'm proposing here: https://github.com/dotnet/aspire/pull/7162/files

Usage Examples

In your Aspire AppHost project's Program.cs file:

  1. Add the Web Api projects you want your client to be able to call with the existing AddProject method
  2. Add your Blazor Server app with AddProject method then chain a call to the new AddWebAssemblyClient to add your client app.
  3. Then chain a call to WithReference to point the client to each web API (you can repeat this for as many Web APIs as you need)

In your WebAssembly client's Program.cs file:

  1. Call AddServiceDiscovery
  2. Configure your HttpClients either globally or one at a time. In each client's BaseAddress property, use the name you gave to the resource in your AppHost and Aspire's https+http:// syntax.

See the example below:

Example Program.cs in AppHost

var builder = DistributedApplication.CreateBuilder(args);

var inventoryApi = builder.AddProject<Projects.AspNetCoreWebApi>("inventoryapi");
var billingApi = builder.AddProject<Projects.SomeOtherWebApi>("billingapi");

builder.AddProject<Projects.Blazor>("blazorServer")
    .AddWebAssemblyClient<Projects.Blazor_Client>("blazorWasmClient")
    .WithReference(inventoryApi)
    .WithReference(billingApi);

builder.Build().Run();

Example Program.cs in your Blazor WebAssembly Client

Install (on the WebAssembly client) the Microsoft.Extensions.ServiceDiscovery Nuget package to get the official Aspire service discovery functionality that is going to read your resource information from your app settings. Then add the code below:

builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(static http =>
{
    http.AddServiceDiscovery();
});

builder.Services.AddHttpClient<IInventoryService, InventoryService>(
    client =>
    {
        client.BaseAddress = new Uri("https+http://inventoryapi"); // The syntax for service discovery in Aspire will now work!
    });

    builder.Services.AddHttpClient<IBillingService, BillingService>(
    client =>
    {
        client.BaseAddress = new Uri("https+http://billingapi"); // The syntax for service discovery in Aspire will now work!
    });

Default behaviour

Using the default behaviour (in the example) your AppHost will write the service discovery information for all the referenced resources into the appsettings.{environmentName}.json file of your client app for you.
It uses the following structure. The structure is important because it allows Aspire to "discover" the information on the client.

{
  "Services": {
    "inventoryapi": {
      "https": [
        "https://localhost:1234"
      ],
      "http": [
        "http://localhost:4321"
      ]
    },
    "billingapi": {
      "https": [
        "https://localhost:9876"
      ],
      "http": [
        "http://localhost:6789"
      ]
    }
  }
}

Custom behaviours (optional)

If you want to serialize the service discovery information some other way in your WebAssembly application (for example, in a different JSON file, or in an XML file) you can do so in the AppHost Program.cs by creating a custom implementation of IServiceDiscoveryInfoSerializer and passing it to the call to AddWebAssemblyClient via the WebAssemblyProjectBuilderOptions class, like this:

var builder = DistributedApplication.CreateBuilder(args);

var inventoryApi = builder.AddProject<Projects.AspNetCoreWebApi>("inventoryapi");
var billingApi = builder.AddProject<Projects.SomeOtherWebApi>("billingapi");

builder.AddProject<Projects.Blazor>("blazorServer")
    .AddWebAssemblyClient<Projects.Blazor_Client>("blazorWasmClient" options => {
        options.ServiceDiscoveryInfoSerializer = yourImplementation;
    })
    .WithReference(inventoryApi)
    .WithReference(billingApi);

builder.Build().Run();

If you choose to make a custom implementation of IServiceDiscoveryInfoSerializer, you only need to override one method:

public void SerializeServiceDiscoveryInfo(IResourceWithServiceDiscovery resource) { }

Note: If you choose to override the default behaviour with an output format that Aspire can't read from your WebAssembly client app, you'll also need to override the discovery behaviour on the client, which is outside the scope of what I've developed here.

Using service discovery to configure CORS in your web API (optional)

You can also reference one or more Blazor apps from a web API. One use case would be to configure Cross Origin Resource Sharing (CORS) in the web API to grant access to your clients to submit HTTP requests.
(Note: None of this section below demonstrates my changes to the existing API of Aspire; it's just a use case demonstrating how the existing functionality can be leveraged to make use of my proposed changes.)

Example updated Program.cs in AppHost project

var builder = DistributedApplication.CreateBuilder(args);

var blazorServer = builder.AddProject<Projects.InMyCountry_UI_Server>("blazorServer");

var webApi = builder.AddProject<Projects.InMyCountry_WebApi>("inventoryApi")
 .WithReference(blazorServer) // This will pass the endpoint URL of the Blazor app to the web API so that it can be added as a trusted origin in CORS.
 .WaitFor(blazorServer);

blazorServer.AddWebAssemblyClient<Projects.InMyCountry_UI_Client>("blazorWasmClient") // Now we can add the Blazor WebAssembly (client) app in the Aspire4Wasm package.
    .WithReference(webApi); // And pass the Blazor client a reference to the web API

builder.Build().Run();

The example above will add the following to the appsettings{.Environment}.json file of the web API project

{
  "Clients": [
    "https://{url of your blazor app should be here}"
  ]
}

It should add as many clients as you configured in the AppHost.

Example continued in Program.cs in the web API project

Now that the web API has a reference to the Blazor app in appsettings, we can configure CORS like this:

var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();

var clients = builder.Configuration.GetSection("Clients").Get<string[]>() ?? []; // Get the clients from the list in appsettings.

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy.WithOrigins(clients); // Add the clients as allowed origins for cross origin resource sharing.
        policy.AllowAnyMethod();
        policy.WithHeaders("X-Requested-With");
        policy.AllowCredentials();
    });
});

// Etc.

Troubleshooting

These are just a few things that I noticed helped me and I hope they help you too.

  • You don't need a launchsettings.json in your webassembly client project. The one in your Blazor server project will do.
  • In the launchsettings.json of your blazor server project, I recommend that you set launchBrowser to false. This means that when the Aspire dashboard opens up, you'll need to click the link to open up your Blazor client. This is good! If you don't do this, your Blazor client is going to launch on a random port chosen by Aspire. When launched on a random port, your web API might reject the requests of your Blazor client because it doesn't have the expected origin to comply with the API's CORS policy. I tried to stop this happening but couldn't, so this is my workaround.

Alternative Designs

I considered various names for the method. I preferred simply AddProject like the others, but that is taken. I considered AddClientApp and AddBlazorWasm and AddBlazorWebAssemblyClient but I settled on AddWebAssemblyClient.

I considered other names for IServiceDiscoveryInfoSerializer and its method SerializeServiceDiscoveryInfo. For example AppSettingsWriter and WriteServiceJson. I chose those names because they weren't wedded to JSON, XML or any other means of writing the service discovery information to the client app.

Risks

These changes will not make the Aspire developer experience with Blazor totally seamless. For example, although it's been great for my hosted Blazor WebAssembly app, I haven't got it to work with stand-alone Blazor WebAssembly apps yet. I haven't figured out how to stop the app launching on a random port as well as the one specified in launchsettings.json but I get around this by setting launchBrowser to false in the Blazor server app's launchSettings.json.

However, this is a step in the right direction and will help make Aspire a bit more Blazor-friendly. If you want to see what it does and doesn't do before accepting the changes, try the Nuget package Aspire4Wasm (version 3.0.0 at the time of writing).

@BenjaminCharlton BenjaminCharlton changed the title allow Aspire service discovery to work in a hosted Blazor WebAssembly project Allow Aspire service discovery to work in a hosted Blazor WebAssembly project Feb 11, 2025
@davidfowl davidfowl added the area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication label Feb 11, 2025
@davidfowl davidfowl added this to the Backlog milestone Feb 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication
Projects
None yet
Development

No branches or pull requests

2 participants