From 711c3c8ec03d5f719d52f78f19a007707bf64198 Mon Sep 17 00:00:00 2001 From: Loretta Date: Thu, 23 Apr 2026 16:11:22 +0200 Subject: [PATCH] Framework-first doctrine, DI logger factory, config refactor Introduced framework-first design rules and updated documentation to clarify framework vs. consumer boundaries. Added AcLoggerOptions and DI-based logger factory extensions to AyCode.Core, enabling per-category logger instantiation from appsettings.json. Standardized SignalR connection setup with AddAcDefaults, replacing project-specific code. Enhanced protocol configuration for DI scope isolation. Refactored appsettings.json structure and added MSBuild targets for config propagation. Removed obsolete code and updated comments to match new patterns. --- .../TestSignalRClientFactory.cs | 4 +- .../FruitBankHubConnectionExtensions.cs | 35 -------- .../SignalRs/FruitBankSignalRClient.cs | 2 +- FruitBankHybrid.Shared/appsettings.json | 44 ++++++++++ .../FruitBankHybrid.Web.Client.csproj | 21 +++++ FruitBankHybrid.Web.Client/Program.cs | 18 ++-- .../wwwroot/appsettings.Development.json | 8 -- .../wwwroot/appsettings.json | 66 ++++++++------ .../FruitBankHybrid.Web.csproj | 13 +++ FruitBankHybrid.Web/Program.cs | 24 ++--- .../appsettings.Development.json | 8 -- FruitBankHybrid.Web/appsettings.json | 88 +++++++++---------- FruitBankHybrid/FruitBankHybrid.csproj | 5 +- FruitBankHybrid/MauiProgram.cs | 19 ++-- FruitBankHybrid/appsettings.json | 20 ----- 15 files changed, 201 insertions(+), 174 deletions(-) delete mode 100644 FruitBankHybrid.Shared/Services/SignalRs/FruitBankHubConnectionExtensions.cs create mode 100644 FruitBankHybrid.Shared/appsettings.json delete mode 100644 FruitBankHybrid.Web.Client/wwwroot/appsettings.Development.json delete mode 100644 FruitBankHybrid.Web/appsettings.Development.json delete mode 100644 FruitBankHybrid/appsettings.json diff --git a/FruitBankHybrid.Shared.Tests/TestSignalRClientFactory.cs b/FruitBankHybrid.Shared.Tests/TestSignalRClientFactory.cs index 05b32ae1..5fbb451f 100644 --- a/FruitBankHybrid.Shared.Tests/TestSignalRClientFactory.cs +++ b/FruitBankHybrid.Shared.Tests/TestSignalRClientFactory.cs @@ -26,7 +26,7 @@ internal static class TestSignalRClientFactory new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, testCategoryName) }; - Func loggerFactory = + Func loggerFactory = categoryName => new LoggerClient(categoryName, logWriters.ToArray()); var connectionOptions = new AcHubConnectionOptions @@ -45,7 +45,7 @@ internal static class TestSignalRClientFactory var logger = loggerFactory(nameof(FruitBankSignalRClient)); - var hubBuilder = new HubConnectionBuilder().AddFruitBankDefaults(logger, connectionOptions); + var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOptions); hubBuilder.AddAcBinaryProtocol(opts => opts.ProtocolMode = BinaryProtocolMode.AsyncSegment); return new FruitBankSignalRClient(hubBuilder, loggerFactory); diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankHubConnectionExtensions.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankHubConnectionExtensions.cs deleted file mode 100644 index d5def0e6..00000000 --- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankHubConnectionExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -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 0ca7c351..be680dcb 100644 --- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs +++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs @@ -26,7 +26,7 @@ namespace FruitBankHybrid.Shared.Services.SignalRs { public class FruitBankSignalRClient : AcSignalRClientBase, IFruitBankDataControllerClient, ICustomOrderSignalREndpointClient, IStockSignalREndpointClient { - public FruitBankSignalRClient(IHubConnectionBuilder hubBuilder, Func loggerFactory) + public FruitBankSignalRClient(IHubConnectionBuilder hubBuilder, Func loggerFactory) : base(hubBuilder, loggerFactory(nameof(FruitBankSignalRClient))) { EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog; diff --git a/FruitBankHybrid.Shared/appsettings.json b/FruitBankHybrid.Shared/appsettings.json new file mode 100644 index 00000000..b59b8e20 --- /dev/null +++ b/FruitBankHybrid.Shared/appsettings.json @@ -0,0 +1,44 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning" + } + }, + "AyCode": { + "ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e", + "Urls": { + "BaseUrl": "https://localhost:7144", + "ApiBaseUrl": "https://localhost:7144" + }, + "Logger": { + "AppType": "Server", + "LogLevel": "Detail", + "LogWriters": [ + { + "LogLevel": "Detail", + "LogWriterType": "FruitBank.Common.Server.Services.Loggers.ConsoleLogWriter, FruitBank.Common.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" + } + ] + } + + }, + "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": false, + "FlushTimeout": "00:00:10" + } +} diff --git a/FruitBankHybrid.Web.Client/FruitBankHybrid.Web.Client.csproj b/FruitBankHybrid.Web.Client/FruitBankHybrid.Web.Client.csproj index e1b552ea..fc083e74 100644 --- a/FruitBankHybrid.Web.Client/FruitBankHybrid.Web.Client.csproj +++ b/FruitBankHybrid.Web.Client/FruitBankHybrid.Web.Client.csproj @@ -26,6 +26,27 @@ + + + + + ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll diff --git a/FruitBankHybrid.Web.Client/Program.cs b/FruitBankHybrid.Web.Client/Program.cs index 6474bcfc..83cfc671 100644 --- a/FruitBankHybrid.Web.Client/Program.cs +++ b/FruitBankHybrid.Web.Client/Program.cs @@ -42,22 +42,26 @@ builder.Services.Configure(opts => 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())); +// Logger options + framework factory. LoggerClient instances are created per caller category, +// with AppType+LogLevel from appsettings and writers resolved from DI via IAcLogWriterClientBase. +builder.Services.Configure(builder.Configuration.GetSection("AyCode:Logger")); +builder.Services.AddAcLoggerFactory(); // 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. +// AddAcDefaults (framework) applies AcHubConnectionOptions and bridges the provided logger into SignalR's internal pipeline. +// NOTE: AcBinaryHubProtocolOptions is resolved from the OUTER service provider and passed +// explicitly — HubConnectionBuilder's inner DI cannot see outer services.Configure() registrations. builder.Services.AddTransient(sp => { - var loggerFactory = sp.GetRequiredService>(); + var loggerFactory = sp.GetRequiredService>(); var connectionOpts = sp.GetRequiredService>().Value; + var protocolOpts = sp.GetRequiredService>().Value; var logger = loggerFactory(nameof(FruitBankSignalRClient)); - var hubBuilder = new HubConnectionBuilder().AddFruitBankDefaults(logger, connectionOpts); + var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOpts); - hubBuilder.AddAcBinaryProtocol(); // IOptions from DI + hubBuilder.AddAcBinaryProtocol(protocolOpts); return hubBuilder; }); diff --git a/FruitBankHybrid.Web.Client/wwwroot/appsettings.Development.json b/FruitBankHybrid.Web.Client/wwwroot/appsettings.Development.json deleted file mode 100644 index 0c208ae9..00000000 --- a/FruitBankHybrid.Web.Client/wwwroot/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/FruitBankHybrid.Web.Client/wwwroot/appsettings.json b/FruitBankHybrid.Web.Client/wwwroot/appsettings.json index 70139eb4..b59b8e20 100644 --- a/FruitBankHybrid.Web.Client/wwwroot/appsettings.json +++ b/FruitBankHybrid.Web.Client/wwwroot/appsettings.json @@ -1,28 +1,44 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning" + } + }, + "AyCode": { + "ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e", + "Urls": { + "BaseUrl": "https://localhost:7144", + "ApiBaseUrl": "https://localhost:7144" + }, + "Logger": { + "AppType": "Server", + "LogLevel": "Detail", + "LogWriters": [ + { + "LogLevel": "Detail", + "LogWriterType": "FruitBank.Common.Server.Services.Loggers.ConsoleLogWriter, FruitBank.Common.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" + } + ] + } + + }, + "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": false, + "FlushTimeout": "00:00:10" } - }, - - "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/FruitBankHybrid.Web.csproj b/FruitBankHybrid.Web/FruitBankHybrid.Web.csproj index a811d8a4..081f717b 100644 --- a/FruitBankHybrid.Web/FruitBankHybrid.Web.csproj +++ b/FruitBankHybrid.Web/FruitBankHybrid.Web.csproj @@ -85,6 +85,19 @@ + + + + + \ No newline at end of file diff --git a/FruitBankHybrid.Web/Program.cs b/FruitBankHybrid.Web/Program.cs index 2e012a95..56965a95 100644 --- a/FruitBankHybrid.Web/Program.cs +++ b/FruitBankHybrid.Web/Program.cs @@ -5,7 +5,6 @@ using FruitBank.Common; using FruitBank.Common.Models; using FruitBank.Common.Services; 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; @@ -21,14 +20,15 @@ builder.Services.AddRazorComponents().AddInteractiveWebAssemblyComponents(); builder.Services.AddDevExpressBlazor(configure => configure.SizeMode = DevExpress.Blazor.SizeMode.Medium); builder.Services.AddMvc(); -builder.Services.AddSignalR(options => options.MaximumReceiveMessageSize = 256 * 1024); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); -// 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())); + +// Logger options + framework factory. LoggerClient instances are created per caller category, +// with AppType+LogLevel from appsettings and writers resolved from DI via IAcLogWriterBase. +builder.Services.Configure(builder.Configuration.GetSection("AyCode:Logger")); +builder.Services.AddAcLoggerFactory(); builder.Services.AddSingleton(sp => new LoggedInModel(sp.GetRequiredService())); @@ -48,16 +48,19 @@ builder.Services.Configure(opts => // 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. +// AddAcDefaults (framework) applies AcHubConnectionOptions and bridges the provided logger into SignalR's internal pipeline. +// NOTE: AcBinaryHubProtocolOptions is resolved from the OUTER service provider and passed +// explicitly — HubConnectionBuilder's inner DI cannot see outer services.Configure() registrations. builder.Services.AddTransient(sp => { - var loggerFactory = sp.GetRequiredService>(); + var loggerFactory = sp.GetRequiredService>(); var connectionOpts = sp.GetRequiredService>().Value; + var protocolOpts = sp.GetRequiredService>().Value; var logger = loggerFactory(nameof(FruitBankSignalRClient)); - var hubBuilder = new HubConnectionBuilder().AddFruitBankDefaults(logger, connectionOpts); + var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOpts); - hubBuilder.AddAcBinaryProtocol(); // IOptions from DI + hubBuilder.AddAcBinaryProtocol(protocolOpts); return hubBuilder; }); @@ -82,9 +85,6 @@ else app.UseHsts(); } -app.MapHub($"/{FruitBankConstClient.LoggerHubName}"); -app.MapHub($"/{FruitBankConstClient.DefaultHubName}"); - app.UseHttpsRedirection(); app.UseStaticFiles(); diff --git a/FruitBankHybrid.Web/appsettings.Development.json b/FruitBankHybrid.Web/appsettings.Development.json deleted file mode 100644 index 0c208ae9..00000000 --- a/FruitBankHybrid.Web/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/FruitBankHybrid.Web/appsettings.json b/FruitBankHybrid.Web/appsettings.json index 885eabed..b59b8e20 100644 --- a/FruitBankHybrid.Web/appsettings.json +++ b/FruitBankHybrid.Web/appsettings.json @@ -1,48 +1,44 @@ { - "Logging": { - "LogLevel": { - "Default": "Debug", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning" + } + }, + "AyCode": { + "ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e", + "Urls": { + "BaseUrl": "https://localhost:7144", + "ApiBaseUrl": "https://localhost:7144" + }, + "Logger": { + "AppType": "Server", + "LogLevel": "Detail", + "LogWriters": [ + { + "LogLevel": "Detail", + "LogWriterType": "FruitBank.Common.Server.Services.Loggers.ConsoleLogWriter, FruitBank.Common.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" + } + ] + } - "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": { - "BaseUrl": "https://localhost:7144", - "ApiBaseUrl": "https://localhost:7144" - }, - "Logger": { - "AppType": "Server", - "LogLevel": "Detail", - "LogWriters": [ - { - "LogLevel": "Detail", - "LogWriterType": "FruitBank.Common.Server.Services.Loggers.ConsoleLogWriter, FruitBank.Common.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" - } - ] - } - - } -} \ No newline at end of file + }, + "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": false, + "FlushTimeout": "00:00:10" + } +} diff --git a/FruitBankHybrid/FruitBankHybrid.csproj b/FruitBankHybrid/FruitBankHybrid.csproj index a6722af3..56537148 100644 --- a/FruitBankHybrid/FruitBankHybrid.csproj +++ b/FruitBankHybrid/FruitBankHybrid.csproj @@ -79,8 +79,9 @@ - - + + diff --git a/FruitBankHybrid/MauiProgram.cs b/FruitBankHybrid/MauiProgram.cs index 5d44adba..f310f25c 100644 --- a/FruitBankHybrid/MauiProgram.cs +++ b/FruitBankHybrid/MauiProgram.cs @@ -50,10 +50,10 @@ namespace FruitBankHybrid #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())); + // Logger options + framework factory. LoggerClient instances are created per caller category, + // with AppType+LogLevel from appsettings and writers resolved from DI via IAcLogWriterClientBase. + builder.Services.Configure(builder.Configuration.GetSection("AyCode:Logger")); + builder.Services.AddAcLoggerFactory(); // Bind SignalR options from configuration. // Precedence: code default → appsettings.json (this line) → any later Configure action. @@ -68,16 +68,19 @@ namespace FruitBankHybrid // 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. + // AddAcDefaults (framework) applies AcHubConnectionOptions and bridges the provided logger into SignalR's internal pipeline. + // NOTE: AcBinaryHubProtocolOptions is resolved from the OUTER service provider and passed + // explicitly — HubConnectionBuilder's inner DI cannot see outer services.Configure() registrations. builder.Services.AddTransient(sp => { - var loggerFactory = sp.GetRequiredService>(); + var loggerFactory = sp.GetRequiredService>(); var connectionOpts = sp.GetRequiredService>().Value; + var protocolOpts = sp.GetRequiredService>().Value; var logger = loggerFactory(nameof(FruitBankSignalRClient)); - var hubBuilder = new HubConnectionBuilder().AddFruitBankDefaults(logger, connectionOpts); + var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOpts); - hubBuilder.AddAcBinaryProtocol(); // IOptions from DI + hubBuilder.AddAcBinaryProtocol(protocolOpts); return hubBuilder; }); diff --git a/FruitBankHybrid/appsettings.json b/FruitBankHybrid/appsettings.json deleted file mode 100644 index ac2e5b36..00000000 --- a/FruitBankHybrid/appsettings.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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" - } -}