Skip to content

Detailed SignalRGen Tutorial

This comprehensive tutorial walks through creating a complete SignalR solution with SignalRGen, including project setup and configuration.

TIP

If you want a more concise version, head over to getting started

Project Setup

Create Solution Structure

First, let's create a solution with three projects:

  • A server project to host our SignalR hub
  • A client project to connect to the hub
  • A shared interface project that defines our SignalR contract
shell
# Create solution
mkdir MySignalRGenExample
cd MySignalRGenExample
dotnet new sln -n MySignalRGenExample

# Create server project
dotnet new web -n MyServer
dotnet sln add MyServer/MyServer.csproj

# Create client project
dotnet new web -n MyClient
dotnet sln add MyClient/MyClient.csproj

# Create shared interface project
dotnet new classlib -n MySharedInterface
dotnet sln add MySharedInterface/MySharedInterface.csproj

Install Required Packages

Install SignalRGen in the shared interface project:

shell
cd MySharedInterface
dotnet add package SignalRGen
dotnet add package Microsoft.AspNetCore.SignalR.Client

Then modify the package reference in your MySharedInterface.csproj file:

xml
<PackageReference Include="SignalRGen" Version="1.0.0">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

Add Project References

Make the shared interface available to both client and server:

shell
# From the solution root
dotnet add MyServer/MyServer.csproj reference MySharedInterface/MySharedInterface.csproj
dotnet add MyClient/MyClient.csproj reference MySharedInterface/MySharedInterface.csproj

Define the SignalR Interface

Create the interface in your shared project:

csharp
// MySharedInterface/IPingPongHub.cs
using SignalRGen.Generator;

namespace MySharedInterface;

[HubClient(HubUri = "ping-pong")]
public interface IPingPongHub
{
    // Client calls this method on the server
    [ClientToServerMethod]
    Task Ping(string message);

    // Server calls this method on the client
    Task Pong(string answer);
}

Server Implementation

Create the Hub

Implement the hub in your server project:

csharp
// MyServer/PongHub.cs
using Microsoft.AspNetCore.SignalR;
using MySharedInterface;

namespace MyServer;

public class PongHub : Hub<IPingPongHub>, IPingPongHub
{
    private readonly ILogger<PongHub> _logger;

    public PongHub(ILogger<PongHub> logger)
    {
        _logger = logger;
    }
    
    public Task Ping(string message)
    {
        _logger.LogInformation("Received Ping: {Message}", message);
        return Clients.All.Pong("Hey, here is the server talking!");
    }

    public Task Pong(string answer)
    {
        throw new InvalidOperationException("This is a Server-to-Client method, hence it is not implemented!");
    }
}

Configure Server Endpoint

Register the hub in your server's Program.cs:

csharp
// MyServer/Program.cs
using MyServer;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();

var app = builder.Build();

// Map the SignalR hub to the path matching our HubUri
app.MapHub<PongHub>($"/{PingPongHub.HubUri}");

app.Run();

Client Implementation

Register the Hub

Configure the hub in your client's Program.cs:

csharp
// MyClient/Program.cs
using MyClient;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Register the SignalR hub generated by SignalRGen
builder.Services.AddSignalRHubs(c => c.HubBaseUri = new Uri("http://localhost:5160"))
    .WithPingPongHub();

// Register our background worker
builder.Services.AddHostedService<Worker>();

var app = builder.Build();

// Add an endpoint to trigger a ping
app.MapGet("/ping", async ([FromServices] PingPongHub hub) =>
{
    await hub.InvokePingAsync("Hello from the client!");
    return "Ping sent!";
});

app.Run();

Create a Background Service

Implement a worker service to maintain the SignalR connection:

csharp
// MyClient/Worker.cs
using MySharedInterface;

namespace MyClient;

public class Worker : IHostedService
{
    private readonly ILogger<Worker> _logger;
    private readonly PingPongHub _hub;

    public Worker(ILogger<Worker> logger, PingPongHub hub)
    {
        _logger = logger;
        _hub = hub;
    }
    
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting SignalR connection...");
        
        // Subscribe to the Pong event
        _hub.OnPong += OnPongReceived;

        // Start the connection
        return _hub.StartAsync(cancellationToken: cancellationToken);
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Stopping SignalR connection...");
        
        // Unsubscribe from the event
        _hub.OnPong -= OnPongReceived;
        
        // Stop the connection
        return _hub.StopAsync(cancellationToken: cancellationToken);
    }

    private Task OnPongReceived(string answer)
    {
        _logger.LogInformation("Received Pong: {Answer}", answer);
        return Task.CompletedTask;
    }
}

Testing the Implementation

  1. Start both projects (in separate terminals):
shell
# Terminal 1 - Start the server
cd MyServer
dotnet run

# Terminal 2 - Start the client
cd MyClient
dotnet run
  1. Navigate to http://localhost:5000/ping in your browser (adjust the port if necessary).

  2. The client will send a ping message to the server, which will respond with a pong.

  3. Check the client console logs to see the received pong message.

Understanding What Happened

  1. The IPingPongHub interface defined our SignalR contract
  2. SignalRGen generated a strongly typed client (PingPongHub)
  3. The server implemented the hub with methods matching our interface
  4. The client connected to the hub and subscribed to events
  5. When the /ping endpoint was triggered, the client sent a message to the server
  6. The server responded with a pong message, which the client received as an event

Important Notes

  • Naming Convention: Note that the generated client class has the same name as the interface but without the "I" prefix.
  • Event Handling: Server-to-client methods are exposed as events on the generated client with an "On" prefix.
  • Method Invocation: Client-to-server methods are exposed as methods on the generated client with an "Invoke" prefix and "Async" suffix.