Skip to content

Tharga/Communication

Repository files navigation

Tharga Communication

GitHub repo Issues NuGet Nuget License

A SignalR-based communication framework for .NET with built-in message handler patterns for request-response and fire-and-forget messaging between clients and servers.

Features

  • Fire-and-forget messaging - Send one-way messages from client to server or server to client(s)
  • Request-response messaging - Send a request and await a typed response with configurable timeout
  • Automatic handler discovery - Message handlers are discovered and registered via dependency injection
  • Client connection tracking - Track connected clients with metadata (machine name, app type, version)
  • Automatic reconnection - Configurable reconnect delays for client connections
  • Extensible storage - Abstract repository pattern for client state with an in-memory default

Installation

dotnet add package Tharga.Communication

Quick start

Server setup

var builder = WebApplication.CreateBuilder(args);

builder.AddThargaCommunicationServer(options =>
{
    options.RegisterClientStateService<MyClientStateService>();
    options.RegisterClientRepository<MemoryClientRepository<ClientConnectionInfo>, ClientConnectionInfo>();
});

var app = builder.Build();
app.UseThargaCommunicationServer();
app.Run();

Client setup

Add the configuration section to appsettings.json:

{
  "Tharga": {
    "Communication": {
      "ServerAddress": "https://localhost:5001"
    }
  }
}

Register the client services:

var builder = Host.CreateApplicationBuilder(args);
builder.AddThargaCommunicationClient();

Creating a message handler (fire-and-forget)

public record MyNotification(string Text);

public class MyNotificationHandler : PostMessageHandlerBase<MyNotification>
{
    public override Task Handle(MyNotification message)
    {
        Console.WriteLine(message.Text);
        return Task.CompletedTask;
    }
}

Register the handler in DI:

builder.Services.AddTransient<PostMessageHandlerBase<MyNotification>, MyNotificationHandler>();

Creating a message handler (request-response)

public record PingRequest(string Message);
public record PingResponse(string Reply);

public class PingHandler : SendMessageHandlerBase<PingRequest, PingResponse>
{
    public override Task<PingResponse> Handle(PingRequest message)
    {
        return Task.FromResult(new PingResponse($"Pong: {message.Message}"));
    }
}

Sending messages

From the client:

public class MyService(IClientCommunication client)
{
    public async Task NotifyServer()
    {
        await client.PostAsync(new MyNotification("Hello from client"));
    }

    public async Task<PingResponse> PingServer()
    {
        return await client.SendMessage<PingRequest, PingResponse>(new PingRequest("Ping"));
    }
}

From the server:

public class MyServerService(IServerCommunication server)
{
    public async Task NotifyClient(string connectionId)
    {
        await server.PostAsync(connectionId, new MyNotification("Hello from server"));
    }

    public async Task NotifyAll()
    {
        await server.PostToAllAsync(new MyNotification("Broadcast message"));
    }

    public async Task<PingResponse> PingClient(string connectionId)
    {
        var response = await server.SendMessageAsync<PingRequest, PingResponse>(
            connectionId, new PingRequest("Ping"));
        return response.Value;
    }
}

Implementing a client state service

public class MyClientStateService : ClientStateServiceBase<ClientConnectionInfo>
{
    public MyClientStateService(IServiceProvider sp, IOptions<CommunicationOptions> options)
        : base(sp, options) { }

    protected override ClientConnectionInfo Build(IClientConnectionInfo info) =>
        new()
        {
            Instance = info.Instance,
            ConnectionId = info.ConnectionId,
            Machine = info.Machine,
            Type = info.Type,
            Version = info.Version,
            IsConnected = info.IsConnected,
            ConnectTime = info.ConnectTime
        };

    protected override ClientConnectionInfo BuildDisconnect(ClientConnectionInfo info, DateTime disconnectTime) =>
        info with { IsConnected = false, DisconnectTime = disconnectTime };
}

Configuration

Client options

Property Description Default
ServerAddress The server URL to connect to (required)
Pattern The hub endpoint pattern "hub"
ReconnectDelays Delays between reconnection attempts [0s, 2s, 10s, 30s]
ApiKey API key sent to the server for authentication (none)
AdditionalAssemblies Extra assemblies to scan for message handlers (none)
SendMessageTimeout Default timeout for request-response messages 60s

Server options

The server requires registering a ClientStateServiceBase implementation and a ClientRepositoryBase implementation via the options callback. Use MemoryClientRepository<T> for an in-memory default.

Property Description Default
PrimaryApiKey Primary API key for client authentication (none)
SecondaryApiKey Secondary API key for zero-downtime key rotation (none)
AdditionalAssemblies Extra assemblies to scan for message handlers (none)

When no API keys are configured on the server, all connections are accepted (backwards compatible). When one or both keys are set, clients must provide a matching key via the X-Api-Key header.

Authentication

To secure the SignalR connection with API key authentication:

Server — configure one or both keys:

builder.AddThargaCommunicationServer(options =>
{
    options.PrimaryApiKey = builder.Configuration["Communication:PrimaryApiKey"];
    options.SecondaryApiKey = builder.Configuration["Communication:SecondaryApiKey"];
    options.RegisterClientStateService<MyClientStateService>();
    options.RegisterClientRepository<MemoryClientRepository<ClientConnectionInfo>, ClientConnectionInfo>();
});

Client — provide the matching key:

{
  "Tharga": {
    "Communication": {
      "ServerAddress": "https://localhost:5001",
      "ApiKey": "your-secret-key"
    }
  }
}

Or via the options callback:

builder.AddThargaCommunicationClient(o =>
{
    o.ApiKey = builder.Configuration["Communication:ApiKey"];
});

API keys can also be configured via User Secrets or environment variables. To rotate keys without downtime, set both PrimaryApiKey and SecondaryApiKey on the server — either key is accepted.

Handler discovery

By default, message handlers are discovered by scanning assemblies that match the entry assembly name prefix. If your handlers are in an external package (e.g. a separate NuGet), they won't be found automatically. Use AdditionalAssemblies to include them:

builder.AddThargaCommunicationClient(o =>
{
    o.ServerAddress = "https://localhost:5001";
    o.AdditionalAssemblies = [typeof(MyExternalHandler).Assembly];
});

The same option is available on the server side:

builder.AddThargaCommunicationServer(options =>
{
    options.AdditionalAssemblies = [typeof(MyExternalHandler).Assembly];
    options.RegisterClientStateService<MyClientStateService>();
    options.RegisterClientRepository<MemoryClientRepository<ClientConnectionInfo>, ClientConnectionInfo>();
});

If a client receives a SendMessage for a type with no registered handler, it immediately returns an error response to the server instead of silently timing out.

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages