Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/guide/messaging/transports/signalr.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,37 @@ return await app.RunJasperFxCommands(args);
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/WolverineChat/Program.cs#L63-L81' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_map_wolverine_signalrhub' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Custom hubs

If the default `WolverineHub` isn't enough, you can provide a custom Hub that will be used for all received messages:

<!-- snippet: sample_custom_signalr_hub -->
<a id='snippet-sample_custom_signalr_hub'></a>
```cs
builder.Services.AddSignalR();
builder.Host.UseWolverine(opts =>
{
opts.ServiceName = "Server";

// Hooking up the SignalR messaging transport
// in Wolverine using a custom hub
opts.UseSignalR<THub>();

// A message for testing
opts.PublishMessage<FromSecond>().ToSignalR();
});

var app = builder.Build();

// Syntactic sugar, really just doing:
// app.MapHub<THub>("/messages");
app.MapWolverineSignalRHub<THub>();
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/SignalR/Wolverine.SignalR.Tests/WebSocketTestContext.cs#L135-L154' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_custom_signalr_hub' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Custom hubs must still inherit from `WolverineHub`. It's possible to override `ReceiveMessage`, but if you don't invoke the base functionality you're gonna have a bad time.

## Messages and Serialization

For the message routing above, you'll notice that I utilized a marker interface just to facilitate message routing
Expand Down
7 changes: 7 additions & 0 deletions src/Samples/WolverineChat/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,16 @@

app.UseAuthorization();

#if NET9_0_OR_GREATER
app.MapStaticAssets();
app.MapRazorPages()
.WithStaticAssets();
#endif
#if NET8_0
app.UseStaticFiles();
app.MapRazorPages();
#endif


// This line puts the SignalR hub for Wolverine at the
// designated route for your clients
Expand Down
112 changes: 101 additions & 11 deletions src/Transports/SignalR/Wolverine.SignalR.Tests/WebSocketTestContext.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Wolverine.Runtime;
Expand All @@ -14,7 +15,7 @@ namespace Wolverine.SignalR.Tests;
public abstract class WebSocketTestContext : IAsyncLifetime
{
protected WebApplication theWebApp;
private readonly int Port = PortFinder.GetAvailablePort();
protected readonly int Port = PortFinder.GetAvailablePort();
protected readonly Uri clientUri;

private readonly List<IHost> _clientHosts = new();
Expand All @@ -34,12 +35,12 @@ public async Task InitializeAsync()
});

#endregion

builder.Services.AddSignalR();
builder.Host.UseWolverine(opts =>
{
opts.ServiceName = "Server";

// Hooking up the SignalR messaging transport
// in Wolverine
opts.UseSignalR();
Expand All @@ -54,11 +55,11 @@ public async Task InitializeAsync()
});

var app = builder.Build();

// Syntactic sure, really just doing:
// app.MapHub<WolverineHub>("/messages");
app.MapWolverineSignalRHub();

await app.StartAsync();

// Remember this, because I'm going to use it in test code
Expand All @@ -76,13 +77,13 @@ public async Task<IHost> StartClientHost(string serviceName = "Client")
.UseWolverine(opts =>
{
opts.ServiceName = serviceName;

opts.UseClientToSignalR(Port);

opts.PublishMessage<ToFirst>().ToSignalRWithClient(Port);

opts.PublishMessage<RequiresResponse>().ToSignalRWithClient(Port);

opts.Publish(x =>
{
x.MessagesImplementing<WebSocketMessage>();
Expand All @@ -91,7 +92,7 @@ public async Task<IHost> StartClientHost(string serviceName = "Client")
}).StartAsync();

#endregion

_clientHosts.Add(host);

return host;
Expand All @@ -106,7 +107,96 @@ public async Task DisposeAsync()
await clientHost.StopAsync();
}
}


}

public abstract class WebSocketTestContextWithCustomHub<THub> : IAsyncLifetime where THub : WolverineHub
{
protected WebApplication theWebApp;
protected readonly int Port = PortFinder.GetAvailablePort();
protected readonly Uri clientUri;

private readonly List<IHost> _clientHosts = new();

public WebSocketTestContextWithCustomHub()
{
clientUri = new Uri($"http://localhost:{Port}/messages");
}

public async Task InitializeAsync()
{
var builder = WebApplication.CreateBuilder();

builder.WebHost.ConfigureKestrel(opts =>
{
opts.ListenLocalhost(Port);
});

#region sample_custom_signalr_hub
builder.Services.AddSignalR();
builder.Host.UseWolverine(opts =>
{
opts.ServiceName = "Server";

// Hooking up the SignalR messaging transport
// in Wolverine using a custom hub
opts.UseSignalR<THub>();

// A message for testing
opts.PublishMessage<FromSecond>().ToSignalR();
});

var app = builder.Build();

// Syntactic sugar, really just doing:
// app.MapHub<THub>("/messages");
app.MapWolverineSignalRHub<THub>();
#endregion

await app.StartAsync();

// Remember this, because I'm going to use it in test code
// later
theWebApp = app;
}

// This starts up a new host to act as a client to the SignalR
// server for testing
public async Task<IHost> StartClientHost(string serviceName = "Client")
{
var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.ServiceName = serviceName;

opts.UseClientToSignalR(Port);

opts.PublishMessage<ToFirst>().ToSignalRWithClient(Port);

opts.PublishMessage<RequiresResponse>().ToSignalRWithClient(Port);

opts.Publish(x =>
{
x.MessagesImplementing<WebSocketMessage>();
x.ToSignalRWithClient(Port);
});
}).StartAsync();

_clientHosts.Add(host);

return host;
}

public virtual async Task DisposeAsync()
{
await theWebApp.StopAsync();

foreach (var clientHost in _clientHosts)
{
await clientHost.StopAsync();
}
}

}

public record ToFirst(string Name) : WebSocketMessage;
Expand Down
80 changes: 80 additions & 0 deletions src/Transports/SignalR/Wolverine.SignalR.Tests/custom_hub.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using JasperFx.Core;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Shouldly;
using Wolverine.SignalR.Client;
using Wolverine.SignalR.Internals;
using Wolverine.Tracking;

namespace Wolverine.SignalR.Tests;

public class custom_hub : WebSocketTestContextWithCustomHub<CustomWolverineHub>
{
public static List<string> ReceivedJson = new();

public override async Task DisposeAsync()
{
ReceivedJson.Clear();
await base.DisposeAsync();
}

[Fact]
public async Task publishing_from_server_uses_custom_hub()
{
var client = await StartClientHost();

var tracked = await theWebApp
.TrackActivity()
.IncludeExternalTransports()
.AlsoTrack(client)
.Timeout(10.Seconds())
.SendMessageAndWaitAsync(new FromSecond("Hollywood Brown"));

var record = tracked.Received.SingleRecord<FromSecond>();
record.ServiceName.ShouldBe("Client");
record.Envelope.ShouldNotBeNull();
record.Envelope.Destination.ShouldBe(new Uri($"signalr-client://localhost:{Port}/messages"));
record.Message.ShouldBeOfType<FromSecond>()
.Name.ShouldBe("Hollywood Brown");
}

[Fact]
public async Task publishing_from_client_uses_custom_hub()
{
// This is an IHost that has the SignalR Client
// transport configured to connect to a SignalR
// server in the "theWebApp" IHost
using var client = await StartClientHost();

var tracked = await client
.TrackActivity()
.IncludeExternalTransports()
.AlsoTrack(theWebApp)
.Timeout(10.Seconds())
.ExecuteAndWaitAsync(c => c.SendViaSignalRClient(clientUri, new ToSecond("Hollywood Brown")));

var record = tracked.Received.SingleRecord<ToSecond>();
record.ServiceName.ShouldBe("Server");
record.Envelope.ShouldNotBeNull();
record.Envelope.Destination.ShouldBe(new Uri("signalr://wolverine"));
record.Message.ShouldBeOfType<ToSecond>()
.Name.ShouldBe("Hollywood Brown");

ReceivedJson.Count.ShouldBe(1);
}
}

public class CustomWolverineHub(SignalRTransport endpoint, ILogger<CustomWolverineHub> logger) : WolverineHub(endpoint)
{
public override Task OnConnectedAsync()
{
logger.LogInformation("Client connected with ID {ConnectionId}", Context.ConnectionId);
return base.OnConnectedAsync();
}

public override Task ReceiveMessage(string json)
{
custom_hub.ReceivedJson.Add(json);
return base.ReceiveMessage(json);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System.Diagnostics;
using System.Text.Json;
using JasperFx.Core;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using Wolverine.Configuration;
using Wolverine.Runtime;
using Wolverine.Runtime.Interop;
Expand All @@ -23,8 +23,8 @@ internal static Uri TranslateToWolverineUri(Uri uri)
{
return new Uri($"{SignalRClientTransport.ProtocolName}://{uri.Host}:{uri.Port}/{uri.Segments.Last()}");
}
public SignalRClientEndpoint(Uri uri, SignalRClientTransport parent) : base(TranslateToWolverineUri(uri),EndpointRole.Application)

public SignalRClientEndpoint(Uri uri, SignalRClientTransport parent) : base(TranslateToWolverineUri(uri), EndpointRole.Application)
{
_parent = parent;
SignalRUri = uri;
Expand All @@ -49,7 +49,7 @@ public override async ValueTask<IListener> BuildListenerAsync(IWolverineRuntime
_mapper ??= BuildCloudEventsMapper(runtime, JsonOptions);

Logger = runtime.LoggerFactory.CreateLogger<SignalRClientEndpoint>();

await _connection.StartAsync();

_connection.On(SignalRTransport.DefaultOperation, [typeof(string)], (args =>
Expand All @@ -61,13 +61,13 @@ public override async ValueTask<IListener> BuildListenerAsync(IWolverineRuntime
Logger.LogDebug("Received an empty message, ignoring");
return Task.CompletedTask;
}

return ReceiveAsync(json);
}));

return this;
}

internal async Task ReceiveAsync(string json)
{
if (Receiver == null || _mapper == null) return;
Expand All @@ -88,7 +88,7 @@ internal async Task ReceiveAsync(string json)
Logger?.LogError(e, "Unable to receive a message from SignalR");
}
}

public IReceiver? Receiver { get; private set; }

protected override ISender CreateSender(IWolverineRuntime runtime)
Expand Down Expand Up @@ -142,7 +142,7 @@ public async ValueTask SendAsync(Envelope envelope)
{
if (_mapper == null || _connection == null)
throw new InvalidOperationException($"SignalR Client {Uri} is not initialized");

var json = _mapper.WriteToString(envelope);

await _connection.InvokeAsync(nameof(WolverineHub.ReceiveMessage), json);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text.Json;
using JasperFx.Core.Reflection;
using Microsoft.AspNetCore.SignalR;
using Wolverine.Configuration;

namespace Wolverine.SignalR.Client;
Expand Down Expand Up @@ -38,9 +39,7 @@ public static Uri UseClientToSignalR(this WolverineOptions options, string url,
/// <param name="route">Default is messages. Route pattern where you have mapped the WolverineHub</param>
/// <returns></returns>
public static Uri UseClientToSignalR(this WolverineOptions options, int port, string route = "messages")
{
return options.UseClientToSignalR($"http://localhost:{port}/{route}");
}
=> options.UseClientToSignalR($"http://localhost:{port}/{route}");

/// <summary>
/// Send a message via a SignalR Client for the given server Uri in the format "http://localhost:[port]/[hub url]"
Expand Down Expand Up @@ -79,7 +78,7 @@ public static void ToSignalRWithClient(this IPublishToExpression publishing, int
var url = $"http://localhost:{port}/{relativeUrl}";
publishing.ToSignalRWithClient(url);
}

/// <summary>
/// Route messages via a SignalR Client pointed at the supplied absolute Url
/// </summary>
Expand Down
Loading
Loading