dotnet / aspire

Tools, templates, and packages to accelerate building observable, production-ready apps
https://learn.microsoft.com/dotnet/aspire
MIT License
3.87k stars 467 forks source link

SignalR client address resolution #1356

Open victorverdoodt opened 11 months ago

victorverdoodt commented 11 months ago

Hello,

I am working on a project integrating SignalR with .NET Aspire and have encountered a specific challenge related to automatic address resolution for connections.

We are using SignalR Client for real-time communication, and currently, the configuration of the SignalR hub connection address is done manually in the appsettings.json.

Specific Challenge: While in HttpClient, using a generic address like http://apiservice automatically resolves the correct address, this approach does not work with the SignalR client. When attempting to use http://apiservice or ws://apiservice with the SignalR client, the connection fails to establish, indicating an issue with automatic address resolution.

Is there a way to configure the SignalR client in .NET Aspire to automatically resolve the connection address, similar to what happens with HttpClient? If so, what are the steps to implement this functionality, or where can I find relevant documentation? I would greatly appreciate any guidance, suggestions, or code examples that could help resolve this issue. Thank you very much for your assistance!

KSemenenko commented 11 months ago

maybe yarp can solve this?

victorverdoodt commented 11 months ago

I tried using YARP, but I couldn't succeed. I managed to find a workaround using var resolve = _configuration.GetValue("services:apiservice:1");".

DylanPMunyard commented 9 months ago

I also came into the same issue - and was interested to try and use service discovery for SignalR clients. I ended up pinching the same code from UseServiceDiscovery .

This is in a Razor page, I had to inject IConfiguration, so that service resolving can presumably look up the service env vars:

@inject IConfiguration Configuration;

var hubBuilder = new HubConnectionBuilder()
    .WithUrl("http://apiservice/eventhub");

hubBuilder.Services.AddSingleton(Configuration);
hubBuilder.Services.AddServiceDiscovery();
hubBuilder.Services.AddSingleton<IConfigureOptions<HttpConnectionOptions>, ServiceDiscoveryHttpOptionsConfigurer>(provider =>
{
    var timeProvider = provider.GetService<TimeProvider>() ?? TimeProvider.System;
    var selectorProvider = provider.GetRequiredService<IServiceEndPointSelectorProvider>();
    var resolverProvider = provider.GetRequiredService<ServiceEndPointResolverFactory>();
    var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider);

    return new ServiceDiscoveryHttpOptionsConfigurer(registry);
});

_hubConnection = hubBuilder.Build();

public class ServiceDiscoveryHttpOptionsConfigurer : IConfigureOptions<HttpConnectionOptions>
{
    private readonly HttpServiceEndPointResolver _registry;

    public ServiceDiscoveryHttpOptionsConfigurer(HttpServiceEndPointResolver registry)
    {
        _registry = registry;
    }

    public void Configure(HttpConnectionOptions options)
    {
        options.HttpMessageHandlerFactory = handler => new ResolvingHttpDelegatingHandler(_registry, handler);
    }
}
davidfowl commented 9 months ago

cc @BrennanConroy

davidfowl commented 9 months ago

The easier way to wire this up assuming you have access to the client factory would be something like this:

public static class HubConnectionExtensions
{
    public static IHubConnectionBuilder WithUrl(this IHubConnectionBuilder builder, string url, IHttpMessageHandlerFactory clientFactory)
    {
        return builder.WithUrl(url, options =>
        {
            options.HttpMessageHandlerFactory = _ => clientFactory.CreateHandler();
        });
    }
}

You need to inject the IHttpMessageHandlerFactory into your code and pass it into the WithUrl call above.

davidfowl commented 9 months ago

@ReubenBond we have to clean up the service discovery API and make it easy to get to wire it up.

@IEvangelist I think we should document using the Http client factory interfaces to get either an HttpClient or an HttpMessageHandler as the first-class way to do integration with libraries that have not been SD enlightened.

tthiery commented 6 months ago

I see similar behavior for the WebSockets (blazor, interactiveserver connecting to apiserver for update notification). I get a name on the wss protocol resolution error (and a delay of 2s). I tried to replace the web socket factory but I am not understanding how to change the name resolver there. When I change the transport to LongPolling and use @davidfowl hack of above, it works smooth as expected.

Lindsay-Mathieson commented 5 months ago

The easier way to wire this up assuming you have access to the client factory would be something like this:

Thanks, took me a bit to figure out, but it worked.

varndellwagglebee commented 4 months ago

I'm having the same problem between react (send/receive), .net 8(signalr here) and aspire, where the api is using signalr stand alone

andi0b commented 1 month ago

I'm currently struggling with service discovery for ClientWebSocket (non-SignalR websocket communication). It would be great to get an API for resolving service discovery on demand. There are probably many cases service discovery needs to be used without HttpClient, HttpMessageHandler, etc.

something like .GetRequiredService<ServiceDiscoverySomething>().ResolveUrl(url)

Edit: After some digging I found out that ClientWebSocket takes a HttpMessageInvoker on the ConnectAsync method instead of the Options where I looked for it. So Service discovery does work with ClientWebSocket. Let's hope this snipped goes into Copilot/etcs training data set :D

var httpClient = scope.ServiceProvider.GetRequiredService<IHttpClientFactory>().CreateClient("named-client");
var client = new ClientWebSocket();
await client.ConnectAsync(new Uri($"ws://service/something/somethingelse"), httpClient);
// AppHost Program.cs
// ws:// needs to be an endpoint on a service though, didn't work with a http:// endpoint
var container = builder.AddContainer("service", ...)
   .WithHttpEndpoint(targetPort: 80)
   .WithEndpoint(targetPort: 80, scheme: "ws");

var project = builder.AddProject<...>("...")
   .WithReference(container.GetEndpoint("http"))
   .WithReference(container.GetEndpoint("ws"));
Plasmadog commented 3 weeks ago

I'm currently struggling with service discovery for ClientWebSocket (non-SignalR websocket communication). It would be great to get an API for resolving service discovery on demand. There are probably many cases service discovery needs to be used without HttpClient, HttpMessageHandler, etc.

something like .GetRequiredService<ServiceDiscoverySomething>().ResolveUrl(url)

I can suggest one such use case; constructing a hyperlink to an endpoint on one of those services. I have a service that sends emails to users, and those emails need to include links to various parts of our platform. Some of those links are to services that are configured via service discovery, but there does not seem to be an easy way of getting that URL except by trying to send something to it.