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" + } +}