[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:
parent
c6e1fa8efc
commit
8b8abb7cbc
File diff suppressed because one or more lines are too long
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<AcBinaryHubProtocolOptions>(...)</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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<AcHubConnectionOptions>(...)</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<T>(section)</c>
|
||||
/// → <c>services.Configure<T>(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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T></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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue