PANiXiDA.Core.Presentation.SignalR 1.0.4

dotnet add package PANiXiDA.Core.Presentation.SignalR --version 1.0.4
                    
NuGet\Install-Package PANiXiDA.Core.Presentation.SignalR -Version 1.0.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PANiXiDA.Core.Presentation.SignalR" Version="1.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PANiXiDA.Core.Presentation.SignalR" Version="1.0.4" />
                    
Directory.Packages.props
<PackageReference Include="PANiXiDA.Core.Presentation.SignalR" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PANiXiDA.Core.Presentation.SignalR --version 1.0.4
                    
#r "nuget: PANiXiDA.Core.Presentation.SignalR, 1.0.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PANiXiDA.Core.Presentation.SignalR@1.0.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PANiXiDA.Core.Presentation.SignalR&version=1.0.4
                    
Install as a Cake Addin
#tool nuget:?package=PANiXiDA.Core.Presentation.SignalR&version=1.0.4
                    
Install as a Cake Tool

PANiXiDA.Core.Presentation.SignalR

Reusable ASP.NET Core SignalR registration, optional Redis backplane configuration, and browser access token forwarding for .NET 10.

Status

CI NuGet NuGet downloads Target Framework License

Overview

Register the package once in the host's composition root. Applications own hubs, client contracts, authorization policies, group names, business subscriptions, and message delivery semantics. There is no dependency on the HTTP presentation, Application or Mediator packages. Result mapping uses PANiXiDA.Core.ResultPattern.

Features

  • Standard SignalR services and builder for additional configuration.
  • ResultPattern helpers using standard HubException errors and strict JSON payload contracts.
  • JSON and MessagePack protocols with source-generated payload contracts for Native AOT.
  • Optional Redis backplane using a named connection string and a literal channel prefix.
  • Startup validation of Redis configuration without printing credentials.
  • Query token forwarding restricted to explicitly registered hub transport paths.
  • Existing Authorization headers and host authentication remain authoritative.

Quick Start

Requirements

  • .NET 10 SDK with Roslyn 5.9 or later, and ASP.NET Core runtime.
  • Redis when the backplane is enabled.
  • Docker for integration tests.

Installation

dotnet add package PANiXiDA.Core.Presentation.SignalR

Minimal setup

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using PANiXiDA.Core.Presentation.SignalR.DependencyInjection;
using PANiXiDA.Core.Presentation.SignalR.Hubs;

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

var app = builder.Build();
app.UseSignalR(typeof(EventsHub).Assembly);
app.Run();

public sealed class EventsHub : Hub, IHubEndpoint
{
    public static HubEndpointConventionBuilder Map(HubMapBuilder hubs) =>
        hubs.MapHub<EventsHub>("/hubs/events");
}

Without SignalR:Redis, SignalR uses its standard in-memory lifetime manager. Merely configuring ConnectionStrings:Redis does not enable the backplane.

Usage

Authentication and browser clients

Configure authentication and authorization in the host, then call UseSignalR on the built WebApplication before authentication. Pass one assembly reference for each presentation module. Generated registrations map its concrete IHubEndpoint implementations and supply their exact transport paths for query tokens. The extension returns the same WebApplication. Protect the hub in its static Map method:

public sealed class AuthorizedEventsHub : Hub, IHubEndpoint
{
    public static HubEndpointConventionBuilder Map(HubMapBuilder hubs) =>
        hubs.MapHub<AuthorizedEventsHub>("/hubs/events").RequireAuthorization();
}

The adapter only copies the token into a Bearer header; normal validation still checks the token and decides access.

app.UseSignalR(typeof(AuthorizedEventsHub).Assembly);
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

With PANiXiDA.Core.Presentation.Http, call UseSignalR before app.UseHttp(), which owns routing, authentication and authorization. Hubs are mapped inside UseSignalR; no separate MapHub call is needed afterwards. Authentication schemes and authorization policies remain host-owned.

const connection = new signalR.HubConnectionBuilder()
  .withUrl('/hubs/events', { accessTokenFactory: () => accessToken })
  .withAutomaticReconnect()
  .build();
await connection.start();

Use the standard SignalR client negotiation and transport selection. Browser WebSocket and Server-Sent Events requests can provide access_token in the query. Negotiation uses normal header authentication.

Path matching is exact, case insensitive, and ignores trailing slashes. Child routes such as /hubs/events/negotiate, sibling paths, and ordinary API endpoints cannot use query tokens. Supply literal paths without query strings, route parameters or PathBase; /gateway/hubs/events behind a /gateway PathBase uses /hubs/events. Existing Authorization headers, including empty ones, take precedence. Missing, blank, repeated and control-character query tokens are ignored. Invalid or duplicate hub paths and calls that map no hubs fail immediately during pipeline configuration. Duplicate detection ignores case and trailing slashes. Trailing slashes are removed from mapped routes.

Assembly discovery

Reference this package directly in every project declaring mapped hubs, with analyzer assets enabled. The included incremental source generator emits direct calls to each hub's static IHubEndpoint.Map contract and registers them through a module initializer. Hub constructors are not invoked during discovery; SignalR still creates hub instances through the host's DI container when needed.

UseSignalR(params Assembly[] assemblies) preserves assembly argument order and maps hubs in ordinal type-name order within each assembly. Repeated assembly arguments are ignored. Abstract and unmarked types are skipped. Accessible internal and nested hubs are supported. Open generic, private, protected and file-local hub types are rejected with PANSIGNALRSG001; mapping contracts on non-hub types produce PANSIGNALRSG002.

An assembly without generated registration fails at startup with instructions to enable the package's analyzers and rebuild. Assemblies with an empty generated registration may accompany modules containing hubs; at least one hub must be mapped by the call. There is no reflection discovery or activation fallback.

Standard SignalR customization

The registration method returns ISignalRServerBuilder:

builder.Services.AddSignalR(builder.Configuration)
  .AddHubOptions<EventsHub>(options =>
  {
      options.MaximumReceiveMessageSize = 32 * 1024;
  });

Use Hub<TClient>, IHubContext<THub, TClient>, standard group operations and standard mapping options. Static Map implementations return standard convention builders for authorization and metadata, and accept standard connection options. For explicit registration, the callback overload remains available and supports multiple MapHub<THub> calls:

app.UseSignalR(hubs =>
    hubs.MapHub<EventsHub>("/hubs/events",
        options => options.CloseOnAuthenticationExpiration = true)
        .RequireAuthorization());

Generated and explicit registrations use generic hub types and preserve the members required by SignalR trimming analysis; it does not scan assemblies at runtime. Application-specific publishers and subscriptions remain in the application.

Result mapping

Call the helpers explicitly in hub methods:

using Microsoft.AspNetCore.SignalR;
using PANiXiDA.Core.Presentation.SignalR.Helpers;
using PANiXiDA.Core.ResultPattern;

public sealed class ResultsHub : Hub
{
    public void Complete() => Result.Success().ThrowIfFailure();
    public int GetCount() => Result.Success(0).GetValueOrThrow();
}

Success returns an ordinary value/DTO or no value. Failure maps only the first error to HubException: expected errors use its message; unexpected/unknown use An unexpected error occurred. Additional errors and metadata are not sent. Use ResultPattern Map first when converting an application value to a response.

EnableDetailedErrors defaults to true only in Development, based on IHostEnvironment; without an environment it is false. A later Configure<HubOptions> can override it. Expected HubException messages reach clients in every environment.

JSON contracts

JSON uses NumberHandling.Strict, RespectRequiredConstructorParameters = true, and RespectNullableAnnotations = true, independently of HTTP and MessagePack. Missing constructor arguments without defaults (including nullable ones) and explicit nulls for non-nullable members are rejected. Invalid non-nullable output also fails serialization. Collection-element and generic nullability are not checked.

Customize JSON through AddJsonProtocol; existing resolvers and converters are preserved. Native AOT requires a JsonSerializerContext covering all payload types, as in the combined example below.

MessagePack contracts

Both protocols are enabled; the client selects one at handshake, without automatic fallback. MessagePack uses the AOT-compatible Nerdbank.MessagePack.SignalR and is tested with Microsoft's client. Defaults use CLR member names, enum names, GUID strings (D), invariant decimal strings, and preserved DateTimeOffset offsets. Custom contracts and serializer settings must match the client.

The default registration supplies common scalar shapes. For DTOs, enums, collections, stream items, and notifications, pass a generated provider containing all top-level payload types. It replaces the built-in provider; member shapes are generated transitively. Example configuring both protocols for Native AOT:

using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using PANiXiDA.Core.Presentation.SignalR.DependencyInjection;
using PolyType;

builder.Services.AddSignalR(builder.Configuration,
        HubMessagePackTypes.GeneratedTypeShapeProvider)
    .AddJsonProtocol(options =>
        options.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, HubJsonContext.Default));

public sealed record CountResponse(int Count);

[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
    GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(CountResponse))]
internal partial class HubJsonContext : JsonSerializerContext;

[GenerateShapeFor<int>]
[GenerateShapeFor<CountResponse>]
internal partial class HubMessagePackTypes;

Unknown MessagePack contracts fail without reflection fallback. Redis publishers need shapes for every notification DTO even with JSON-only receivers, because the backplane serializes all enabled protocols. The registration overload also accepts an optional Nerdbank.MessagePack.MessagePackSerializer for host customization.

Logging

AddSignalR(configuration) installs a global IHubFilter for client-invoked hub methods and the OnConnectedAsync / OnDisconnectedAsync callbacks. Logs written inside these operations inherit snapshot scopes with rpc.system, rpc.service, rpc.method, signalr.connection.id, and enduser.id. The user ID is SignalR's UserIdentifier, including a host-provided IUserIdProvider; it may be null.

Each operation writes one Information event, SignalR operation finished, with signalr.operation.duration_ms and signalr.operation.outcome (succeeded, failed, or cancelled). Cancellation is classified as cancelled only when the connection cancellation token is signalled. A disconnect with a transport exception is reported as failed. Exceptions propagate unchanged, and their details remain owned by the standard SignalR logger to avoid duplicate error logs. Normal logging filters apply to PANiXiDA.Core.Presentation.SignalR.Logging.HubLoggingFilter.

The filter does not log arguments, return values, tokens, or notification payloads. Outgoing notifications through IHubContext are not intercepted. For streaming methods the duration measures the method invocation, not subsequent stream enumeration. The filter does not create activities; OpenTelemetry can collect SignalR's built-in activities independently. HTTP request and proxy logs still need their own query token redaction.

Configuration

Enable Redis with the SignalR:Redis section:

{
  "ConnectionStrings": {
    "Redis": "localhost:6379"
  },
  "SignalR": {
    "Redis": {
      "ConnectionStringName": "Redis",
      "ChannelPrefix": "my-application:development"
    }
  }
}

ConnectionStringName defaults to Redis. When the section exists, a nonblank connection string with a Redis endpoint and a nonblank ChannelPrefix are required. Invalid settings fail during application startup. Validation checks configuration syntax, not server availability. Settings require an application restart; configure Redis transport details such as TLS, credentials and timeouts in the connection string. Supply production credentials through the host's secret store, for example ConnectionStrings__Redis.

All replicas of one application/environment must use the same channel prefix. Different applications and environments must use different prefixes to isolate messages on a shared Redis server. All hubs registered in one host share the configured backplane.

Hosting and delivery

  • Run Redis close to the API replicas and allow the necessary Pub/Sub commands.
  • Configure load balancer session affinity for standard negotiated connections.
  • Configure host CORS policies for browser clients on other origins.
  • Use HTTPS and redact access_token from request and proxy logs.
  • Redis backplane does not persist or replay messages during an outage.
  • Reconnects and initial snapshots remain application responsibilities.
  • An established connection does not automatically revalidate token revocation. Use the standard CloseOnAuthenticationExpiration hub mapping option when expiry should close a connection; immediate revocation needs host-specific handling.

See Microsoft's Redis backplane guidance and SignalR authentication guidance.

Native AOT

The library enables IsAotCompatible and EnableConfigurationBindingGenerator. AOT/trimming analyzers run during library builds, and Redis options binding uses generated code. These settings do not enable Native AOT publishing in consuming applications; the host controls PublishAot and must validate its own dependencies and hub implementations.

For Native AOT hosts, use source-generated contracts for each enabled protocol: register a JsonSerializerContext in JsonHubProtocolOptions.PayloadSerializerOptions.TypeInfoResolverChain for JSON and pass a generated type shape provider for MessagePack as shown above. Use ordinary Hub and IHubContext<THub>: strongly typed Hub<TClient> and IHubContext<THub, TClient> require runtime proxy generation and are not supported by Native AOT. They remain available for normal JIT deployments.

See Microsoft's SignalR Native AOT guidance and configuration source generator documentation.

Project Structure

src/
  PANiXiDA.Core.Presentation.SignalR.Generators/
  PANiXiDA.Core.Presentation.SignalR/
    Configurations/
    DependencyInjection/
    Hubs/
tests/
  PANiXiDA.Core.Presentation.SignalR.UnitTests/
  PANiXiDA.Core.Presentation.SignalR.IntegrationTests/

Development

dotnet restore
dotnet format
dotnet build --configuration Release
dotnet test --configuration Release
dotnet pack --configuration Release

Hosting tests start Kestrel on 127.0.0.1 with an OS-assigned port and use real HTTP, WebSocket and Long Polling connections. Authentication uses a test handler. Integration tests start an isolated Redis container with Testcontainers and clean it up afterwards. They exercise delivery between independent application hosts. To run only the tests that do not require Docker:

dotnet test --project tests/PANiXiDA.Core.Presentation.SignalR.UnitTests/PANiXiDA.Core.Presentation.SignalR.UnitTests.csproj --configuration Release

Continuous integration

Every pull request and push to main runs formatting, tests, and mandatory SonarQube analysis. Publishing starts only after formatting, tests and the SonarQube Quality Gate succeed. Versioning uses Nerdbank.GitVersioning and tracks package source, shared build settings, README and icon.

Package Contents

The NuGet package includes the net10.0 library, its netstandard2.0 source generator under analyzers/dotnet/cs, XML documentation, README, license metadata, icon and repository metadata. Redis integration, PANiXiDA.Core.ResultPattern, and Nerdbank.MessagePack.SignalR are runtime package dependencies. PolyType supplies the MessagePack type shape generator; Microsoft's SignalR Client, MessagePack protocol, and Testcontainers are test-only. A symbols package is produced by the repository's packaging settings.

License

Licensed under Apache-2.0. See LICENSE.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.4 0 11/9/2026
1.0.3 0 11/9/2026
1.0.2 0 11/9/2026