From 33d84a82574ac28a66e00c9e6465c96ec99031ef Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 22 Apr 2026 22:45:32 +0200 Subject: [PATCH] [LOADED_DOCS: .github\copilot-instructions.md, C:\Users\Fullepi\copilot-instructions.md] Refactor SignalR client DI and config, add test factory Refactored FruitBankSignalRClient construction to use DI and centralized configuration from appsettings.json across all platforms. Introduced TestSignalRClientFactory for consistent test setup. Added FruitBankHubConnectionExtensions for reusable SignalR connection and logging configuration. Updated Program.cs and MauiProgram.cs to register logger factories, log writers, and IHubConnectionBuilder via DI. Embedded appsettings.json in MAUI and updated .csproj references for build flexibility. No business logic changes; all updates are infrastructure and test setup. --- .github/copilot-instructions.md | 8 +++ .../FruitBankClientTests.cs | 6 +- .../JsonExtensionTests.cs | 5 +- .../OrderClientTests.cs | 6 +- .../SandboxEndpointSimpleTests.cs | 36 +++++------ .../TestSignalRClientFactory.cs | 53 +++++++++++++++++ FruitBankHybrid.Shared.Tests/ToonTests.cs | 6 +- .../FruitBankHubConnectionExtensions.cs | 35 +++++++++++ .../SignalRs/FruitBankSignalRClient.cs | 9 +-- FruitBankHybrid.Web.Client/Program.cs | 45 ++++++++++++-- .../wwwroot/appsettings.json | 20 +++++++ FruitBankHybrid.Web/Program.cs | 45 ++++++++++++-- FruitBankHybrid.Web/appsettings.json | 24 +++++++- FruitBankHybrid/FruitBankHybrid.csproj | 13 ++-- FruitBankHybrid/MauiProgram.cs | 59 ++++++++++++++++--- FruitBankHybrid/appsettings.json | 20 +++++++ 16 files changed, 318 insertions(+), 72 deletions(-) create mode 100644 FruitBankHybrid.Shared.Tests/TestSignalRClientFactory.cs create mode 100644 FruitBankHybrid.Shared/Services/SignalRs/FruitBankHubConnectionExtensions.cs create mode 100644 FruitBankHybrid/appsettings.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a186ec18..e84d4370 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -61,6 +61,14 @@ You are operating in a multi-repo, documentation-first architecture. You MUST ST > For nopCommerce plugin (server side) rules see: `../NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/README.md` (in Mango.Nop Plugins repo) > External repos in `own-dep-repos` are fully accessible — read their source code, docs, and `.github/copilot-instructions.md` freely when you need type definitions, base classes, or context. Do not limit yourself to the current workspace. +## Shared Agent Skills + +Skills defined in other repos that can be referenced from here: + +- **protocol-audit** — Cross-repo consistency audit for `.github/copilot-instructions.md` across all 5 repos. + Location: `AyCode.Core/.github/skills/protocol-audit/SKILL.md` + Activate from an AyCode.Core session, or read the SKILL.md directly and follow its steps. + ## Business Domain 1. **FruitBank** = fruit & vegetable wholesaler. The server side runs as a **nopCommerce plugin** — Customer, Order, Product, GenericAttribute are nopCommerce entities. 2. **Shipping** = INBOUND delivery (supplier → warehouse). **Order** = OUTBOUND delivery (warehouse → customer). Never confuse the two directions. diff --git a/FruitBankHybrid.Shared.Tests/FruitBankClientTests.cs b/FruitBankHybrid.Shared.Tests/FruitBankClientTests.cs index 6d97f3c3..fe542b21 100644 --- a/FruitBankHybrid.Shared.Tests/FruitBankClientTests.cs +++ b/FruitBankHybrid.Shared.Tests/FruitBankClientTests.cs @@ -31,11 +31,7 @@ namespace FruitBankHybrid.Shared.Tests { if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!"); - _signalRClient = new FruitBankSignalRClient(new List - { - //new ConsoleLogWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)), - new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)) - }); + _signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests)); } #region Partner diff --git a/FruitBankHybrid.Shared.Tests/JsonExtensionTests.cs b/FruitBankHybrid.Shared.Tests/JsonExtensionTests.cs index 0fc656c5..cd1ad43f 100644 --- a/FruitBankHybrid.Shared.Tests/JsonExtensionTests.cs +++ b/FruitBankHybrid.Shared.Tests/JsonExtensionTests.cs @@ -190,10 +190,7 @@ public sealed class JsonExtensionTests { if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!"); - _signalRClient = new FruitBankSignalRClient(new List - { - new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)) - }); + _signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests)); } [TestMethod] diff --git a/FruitBankHybrid.Shared.Tests/OrderClientTests.cs b/FruitBankHybrid.Shared.Tests/OrderClientTests.cs index 983646bb..5779d4cf 100644 --- a/FruitBankHybrid.Shared.Tests/OrderClientTests.cs +++ b/FruitBankHybrid.Shared.Tests/OrderClientTests.cs @@ -28,11 +28,7 @@ public sealed class OrderClientTests { if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!"); - _signalRClient = new FruitBankSignalRClient(new List - { - //new ConsoleLogWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)), - new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)) - }); + _signalRClient = TestSignalRClientFactory.Create(nameof(OrderClientTests)); } diff --git a/FruitBankHybrid.Shared.Tests/SandboxEndpointSimpleTests.cs b/FruitBankHybrid.Shared.Tests/SandboxEndpointSimpleTests.cs index cb3c2d0c..b6d05bdc 100644 --- a/FruitBankHybrid.Shared.Tests/SandboxEndpointSimpleTests.cs +++ b/FruitBankHybrid.Shared.Tests/SandboxEndpointSimpleTests.cs @@ -15,8 +15,8 @@ namespace FruitBankHybrid.Shared.Tests; /// /// Teszt a TestSignalREndpoint-hoz. -/// FONTOS: A SANDBOX-ot manuálisan kell elindítani a tesztek futtatása elõtt! -/// Indítás: dotnet run --project Mango.Sandbox.EndPoints --urls http://localhost:59579 +/// FONTOS: A SANDBOX-ot manu�lisan kell elind�tani a tesztek futtat�sa el�tt! +/// Ind�t�s: dotnet run --project Mango.Sandbox.EndPoints --urls http://localhost:59579 /// [TestClass] public class SandboxEndpointSimpleTests @@ -24,7 +24,7 @@ public class SandboxEndpointSimpleTests private static readonly string SandboxUrl = FruitBankConstClient.BaseUrl; //"http://localhost:59579"; private static readonly string HubUrl = $"{SandboxUrl}/fbHub"; - // Teszt SignalR Tags (TestSignalRTags-bõl) + // Teszt SignalR Tags (TestSignalRTags-b�l) private const int PingTag = SignalRTags.PingTag; private const int EchoTag = SignalRTags.EchoTag; private const int GetTestItemsTag = 9003; @@ -34,13 +34,9 @@ public class SandboxEndpointSimpleTests [TestInitialize] public void TestInit() { - if (!SandboxUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!"); + if (!SandboxUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTEL�NK!"); - _signalRClient = new FruitBankSignalRClient(new List - { - //new ConsoleLogWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)), - new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(SandboxEndpointSimpleTests)) - }); + _signalRClient = TestSignalRClientFactory.Create(nameof(SandboxEndpointSimpleTests)); } #region HTTP Endpoint Tests @@ -121,7 +117,7 @@ public class SandboxEndpointSimpleTests // using var jsonDoc = JsonDocument.Parse(response); // var root = jsonDoc.RootElement; - // // Ellenõrizzük, hogy van Message property + // // Ellen�rizz�k, hogy van Message property // Assert.IsTrue(root.TryGetProperty("Message", out var messageElement) || // root.TryGetProperty("message", out messageElement), // "Response should contain 'Message' property"); @@ -141,13 +137,13 @@ public class SandboxEndpointSimpleTests // using var jsonDoc = JsonDocument.Parse(response); // var root = jsonDoc.RootElement; - // // Ellenõrizzük az Id-t + // // Ellen�rizz�k az Id-t // Assert.IsTrue(root.TryGetProperty("Id", out var idElement) || // root.TryGetProperty("id", out idElement), // "Response should contain 'Id' property"); // Assert.AreEqual(42, idElement.GetInt32(), "Id should be 42"); - // // Ellenõrizzük a Name-et + // // Ellen�rizz�k a Name-et // Assert.IsTrue(root.TryGetProperty("Name", out var nameElement) || // root.TryGetProperty("name", out nameElement), // "Response should contain 'Name' property"); @@ -167,13 +163,13 @@ public class SandboxEndpointSimpleTests // using var jsonDoc = JsonDocument.Parse(response); // var root = jsonDoc.RootElement; - // // Ellenõrizzük, hogy tömb-e + // // Ellen�rizz�k, hogy t�mb-e // Assert.AreEqual(JsonValueKind.Array, root.ValueKind, "Response should be an array"); // Assert.IsTrue(root.GetArrayLength() > 0, "Array should have items"); // Console.WriteLine($"[GetTestItems] Received {root.GetArrayLength()} items"); - // // Ellenõrizzük az elsõ elemet + // // Ellen�rizz�k az els� elemet // var firstItem = root[0]; // Assert.IsTrue(firstItem.TryGetProperty("Id", out _) || firstItem.TryGetProperty("id", out _), // "Item should have 'Id' property"); @@ -187,8 +183,8 @@ public class SandboxEndpointSimpleTests //#region EREDETI BUSINESS ENDPOINT TESZTEK - KIKOMMENTEZVE //// =========================================== - //// === Az alábbi tesztek az eredeti 3 endpoint-ot tesztelik === - //// === Visszaállításhoz: töröld a kommenteket és regisztráld az endpoint-okat a Program.cs-ben === + //// === Az al�bbi tesztek az eredeti 3 endpoint-ot tesztelik === + //// === Vissza�ll�t�shoz: t�r�ld a kommenteket �s regisztr�ld az endpoint-okat a Program.cs-ben === //// =========================================== //// [TestMethod] @@ -260,13 +256,13 @@ public class SandboxEndpointSimpleTests // await connection.StartAsync(); // Assert.AreEqual(HubConnectionState.Connected, connection.State, $"Failed to connect to SignalR hub for {endpointName}"); - // // Készítsük el a request data-t - // // Ha nincs paraméter, null-t küldünk (nem üres byte tömböt!) + // // K�sz�ts�k el a request data-t + // // Ha nincs param�ter, null-t k�ld�nk (nem �res byte t�mb�t!) // byte[]? requestData = parameter != null // ? Encoding.UTF8.GetBytes(JsonSerializer.Serialize(parameter)) // : null; - // // A Hub metódus neve: OnReceiveMessage (3 paraméter: messageTag, messageBytes, requestId) + // // A Hub met�dus neve: OnReceiveMessage (3 param�ter: messageTag, messageBytes, requestId) // await connection.InvokeAsync("OnReceiveMessage", tag, requestData, (int?)null); // var completed = await Task.WhenAny(responseReceived.Task, Task.Delay(15000)); @@ -276,7 +272,7 @@ public class SandboxEndpointSimpleTests // Console.WriteLine($"[{endpointName}] Response tag: {receivedTag}"); // Console.WriteLine($"[{endpointName}] Response JSON: {receivedJson?.Substring(0, Math.Min(500, receivedJson?.Length ?? 0))}..."); - // // Ellenõrizzük, hogy valid JSON-e (ha van adat) + // // Ellen�rizz�k, hogy valid JSON-e (ha van adat) // if (!string.IsNullOrEmpty(receivedJson)) // { // try diff --git a/FruitBankHybrid.Shared.Tests/TestSignalRClientFactory.cs b/FruitBankHybrid.Shared.Tests/TestSignalRClientFactory.cs new file mode 100644 index 00000000..05b32ae1 --- /dev/null +++ b/FruitBankHybrid.Shared.Tests/TestSignalRClientFactory.cs @@ -0,0 +1,53 @@ +using AyCode.Core.Enums; +using AyCode.Core.Loggers; +using AyCode.Core.Serializers.Binaries; +using AyCode.Services.SignalRs; +using FruitBank.Common; +using FruitBank.Common.Loggers; +using FruitBankHybrid.Shared.Services.Loggers; +using FruitBankHybrid.Shared.Services.SignalRs; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.SignalR.Client; + +namespace FruitBankHybrid.Shared.Tests; + +/// +/// Test-only factory for . Builds a HubConnectionBuilder +/// with the same connection settings a production Program.cs would use, wires a logger factory +/// backed by a single SignaRClientLogItemWriter (test-unit AppType, Detail level), +/// and uses for the protocol. +/// +internal static class TestSignalRClientFactory +{ + public static FruitBankSignalRClient Create(string testCategoryName) + { + var logWriters = new List + { + new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, testCategoryName) + }; + + Func loggerFactory = + categoryName => new LoggerClient(categoryName, logWriters.ToArray()); + + var connectionOptions = new AcHubConnectionOptions + { + Url = $"{FruitBankConstClient.BaseUrl}/{FruitBankConstClient.DefaultHubName}", + TransportMaxBufferSize = 30_000_000, + ApplicationMaxBufferSize = 30_000_000, + CloseTimeout = TimeSpan.FromSeconds(10), + KeepAliveInterval = TimeSpan.FromSeconds(60), + ServerTimeout = TimeSpan.FromSeconds(180), + SkipNegotiation = true, + Transports = HttpTransportType.WebSockets, + UseAutomaticReconnect = true, + UseStatefulReconnect = true + }; + + var logger = loggerFactory(nameof(FruitBankSignalRClient)); + + var hubBuilder = new HubConnectionBuilder().AddFruitBankDefaults(logger, connectionOptions); + hubBuilder.AddAcBinaryProtocol(opts => opts.ProtocolMode = BinaryProtocolMode.AsyncSegment); + + return new FruitBankSignalRClient(hubBuilder, loggerFactory); + } +} diff --git a/FruitBankHybrid.Shared.Tests/ToonTests.cs b/FruitBankHybrid.Shared.Tests/ToonTests.cs index e7ed4092..cec68f19 100644 --- a/FruitBankHybrid.Shared.Tests/ToonTests.cs +++ b/FruitBankHybrid.Shared.Tests/ToonTests.cs @@ -101,11 +101,7 @@ public sealed class ToonTests { if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!"); - _signalRClient = new FruitBankSignalRClient(new List - { - //new ConsoleLogWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)), - new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)) - }); + _signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests)); } #if DEBUG diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankHubConnectionExtensions.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankHubConnectionExtensions.cs new file mode 100644 index 00000000..d5def0e6 --- /dev/null +++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankHubConnectionExtensions.cs @@ -0,0 +1,35 @@ +using AyCode.Core.Loggers; +using AyCode.Services.SignalRs; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Logging; + +namespace FruitBankHybrid.Shared.Services.SignalRs; + +/// +/// FruitBank-specific hub-connection setup. +/// +/// Connection-level configuration (URL, buffers, timeouts, reconnect) is delegated to the +/// framework's , driven by +/// from appsettings.json. The logger is supplied +/// by the caller (typically via a DI-registered Func<string, AcLoggerBase> factory +/// in Program.cs), keeping this extension free of LoggerClient construction. +/// +/// +public static class FruitBankHubConnectionExtensions +{ + /// + /// Applies with the given + /// , then bridges into + /// SignalR's internal ILogger pipeline. + /// + public static IHubConnectionBuilder AddFruitBankDefaults(this IHubConnectionBuilder builder, AcLoggerBase logger, AcHubConnectionOptions connectionOptions) + { + return builder + .AddAcConnection(connectionOptions) + .ConfigureLogging(logging => + { + logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Information); + logging.AddAcLogger(_ => logger); + }); + } +} diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs index 06625878..0ca7c351 100644 --- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs +++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs @@ -26,14 +26,9 @@ namespace FruitBankHybrid.Shared.Services.SignalRs { public class FruitBankSignalRClient : AcSignalRClientBase, IFruitBankDataControllerClient, ICustomOrderSignalREndpointClient, IStockSignalREndpointClient { - public FruitBankSignalRClient( /*IServiceProvider serviceProvider, */ IEnumerable logWriters) : base($"{FruitBankConstClient.BaseUrl}/{FruitBankConstClient.DefaultHubName}", new LoggerClient(nameof(FruitBankSignalRClient), logWriters.ToArray())) + public FruitBankSignalRClient(IHubConnectionBuilder hubBuilder, Func loggerFactory) + : base(hubBuilder, loggerFactory(nameof(FruitBankSignalRClient))) { - //var hubConnection = new HubConnectionBuilder() - // .WithUrl("fullHubName") - // .WithAutomaticReconnect() - // .WithStatefulReconnect() - // .WithKeepAliveInterval(TimeSpan.FromSeconds(60)) - // .WithServerTimeout(TimeSpan.FromSeconds(120)) EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog; ConstHelper.NameByValue(0); } diff --git a/FruitBankHybrid.Web.Client/Program.cs b/FruitBankHybrid.Web.Client/Program.cs index 7691ea0a..6474bcfc 100644 --- a/FruitBankHybrid.Web.Client/Program.cs +++ b/FruitBankHybrid.Web.Client/Program.cs @@ -1,4 +1,5 @@ using AyCode.Core.Loggers; +using AyCode.Core.Serializers.Binaries; using AyCode.Services.SignalRs; using FruitBank.Common; using FruitBank.Common.Loggers; @@ -6,11 +7,13 @@ using FruitBank.Common.Models; using FruitBank.Common.Services; using FruitBankHybrid.Shared.Databases; using FruitBankHybrid.Shared.Services; +using FruitBankHybrid.Shared.Services.Loggers; using FruitBankHybrid.Shared.Services.SignalRs; using FruitBankHybrid.Web.Client.Services; using FruitBankHybrid.Web.Client.Services.Loggers; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Options; var builder = WebAssemblyHostBuilder.CreateDefault(args); @@ -20,17 +23,47 @@ builder.Services.AddDevExpressBlazor(configure => configure.SizeMode = DevExpres builder.Services.AddSingleton(); builder.Services.AddSingleton(); -//#if DEBUG +#if DEBUG builder.Services.AddSingleton(); -//#endif +#endif +builder.Services.AddSingleton(); +builder.Services.AddSingleton(sp => new LoggedInModel(sp.GetRequiredService())); + +// Bind SignalR options from wwwroot/appsettings.json (loaded automatically by WebAssemblyHostBuilder) — +// single Configure call per options type, combining section Bind with runtime overrides. +builder.Services.Configure(opts => builder.Configuration.GetSection("AcHubConnection").Bind(opts)); + +builder.Services.Configure(opts => +{ + builder.Configuration.GetSection("AcBinaryHubProtocol").Bind(opts); + + // WASM safety net: AsyncSegment send-path is unsupported here — Validate() would throw. + // Downgrade if appsettings.json accidentally specifies it. + if (opts.ProtocolMode == BinaryProtocolMode.AsyncSegment) opts.ProtocolMode = BinaryProtocolMode.Segment; +}); + +// Logger factory — thin Logger, DI-singleton writers. Each call yields a fresh LoggerClient +// with the caller's categoryName. Mirrors Microsoft's ILoggerFactory / ILogger pattern. +builder.Services.AddSingleton>(sp => categoryName => new LoggerClient(categoryName, sp.GetServices().ToArray())); + +// HubConnectionBuilder — transient so each consumer gets a fresh builder to Build(). +// All connection and protocol configuration flows from appsettings.json via IOptions; +// AddFruitBankDefaults only bridges the provided logger into SignalR's internal pipeline. +builder.Services.AddTransient(sp => +{ + var loggerFactory = sp.GetRequiredService>(); + var connectionOpts = sp.GetRequiredService>().Value; + + var logger = loggerFactory(nameof(FruitBankSignalRClient)); + var hubBuilder = new HubConnectionBuilder().AddFruitBankDefaults(logger, connectionOpts); + + hubBuilder.AddAcBinaryProtocol(); // IOptions from DI + return hubBuilder; +}); -builder.Services.AddSingleton(sp => - new LoggedInModel(sp.GetRequiredService())); builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - #if DEBUG if (FruitBankConstClient.SignalRSerializerDiagnosticLog) { diff --git a/FruitBankHybrid.Web.Client/wwwroot/appsettings.json b/FruitBankHybrid.Web.Client/wwwroot/appsettings.json index 0c208ae9..70139eb4 100644 --- a/FruitBankHybrid.Web.Client/wwwroot/appsettings.json +++ b/FruitBankHybrid.Web.Client/wwwroot/appsettings.json @@ -4,5 +4,25 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + + "AcHubConnection": { + "Url": "https://localhost:59579/fbHub", + "TransportMaxBufferSize": 30000000, + "ApplicationMaxBufferSize": 30000000, + "CloseTimeout": "00:00:10", + "KeepAliveInterval": "00:01:00", + "ServerTimeout": "00:03:00", + "SkipNegotiation": true, + "Transports": "WebSockets", + "UseAutomaticReconnect": true, + "UseStatefulReconnect": true + }, + + "AcBinaryHubProtocol": { + "ProtocolMode": "Segment", + "BufferSize": 4096, + "WaitForFlush": true, + "FlushTimeout": "00:00:10" } } diff --git a/FruitBankHybrid.Web/Program.cs b/FruitBankHybrid.Web/Program.cs index 18f69d89..2e012a95 100644 --- a/FruitBankHybrid.Web/Program.cs +++ b/FruitBankHybrid.Web/Program.cs @@ -1,4 +1,6 @@ using AyCode.Core.Loggers; +using AyCode.Core.Serializers.Binaries; +using AyCode.Services.SignalRs; using FruitBank.Common; using FruitBank.Common.Models; using FruitBank.Common.Services; @@ -6,9 +8,12 @@ using FruitBank.Common.Server.Services.Loggers; using FruitBank.Common.Server.Services.SignalRs; using FruitBankHybrid.Shared.Databases; using FruitBankHybrid.Shared.Services; +using FruitBankHybrid.Shared.Services.Loggers; using FruitBankHybrid.Shared.Services.SignalRs; using FruitBankHybrid.Web.Components; using FruitBankHybrid.Web.Services; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Options; var builder = WebApplication.CreateBuilder(args); @@ -21,8 +26,40 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton(sp => - new LoggedInModel(sp.GetRequiredService())); +// Logger factory — thin Logger, DI-singleton writers. Each call yields a fresh LoggerClient +// with the caller's categoryName. Mirrors Microsoft's ILoggerFactory / ILogger pattern. +builder.Services.AddSingleton>(sp => categoryName => new LoggerClient(categoryName, sp.GetServices().ToArray())); + +builder.Services.AddSingleton(sp => new LoggedInModel(sp.GetRequiredService())); + +// Bind SignalR options from appsettings.json — single Configure call per options type. +// The lambda runs the appsettings Bind first, then any runtime overrides (e.g. the WASM safety net). +builder.Services.Configure(opts => builder.Configuration.GetSection("AcHubConnection").Bind(opts)); + +builder.Services.Configure(opts => +{ + builder.Configuration.GetSection("AcBinaryHubProtocol").Bind(opts); + + // Platform safety net: on WebAssembly the AsyncSegment send-path is unsupported + // (Validate() would throw). No-op on this server host, but matches the contract. + if (OperatingSystem.IsBrowser() && opts.ProtocolMode == BinaryProtocolMode.AsyncSegment) + opts.ProtocolMode = BinaryProtocolMode.Segment; +}); + +// HubConnectionBuilder — transient so each consumer gets a fresh builder to Build(). +// All connection and protocol configuration flows from appsettings.json via IOptions; +// AddFruitBankDefaults only bridges the provided logger into SignalR's internal pipeline. +builder.Services.AddTransient(sp => +{ + var loggerFactory = sp.GetRequiredService>(); + var connectionOpts = sp.GetRequiredService>().Value; + + var logger = loggerFactory(nameof(FruitBankSignalRClient)); + var hubBuilder = new HubConnectionBuilder().AddFruitBankDefaults(logger, connectionOpts); + + hubBuilder.AddAcBinaryProtocol(); // IOptions from DI + return hubBuilder; +}); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -57,8 +94,6 @@ app.MapStaticAssets(); app.MapRazorComponents() //.AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() - .AddAdditionalAssemblies( - typeof(FruitBankHybrid.Shared._Imports).Assembly, - typeof(FruitBankHybrid.Web.Client._Imports).Assembly); + .AddAdditionalAssemblies(typeof(FruitBankHybrid.Shared._Imports).Assembly, typeof(FruitBankHybrid.Web.Client._Imports).Assembly); app.Run(); diff --git a/FruitBankHybrid.Web/appsettings.json b/FruitBankHybrid.Web/appsettings.json index 48f2d688..885eabed 100644 --- a/FruitBankHybrid.Web/appsettings.json +++ b/FruitBankHybrid.Web/appsettings.json @@ -1,12 +1,32 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Debug", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", + "AcHubConnection": { + "Url": "https://localhost:59579/fbHub", + "TransportMaxBufferSize": 30000000, + "ApplicationMaxBufferSize": 30000000, + "CloseTimeout": "00:00:10", + "KeepAliveInterval": "00:01:00", + "ServerTimeout": "00:03:00", + "SkipNegotiation": true, + "Transports": "WebSockets", + "UseAutomaticReconnect": true, + "UseStatefulReconnect": true + }, + + "AcBinaryHubProtocol": { + "ProtocolMode": "AsyncSegment", + "BufferSize": 4096, + "WaitForFlush": true, + "FlushTimeout": "00:00:10" + }, + "AyCode": { "ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e", "Urls": { @@ -19,7 +39,7 @@ "LogWriters": [ { "LogLevel": "Detail", - "LogWriterType": "FruitBank.Common.Server.Services.Loggers.ConsoleLogWriter, FruitBank.Common.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "LogWriterType": "FruitBank.Common.Server.Services.Loggers.ConsoleLogWriter, FruitBank.Common.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" } ] } diff --git a/FruitBankHybrid/FruitBankHybrid.csproj b/FruitBankHybrid/FruitBankHybrid.csproj index 44d2dee7..a6722af3 100644 --- a/FruitBankHybrid/FruitBankHybrid.csproj +++ b/FruitBankHybrid/FruitBankHybrid.csproj @@ -78,6 +78,11 @@ True + + + + + @@ -131,11 +136,11 @@ ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.dll - diff --git a/FruitBankHybrid/MauiProgram.cs b/FruitBankHybrid/MauiProgram.cs index 7925edfc..5d44adba 100644 --- a/FruitBankHybrid/MauiProgram.cs +++ b/FruitBankHybrid/MauiProgram.cs @@ -1,4 +1,5 @@ using AyCode.Core.Loggers; +using AyCode.Services.SignalRs; using FruitBank.Common.Loggers; using FruitBank.Common.Models; using FruitBank.Common.Services; @@ -6,9 +7,15 @@ using FruitBankHybrid.Services; using FruitBankHybrid.Services.Loggers; using FruitBankHybrid.Shared.Databases; using FruitBankHybrid.Shared.Services; +using FruitBankHybrid.Shared.Services.Loggers; using FruitBankHybrid.Shared.Services.SignalRs; //using DevExpress.Maui; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Reflection; namespace FruitBankHybrid { @@ -27,22 +34,56 @@ namespace FruitBankHybrid fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); + // Load embedded appsettings.json — MAUI has no automatic config file discovery, + // so the JSON is shipped as an EmbeddedResource (see FruitBankHybrid.csproj). + using (var appsettingsStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("FruitBankHybrid.appsettings.json")) + { + if (appsettingsStream is not null) + { + var jsonConfig = new ConfigurationBuilder().AddJsonStream(appsettingsStream).Build(); + builder.Configuration.AddConfiguration(jsonConfig); + } + } + +#if DEBUG + builder.Services.AddSingleton(); +#endif + builder.Services.AddSingleton(); + + // Logger factory — the Logger itself is a thin wrapper; writers (the real sinks) + // are DI singletons. Each call produces a fresh LoggerClient with the caller's categoryName. + // Mirrors the Microsoft ILoggerFactory / ILogger pattern. + builder.Services.AddSingleton>(sp => categoryName => new LoggerClient(categoryName, sp.GetServices().ToArray())); + + // Bind SignalR options from configuration. + // Precedence: code default → appsettings.json (this line) → any later Configure action. + builder.Services.Configure(builder.Configuration.GetSection("AcHubConnection")); + builder.Services.Configure(builder.Configuration.GetSection("AcBinaryHubProtocol")); + // Add device-specific services used by the FruitBankHybrid.Shared project builder.Services.AddSingleton(); builder.Services.AddSingleton(); - #if DEBUG - builder.Services.AddSingleton(); - #endif - - builder.Services.AddSingleton(sp => - new LoggedInModel(sp.GetRequiredService())); + builder.Services.AddSingleton(sp => new LoggedInModel(sp.GetRequiredService())); + + // SignalR HubConnectionBuilder — transient so each consumer gets a fresh builder to Build(). + // All connection and protocol configuration flows from appsettings.json via IOptions; + // AddFruitBankDefaults only bridges the provided logger into SignalR's internal pipeline. + builder.Services.AddTransient(sp => + { + var loggerFactory = sp.GetRequiredService>(); + var connectionOpts = sp.GetRequiredService>().Value; + + var logger = loggerFactory(nameof(FruitBankSignalRClient)); + var hubBuilder = new HubConnectionBuilder().AddFruitBankDefaults(logger, connectionOpts); + + hubBuilder.AddAcBinaryProtocol(); // IOptions from DI + return hubBuilder; + }); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - builder.Services.AddMauiBlazorWebView(); builder.Services.AddDevExpressBlazor(configure => configure.SizeMode = DevExpress.Blazor.SizeMode.Medium); diff --git a/FruitBankHybrid/appsettings.json b/FruitBankHybrid/appsettings.json new file mode 100644 index 00000000..ac2e5b36 --- /dev/null +++ b/FruitBankHybrid/appsettings.json @@ -0,0 +1,20 @@ +{ + "AcHubConnection": { + "Url": "https://localhost:59579/fbHub", + "TransportMaxBufferSize": 30000000, + "ApplicationMaxBufferSize": 30000000, + "CloseTimeout": "00:00:10", + "KeepAliveInterval": "00:01:00", + "ServerTimeout": "00:03:00", + "SkipNegotiation": true, + "Transports": "WebSockets", + "UseAutomaticReconnect": true, + "UseStatefulReconnect": true + }, + "AcBinaryHubProtocol": { + "ProtocolMode": "AsyncSegment", + "BufferSize": 4096, + "WaitForFlush": true, + "FlushTimeout": "00:00:10" + } +}