[LOADED_DOCS: .github\copilot-instructions.md]

Refactor SignalR protocol registration; add DI options

- Added AcSignalRServerProtocolExtensions and AcSignalRProtocolExtensions for idiomatic AddAcBinaryProtocol registration (server/client), using a shared BuildProtocol factory for DI/IOptions/inline config.
- Introduced AcHubConnectionOptions and AcSignalRConnectionExtensions for configuration-driven client setup.
- Refactored AcSignalRClientBase to require a preconfigured IHubConnectionBuilder, moving all connection/protocol config out of the base class.
- Removed legacy protocol constructors; all protocol instantiation is now options-based.
- Enforced WASM + AsyncSegment guard in AcBinaryHubProtocolOptions.Validate.
- Updated SIGNALR_BINARY_PROTOCOL.md and GLOSSARY.md for new DI/config patterns.
- Minor: updated settings.local.json with new DLL/plugin inspection commands.
This commit is contained in:
Loretta 2026-04-22 22:44:37 +02:00
parent c6e1fa8efc
commit 8b8abb7cbc
12 changed files with 293 additions and 100 deletions

File diff suppressed because one or more lines are too long

View File

@ -29,7 +29,11 @@ internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
private readonly BinaryProtocolMode _mode;
public TestMultiSegmentProtocol(BinaryProtocolMode mode = BinaryProtocolMode.Bytes)
: base(AcBinarySerializerOptions.Default, mode)
: base(new AcBinaryHubProtocolOptions
{
SerializerOptions = AcBinarySerializerOptions.Default,
ProtocolMode = mode
})
{
_mode = mode;
Options.BufferWriterChunkSize = SegmentSize;

View File

@ -0,0 +1,35 @@
using AyCode.Services.SignalRs;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.DependencyInjection;
namespace AyCode.Services.Server.SignalRs;
/// <summary>
/// Server-side registration extension for the <see cref="AyCodeBinaryHubProtocol"/> (<c>"acbinary"</c>).
/// Mirrors the ASP.NET Core idiomatic <c>AddJsonProtocol(...)</c> / <c>AddMessagePackProtocol(...)</c>.
/// <para>
/// Kept separate from the client-side extension (in <c>AyCode.Services</c>) so that pure client
/// projects (MAUI, WASM) do not pull in the server SignalR assembly
/// (<c>Microsoft.AspNetCore.SignalR.Core</c>) through a transitive reference.
/// </para>
/// </summary>
public static class AcSignalRServerProtocolExtensions
{
/// <summary>
/// Registers <see cref="AyCodeBinaryHubProtocol"/> (name: <c>"acbinary"</c>) as a SignalR hub
/// protocol on the server. Options can be configured via either:
/// <list type="bullet">
/// <item><c>services.Configure&lt;AcBinaryHubProtocolOptions&gt;(...)</c> — DI-level defaults</item>
/// <item>The optional <paramref name="configure"/> callback — overrides DI values inline</item>
/// </list>
/// </summary>
public static ISignalRServerBuilder AddAcBinaryProtocol(
this ISignalRServerBuilder builder,
Action<AcBinaryHubProtocolOptions>? configure = null)
{
builder.Services.AddSingleton<IHubProtocol>(sp =>
AcSignalRProtocolExtensions.BuildProtocol(sp, configure));
return builder;
}
}

View File

@ -127,42 +127,16 @@ public class AcBinaryHubProtocol : IHubProtocol
/// </summary>
public AcBinaryHubProtocol() : this(new AcBinaryHubProtocolOptions()) { }
/// <summary>
/// Legacy constructor — wraps the arguments into <see cref="AcBinaryHubProtocolOptions"/>
/// and delegates to the options-based constructor. Kept for backward compatibility;
/// will be removed in a future version in favor of the options-based API.
/// </summary>
public AcBinaryHubProtocol(AcBinarySerializerOptions options, BinaryProtocolMode protocolMode = BinaryProtocolMode.Bytes, ILogger? logger = null)
: this(new AcBinaryHubProtocolOptions
{
SerializerOptions = options,
ProtocolMode = protocolMode,
Logger = logger,
BufferSize = 4096
// FlushTimeout, WaitForFlush, Name — use options defaults (30s, true, "acbinary")
})
{ }
/// <summary>
/// Primary constructor. All configuration flows through <see cref="AcBinaryHubProtocolOptions"/>.
/// Invalid configuration (incl. WebAssembly + AsyncSegment send-path) throws from
/// <see cref="AcBinaryHubProtocolOptions.Validate"/>.
/// </summary>
public AcBinaryHubProtocol(AcBinaryHubProtocolOptions options)
{
if (options is null) throw new ArgumentNullException(nameof(options));
options.Validate();
// Send-side guard: AsyncSegment uses AsyncPipeWriterOutput whose sync-over-async flush
// would block the browser's single UI thread. The receive side converts chunked wire
// to a synchronous deserialize on WASM automatically (see TryParseChunkData).
//
// TEMP: commented out to test AsyncSegment on both Windows app and WASM without rebuild.
// Small WASM payloads work; larger ones may deadlock on sync-over-async FlushAsync.
// Restore once BinaryProtocolMode is runtime-configurable in Program.cs.
//if (IsBrowser && options.ProtocolMode == BinaryProtocolMode.AsyncSegment)
// throw new PlatformNotSupportedException(
// "BinaryProtocolMode.AsyncSegment is not supported on WebAssembly. " +
// "Use BinaryProtocolMode.Bytes or BinaryProtocolMode.Segment instead.");
_options = options.SerializerOptions;
_options.BufferWriterChunkSize = options.BufferSize;
_protocolMode = options.ProtocolMode;

View File

@ -76,13 +76,14 @@ public sealed class AcBinaryHubProtocolOptions
/// </summary>
public void Validate()
{
// NOTE: WASM + AsyncSegment send-path guard is currently commented out in the protocol
// constructor for testing. Once BinaryProtocolMode becomes runtime-configurable in
// Program.cs, this validation will be re-enabled here as the primary guard.
//if (OperatingSystem.IsBrowser() && ProtocolMode == BinaryProtocolMode.AsyncSegment)
// throw new PlatformNotSupportedException(
// "BinaryProtocolMode.AsyncSegment is not supported on WebAssembly. " +
// "Use BinaryProtocolMode.Bytes or BinaryProtocolMode.Segment instead.");
// WASM + AsyncSegment send-path guard — the AsyncPipeWriterOutput sync-over-async flush
// would block the browser's single UI thread. The receive side converts chunked wire to
// synchronous deser automatically, so WASM clients can still *receive* AsyncSegment data
// from a non-WASM server — they just cannot *send* via AsyncSegment themselves.
if (OperatingSystem.IsBrowser() && ProtocolMode == BinaryProtocolMode.AsyncSegment)
throw new PlatformNotSupportedException(
"BinaryProtocolMode.AsyncSegment is not supported on WebAssembly. " +
"Use BinaryProtocolMode.Bytes or BinaryProtocolMode.Segment instead.");
if (BufferSize < 256 || BufferSize > AsyncPipeWriterOutput.MaxChunkSize)
throw new ArgumentOutOfRangeException(nameof(BufferSize), BufferSize,

View File

@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Http.Connections;
namespace AyCode.Services.SignalRs;
/// <summary>
/// Options for a client-side SignalR <c>HubConnection</c>, designed to be bindable from
/// configuration (<c>appsettings.json</c>) via <c>services.Configure&lt;AcHubConnectionOptions&gt;(...)</c>.
/// Applied to an <c>IHubConnectionBuilder</c> via
/// <see cref="AcSignalRConnectionExtensions.AddAcConnection(Microsoft.AspNetCore.SignalR.Client.IHubConnectionBuilder, AcHubConnectionOptions)"/>.
/// <para>
/// Most properties are nullable — when <c>null</c> the underlying SignalR / Microsoft default
/// is kept (the framework is non-opinionated). Resilience flags default to sensible modern values
/// (auto-reconnect on). Override in code or configuration.
/// </para>
/// <para>Precedence (low → high): property initializer → <c>services.Configure&lt;T&gt;(section)</c>
/// → <c>services.Configure&lt;T&gt;(action)</c>.</para>
/// </summary>
public sealed class AcHubConnectionOptions
{
/// <summary>Target hub URL — absolute, including the hub path (e.g. <c>"https://host/fbHub"</c>).</summary>
public string Url { get; set; } = "";
// --- HttpConnectionOptions (applied via WithUrl) ---
/// <summary>Transport(s) to negotiate. <c>null</c> → Microsoft default (<c>WebSockets | LongPolling | ServerSentEvents</c>).</summary>
public HttpTransportType? Transports { get; set; }
/// <summary>Max outbound transport buffer (bytes). <c>null</c> → Microsoft default (~64 KB).</summary>
public int? TransportMaxBufferSize { get; set; }
/// <summary>Max application-level send buffer (bytes). <c>null</c> → Microsoft default (~64 KB).</summary>
public int? ApplicationMaxBufferSize { get; set; }
/// <summary>WebSocket close handshake timeout. <c>null</c> → Microsoft default (5 s).</summary>
public TimeSpan? CloseTimeout { get; set; }
/// <summary>Skip negotiation (WebSockets-only setups). <c>null</c> → Microsoft default (<c>false</c>).</summary>
public bool? SkipNegotiation { get; set; }
// --- Connection-level ---
/// <summary>Client-side keep-alive ping interval. <c>null</c> → SignalR default (15 s).</summary>
public TimeSpan? KeepAliveInterval { get; set; }
/// <summary>Server timeout (no-data threshold to declare the connection dead). <c>null</c> → SignalR default (30 s).</summary>
public TimeSpan? ServerTimeout { get; set; }
// --- Resilience (framework defaults — opinionated toward modern apps) ---
/// <summary>Enable <c>WithAutomaticReconnect()</c>. Default: <c>true</c>.</summary>
public bool UseAutomaticReconnect { get; set; } = true;
/// <summary>Enable <c>WithStatefulReconnect()</c> (SignalR 8+). Default: <c>false</c> (opt-in — requires server support).</summary>
public bool UseStatefulReconnect { get; set; } = false;
}

View File

@ -17,7 +17,6 @@ namespace AyCode.Services.SignalRs
public abstract class AcSignalRClientBase : IAcSignalRHubClient
{
private readonly ConcurrentDictionary<int, SignalRRequestModel> _responseByRequestId = new();
private readonly bool _useAcBinaryProtocol;
protected readonly HubConnection? HubConnection;
protected readonly AcLoggerBase Logger;
@ -36,64 +35,24 @@ namespace AyCode.Services.SignalRs
public int TransportSendTimeout = 60000;
private const string TagsName = "SignalRTags";
protected AcSignalRClientBase(string fullHubName, AcLoggerBase logger, bool useAcBinaryProtocol = true)
/// <summary>
/// Primary constructor. The <paramref name="hubBuilder"/> is expected to be fully configured
/// (URL, transport, reconnect, keep-alive, protocol) — typically via a transient DI registration
/// in the consuming project's <c>Program.cs</c>. This class only calls <c>Build()</c> and wires
/// the dispatch callback; no connection parameters are hard-coded here.
/// </summary>
protected AcSignalRClientBase(IHubConnectionBuilder hubBuilder, AcLoggerBase logger)
{
_useAcBinaryProtocol = useAcBinaryProtocol;
Logger = logger;
Logger.Detail(fullHubName);
var hubBuilder = new HubConnectionBuilder()
.WithUrl(fullHubName, HttpTransportType.WebSockets,
options =>
{
options.TransportMaxBufferSize = 30_000_000;
options.ApplicationMaxBufferSize = 30_000_000;
options.CloseTimeout = TimeSpan.FromSeconds(10);
options.SkipNegotiation = true;
})
.ConfigureLogging(logging =>
{
// alap minimális MS log level
logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Information);
// regisztráljuk az AcLoggerProvider-t úgy, hogy visszaadja a meglévő Logger példányt
logging.AddAcLogger(_ => Logger);
// ha inkább csak AcLogger legyen:
// logging.ClearProviders();
// logging.AddProvider(new AcLoggerProvider<AcLoggerBase>(category => Logger));
})
.WithAutomaticReconnect()
.WithStatefulReconnect()
.WithKeepAliveInterval(TimeSpan.FromSeconds(60))
.WithServerTimeout(TimeSpan.FromSeconds(180));
if (useAcBinaryProtocol)
{
hubBuilder.Services.AddSingleton<IHubProtocol>(sp =>
{
var binaryOptions = AcBinarySerializerOptions.Default;
binaryOptions.BufferWriterChunkSize = 4096;
// AcSignalRClientBase — a 84. sor környékén:
var signalLogger = sp.GetRequiredService<ILoggerFactory>().CreateLogger<AyCodeBinaryHubProtocol>();
return new AyCodeBinaryHubProtocol(binaryOptions, BinaryProtocolMode.AsyncSegment, signalLogger);
// és törölhető: AcBinaryHubProtocol.DiagnosticLogger = msg => Logger.Debug(msg);
});
//Vagy ha az options-t is DI-ből:
//hubBuilder.Services.AddSingleton<IHubProtocol>(sp => new AyCodeBinaryHubProtocol(sp.GetRequiredService<AcBinarySerializerOptions>()));
AcBinaryHubProtocol.DiagnosticLogger = msg => Logger.Debug(msg);
AcBinaryDeserializer.DiagnosticLogger = msg => Logger.Debug(msg);
}
HubConnection = hubBuilder.Build();
HubConnection.Closed += HubConnection_Closed;
_ = HubConnection.On<int, int?, SignalParams, object>(nameof(IAcSignalRHubClient.OnReceiveMessage), OnReceiveMessage);
}
/// <summary>
/// Connection-less constructor — used by derived classes that manage their own connection lifecycle
/// or run in test / offline scenarios where <see cref="HubConnection"/> stays <c>null</c>.
/// </summary>
protected AcSignalRClientBase(AcLoggerBase logger)
{
Logger = logger;

View File

@ -0,0 +1,49 @@
using Microsoft.AspNetCore.SignalR.Client;
namespace AyCode.Services.SignalRs;
/// <summary>
/// Extension methods for applying <see cref="AcHubConnectionOptions"/> to
/// a client <see cref="IHubConnectionBuilder"/>.
/// </summary>
public static class AcSignalRConnectionExtensions
{
/// <summary>
/// Applies <see cref="AcHubConnectionOptions"/> to the builder:
/// <c>WithUrl</c> (with HttpConnectionOptions), keep-alive, server timeout,
/// <c>WithAutomaticReconnect</c>, <c>WithStatefulReconnect</c>.
/// <para>
/// Nullable properties are skipped when <c>null</c> — the underlying SignalR default is kept.
/// Combine freely with any other builder extensions before or after this call; the ordering
/// follows the fluent chain (last write wins for a given setting).
/// </para>
/// </summary>
public static IHubConnectionBuilder AddAcConnection(
this IHubConnectionBuilder builder,
AcHubConnectionOptions options)
{
if (options is null) throw new ArgumentNullException(nameof(options));
if (string.IsNullOrWhiteSpace(options.Url))
throw new ArgumentException("AcHubConnectionOptions.Url must be set.", nameof(options));
builder.WithUrl(options.Url, http =>
{
if (options.Transports.HasValue) http.Transports = options.Transports.Value;
if (options.TransportMaxBufferSize.HasValue) http.TransportMaxBufferSize = options.TransportMaxBufferSize.Value;
if (options.ApplicationMaxBufferSize.HasValue) http.ApplicationMaxBufferSize = options.ApplicationMaxBufferSize.Value;
if (options.CloseTimeout.HasValue) http.CloseTimeout = options.CloseTimeout.Value;
if (options.SkipNegotiation.HasValue) http.SkipNegotiation = options.SkipNegotiation.Value;
});
if (options.KeepAliveInterval.HasValue)
builder.WithKeepAliveInterval(options.KeepAliveInterval.Value);
if (options.ServerTimeout.HasValue)
builder.WithServerTimeout(options.ServerTimeout.Value);
if (options.UseAutomaticReconnect)
builder.WithAutomaticReconnect();
if (options.UseStatefulReconnect)
builder.WithStatefulReconnect();
return builder;
}
}

View File

@ -0,0 +1,50 @@
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace AyCode.Services.SignalRs;
/// <summary>
/// Client-side registration extension for the <see cref="AyCodeBinaryHubProtocol"/> (<c>"acbinary"</c>).
/// Mirrors the ASP.NET Core idiomatic pattern of <c>AddJsonProtocol(...)</c> /
/// <c>AddMessagePackProtocol(...)</c>.
/// <para>
/// For the server-side equivalent see <c>AcSignalRServerProtocolExtensions</c> in
/// <c>AyCode.Services.Server</c> — kept separate to avoid dragging the server SignalR assembly
/// (<c>Microsoft.AspNetCore.SignalR.Core</c>) into pure client projects (MAUI, WASM).
/// </para>
/// </summary>
public static class AcSignalRProtocolExtensions
{
/// <summary>
/// Registers <see cref="AyCodeBinaryHubProtocol"/> as the protocol for a client <see cref="HubConnection"/>.
/// Call on the <see cref="IHubConnectionBuilder"/> during client setup.
/// </summary>
public static IHubConnectionBuilder AddAcBinaryProtocol(this IHubConnectionBuilder builder, Action<AcBinaryHubProtocolOptions>? configure = null)
{
builder.Services.AddSingleton<IHubProtocol>(sp => BuildProtocol(sp, configure));
return builder;
}
/// <summary>
/// Shared factory used by both client (this file) and server
/// (<c>AcSignalRServerProtocolExtensions</c> in AyCode.Services.Server).
/// Resolves options from DI (<c>IOptions&lt;T&gt;</c>), clones them, applies the inline
/// <paramref name="configure"/> override, validates, and constructs the protocol.
/// </summary>
public static IHubProtocol BuildProtocol(IServiceProvider sp, Action<AcBinaryHubProtocolOptions>? configure)
{
var diOptions = sp.GetService<IOptions<AcBinaryHubProtocolOptions>>()?.Value;
var options = diOptions?.Clone() ?? new AcBinaryHubProtocolOptions();
options.Logger ??= sp.GetService<ILogger<AcBinaryHubProtocol>>();
configure?.Invoke(options);
options.Validate();
return new AyCodeBinaryHubProtocol(options);
}
}

View File

@ -29,13 +29,6 @@ public class AyCodeBinaryHubProtocol : AcBinaryHubProtocol
/// </summary>
public AyCodeBinaryHubProtocol() : base() { }
/// <summary>
/// Legacy constructor — delegates to the base legacy constructor, which wraps into
/// <see cref="AcBinaryHubProtocolOptions"/>. Kept for backward compatibility.
/// </summary>
public AyCodeBinaryHubProtocol(AcBinarySerializerOptions options, BinaryProtocolMode protocolMode = BinaryProtocolMode.Bytes, ILogger? logger = null)
: base(options, protocolMode, logger) { }
/// <summary>
/// Primary constructor — accepts a fully-configured <see cref="AcBinaryHubProtocolOptions"/>.
/// </summary>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long