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.
This commit is contained in:
parent
33d84a8257
commit
711c3c8ec0
|
|
@ -26,7 +26,7 @@ internal static class TestSignalRClientFactory
|
||||||
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, testCategoryName)
|
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, testCategoryName)
|
||||||
};
|
};
|
||||||
|
|
||||||
Func<string, AcLoggerBase> loggerFactory =
|
Func<string, LoggerClient> loggerFactory =
|
||||||
categoryName => new LoggerClient(categoryName, logWriters.ToArray());
|
categoryName => new LoggerClient(categoryName, logWriters.ToArray());
|
||||||
|
|
||||||
var connectionOptions = new AcHubConnectionOptions
|
var connectionOptions = new AcHubConnectionOptions
|
||||||
|
|
@ -45,7 +45,7 @@ internal static class TestSignalRClientFactory
|
||||||
|
|
||||||
var logger = loggerFactory(nameof(FruitBankSignalRClient));
|
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);
|
hubBuilder.AddAcBinaryProtocol(opts => opts.ProtocolMode = BinaryProtocolMode.AsyncSegment);
|
||||||
|
|
||||||
return new FruitBankSignalRClient(hubBuilder, loggerFactory);
|
return new FruitBankSignalRClient(hubBuilder, loggerFactory);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// FruitBank-specific hub-connection setup.
|
|
||||||
/// <para>
|
|
||||||
/// Connection-level configuration (URL, buffers, timeouts, reconnect) is delegated to the
|
|
||||||
/// framework's <see cref="AcSignalRConnectionExtensions.AddAcConnection"/>, driven by
|
|
||||||
/// <see cref="AcHubConnectionOptions"/> from <c>appsettings.json</c>. The logger is supplied
|
|
||||||
/// by the caller (typically via a DI-registered <c>Func<string, AcLoggerBase></c> factory
|
|
||||||
/// in <c>Program.cs</c>), keeping this extension free of <c>LoggerClient</c> construction.
|
|
||||||
/// </para>
|
|
||||||
/// </summary>
|
|
||||||
public static class FruitBankHubConnectionExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Applies <see cref="AcSignalRConnectionExtensions.AddAcConnection"/> with the given
|
|
||||||
/// <paramref name="connectionOptions"/>, then bridges <paramref name="logger"/> into
|
|
||||||
/// SignalR's internal <c>ILogger</c> pipeline.
|
|
||||||
/// </summary>
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -26,7 +26,7 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
|
||||||
{
|
{
|
||||||
public class FruitBankSignalRClient : AcSignalRClientBase, IFruitBankDataControllerClient, ICustomOrderSignalREndpointClient, IStockSignalREndpointClient
|
public class FruitBankSignalRClient : AcSignalRClientBase, IFruitBankDataControllerClient, ICustomOrderSignalREndpointClient, IStockSignalREndpointClient
|
||||||
{
|
{
|
||||||
public FruitBankSignalRClient(IHubConnectionBuilder hubBuilder, Func<string, AcLoggerBase> loggerFactory)
|
public FruitBankSignalRClient(IHubConnectionBuilder hubBuilder, Func<string, LoggerClient> loggerFactory)
|
||||||
: base(hubBuilder, loggerFactory(nameof(FruitBankSignalRClient)))
|
: base(hubBuilder, loggerFactory(nameof(FruitBankSignalRClient)))
|
||||||
{
|
{
|
||||||
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
|
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,27 @@
|
||||||
<ProjectReference Include="..\FruitBankHybrid.Shared\FruitBankHybrid.Shared.csproj" />
|
<ProjectReference Include="..\FruitBankHybrid.Shared\FruitBankHybrid.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Shared appsettings.json synced into wwwroot at build time.
|
||||||
|
Approach: pre-build Copy Target (not <Content Include Link=...>), because:
|
||||||
|
- <Content Include="..\..." Link="wwwroot\..."/> triggers StaticWebAssets.Normalize() "Illegal characters"
|
||||||
|
- pre-normalizing via [System.IO.Path]::GetFullPath(...) still fails (the absolute path also trips the validator)
|
||||||
|
- a pre-build Copy creates a physical wwwroot/appsettings.json which the StaticWebAssets auto-discovery
|
||||||
|
picks up naturally, same as any other wwwroot file
|
||||||
|
BeforeTargets lists multiple early targets to ensure the Copy runs before static-asset discovery,
|
||||||
|
regardless of which one triggers first in a given SDK version.
|
||||||
|
NOTE: a clean build (delete obj/) is required the first time, because the static-asset manifest
|
||||||
|
is cached in obj/ and stale entries persist across incremental builds.
|
||||||
|
The physical wwwroot/appsettings.json is a build artifact — commit or gitignore per team policy;
|
||||||
|
edits should always be made in FruitBankHybrid.Shared/appsettings.json (the canonical source). -->
|
||||||
|
<Target Name="CopySharedAppSettings"
|
||||||
|
BeforeTargets="CollectPackageReferences;AssignTargetPaths;ResolveStaticWebAssetsInputs;BeforeBuild"
|
||||||
|
Inputs="..\FruitBankHybrid.Shared\appsettings.json"
|
||||||
|
Outputs="wwwroot\appsettings.json">
|
||||||
|
<Copy SourceFiles="..\FruitBankHybrid.Shared\appsettings.json"
|
||||||
|
DestinationFiles="wwwroot\appsettings.json"
|
||||||
|
SkipUnchangedFiles="true" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="AyCode.Core">
|
<Reference Include="AyCode.Core">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>
|
||||||
|
|
|
||||||
|
|
@ -42,22 +42,26 @@ builder.Services.Configure<AcBinaryHubProtocolOptions>(opts =>
|
||||||
if (opts.ProtocolMode == BinaryProtocolMode.AsyncSegment) opts.ProtocolMode = BinaryProtocolMode.Segment;
|
if (opts.ProtocolMode == BinaryProtocolMode.AsyncSegment) opts.ProtocolMode = BinaryProtocolMode.Segment;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Logger factory — thin Logger, DI-singleton writers. Each call yields a fresh LoggerClient
|
// Logger options + framework factory. LoggerClient instances are created per caller category,
|
||||||
// with the caller's categoryName. Mirrors Microsoft's ILoggerFactory / ILogger<T> pattern.
|
// with AppType+LogLevel from appsettings and writers resolved from DI via IAcLogWriterClientBase.
|
||||||
builder.Services.AddSingleton<Func<string, AcLoggerBase>>(sp => categoryName => new LoggerClient(categoryName, sp.GetServices<IAcLogWriterClientBase>().ToArray()));
|
builder.Services.Configure<AcLoggerOptions>(builder.Configuration.GetSection("AyCode:Logger"));
|
||||||
|
builder.Services.AddAcLoggerFactory<LoggerClient, IAcLogWriterClientBase>();
|
||||||
|
|
||||||
// HubConnectionBuilder — transient so each consumer gets a fresh builder to Build().
|
// HubConnectionBuilder — transient so each consumer gets a fresh builder to Build().
|
||||||
// All connection and protocol configuration flows from appsettings.json via IOptions<T>;
|
// All connection and protocol configuration flows from appsettings.json via IOptions<T>;
|
||||||
// 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<T>() registrations.
|
||||||
builder.Services.AddTransient<IHubConnectionBuilder>(sp =>
|
builder.Services.AddTransient<IHubConnectionBuilder>(sp =>
|
||||||
{
|
{
|
||||||
var loggerFactory = sp.GetRequiredService<Func<string, AcLoggerBase>>();
|
var loggerFactory = sp.GetRequiredService<Func<string, LoggerClient>>();
|
||||||
var connectionOpts = sp.GetRequiredService<IOptions<AcHubConnectionOptions>>().Value;
|
var connectionOpts = sp.GetRequiredService<IOptions<AcHubConnectionOptions>>().Value;
|
||||||
|
var protocolOpts = sp.GetRequiredService<IOptions<AcBinaryHubProtocolOptions>>().Value;
|
||||||
|
|
||||||
var logger = loggerFactory(nameof(FruitBankSignalRClient));
|
var logger = loggerFactory(nameof(FruitBankSignalRClient));
|
||||||
var hubBuilder = new HubConnectionBuilder().AddFruitBankDefaults(logger, connectionOpts);
|
var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOpts);
|
||||||
|
|
||||||
hubBuilder.AddAcBinaryProtocol(); // IOptions<AcBinaryHubProtocolOptions> from DI
|
hubBuilder.AddAcBinaryProtocol(protocolOpts);
|
||||||
return hubBuilder;
|
return hubBuilder;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +1,44 @@
|
||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Debug",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,19 @@
|
||||||
<Folder Include="Services\SignalRs\" />
|
<Folder Include="Services\SignalRs\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Shared appsettings.json copied into project root at build time.
|
||||||
|
Why a Copy target instead of <Content Include="..\..." Link="appsettings.json" CopyToOutputDirectory="PreserveNewest"/>:
|
||||||
|
the Link approach only copies the file to bin/Debug output, but ASP.NET Core's WebApplicationBuilder
|
||||||
|
in development reads appsettings.json from the ContentRoot (= project directory), not from the output folder.
|
||||||
|
Materializing the file physically in the project root makes it discoverable by the default configuration
|
||||||
|
loader in every run mode (F5 / dotnet run / published), and the SDK auto-include for appsettings*.json
|
||||||
|
takes care of copy-to-output from there. -->
|
||||||
|
<Target Name="CopySharedAppSettings" BeforeTargets="BeforeBuild" Inputs="..\FruitBankHybrid.Shared\appsettings.json" Outputs="appsettings.json">
|
||||||
|
<Copy SourceFiles="..\FruitBankHybrid.Shared\appsettings.json"
|
||||||
|
DestinationFiles="appsettings.json"
|
||||||
|
SkipUnchangedFiles="true" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -5,7 +5,6 @@ using FruitBank.Common;
|
||||||
using FruitBank.Common.Models;
|
using FruitBank.Common.Models;
|
||||||
using FruitBank.Common.Services;
|
using FruitBank.Common.Services;
|
||||||
using FruitBank.Common.Server.Services.Loggers;
|
using FruitBank.Common.Server.Services.Loggers;
|
||||||
using FruitBank.Common.Server.Services.SignalRs;
|
|
||||||
using FruitBankHybrid.Shared.Databases;
|
using FruitBankHybrid.Shared.Databases;
|
||||||
using FruitBankHybrid.Shared.Services;
|
using FruitBankHybrid.Shared.Services;
|
||||||
using FruitBankHybrid.Shared.Services.Loggers;
|
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.AddDevExpressBlazor(configure => configure.SizeMode = DevExpress.Blazor.SizeMode.Medium);
|
||||||
builder.Services.AddMvc();
|
builder.Services.AddMvc();
|
||||||
|
|
||||||
builder.Services.AddSignalR(options => options.MaximumReceiveMessageSize = 256 * 1024);
|
|
||||||
builder.Services.AddSingleton<IFormFactor, FormFactor>();
|
builder.Services.AddSingleton<IFormFactor, FormFactor>();
|
||||||
builder.Services.AddSingleton<ISecureCredentialService, ServerSecureCredentialService>();
|
builder.Services.AddSingleton<ISecureCredentialService, ServerSecureCredentialService>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<IAcLogWriterBase, ConsoleLogWriter>();
|
builder.Services.AddSingleton<IAcLogWriterBase, ConsoleLogWriter>();
|
||||||
// Logger factory — thin Logger, DI-singleton writers. Each call yields a fresh LoggerClient
|
|
||||||
// with the caller's categoryName. Mirrors Microsoft's ILoggerFactory / ILogger<T> pattern.
|
// Logger options + framework factory. LoggerClient instances are created per caller category,
|
||||||
builder.Services.AddSingleton<Func<string, AcLoggerBase>>(sp => categoryName => new LoggerClient(categoryName, sp.GetServices<IAcLogWriterClientBase>().ToArray()));
|
// with AppType+LogLevel from appsettings and writers resolved from DI via IAcLogWriterBase.
|
||||||
|
builder.Services.Configure<AcLoggerOptions>(builder.Configuration.GetSection("AyCode:Logger"));
|
||||||
|
builder.Services.AddAcLoggerFactory<LoggerClient>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<LoggedInModel>(sp => new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
|
builder.Services.AddSingleton<LoggedInModel>(sp => new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
|
||||||
|
|
||||||
|
|
@ -48,16 +48,19 @@ builder.Services.Configure<AcBinaryHubProtocolOptions>(opts =>
|
||||||
|
|
||||||
// HubConnectionBuilder — transient so each consumer gets a fresh builder to Build().
|
// HubConnectionBuilder — transient so each consumer gets a fresh builder to Build().
|
||||||
// All connection and protocol configuration flows from appsettings.json via IOptions<T>;
|
// All connection and protocol configuration flows from appsettings.json via IOptions<T>;
|
||||||
// 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<T>() registrations.
|
||||||
builder.Services.AddTransient<IHubConnectionBuilder>(sp =>
|
builder.Services.AddTransient<IHubConnectionBuilder>(sp =>
|
||||||
{
|
{
|
||||||
var loggerFactory = sp.GetRequiredService<Func<string, AcLoggerBase>>();
|
var loggerFactory = sp.GetRequiredService<Func<string, LoggerClient>>();
|
||||||
var connectionOpts = sp.GetRequiredService<IOptions<AcHubConnectionOptions>>().Value;
|
var connectionOpts = sp.GetRequiredService<IOptions<AcHubConnectionOptions>>().Value;
|
||||||
|
var protocolOpts = sp.GetRequiredService<IOptions<AcBinaryHubProtocolOptions>>().Value;
|
||||||
|
|
||||||
var logger = loggerFactory(nameof(FruitBankSignalRClient));
|
var logger = loggerFactory(nameof(FruitBankSignalRClient));
|
||||||
var hubBuilder = new HubConnectionBuilder().AddFruitBankDefaults(logger, connectionOpts);
|
var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOpts);
|
||||||
|
|
||||||
hubBuilder.AddAcBinaryProtocol(); // IOptions<AcBinaryHubProtocolOptions> from DI
|
hubBuilder.AddAcBinaryProtocol(protocolOpts);
|
||||||
return hubBuilder;
|
return hubBuilder;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -82,9 +85,6 @@ else
|
||||||
app.UseHsts();
|
app.UseHsts();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.MapHub<LoggerSignalRHub>($"/{FruitBankConstClient.LoggerHubName}");
|
|
||||||
app.MapHub<DevAdminSignalRHub>($"/{FruitBankConstClient.DefaultHubName}");
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +1,44 @@
|
||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Debug",
|
"Default": "Debug",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"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",
|
"AcHubConnection": {
|
||||||
"TransportMaxBufferSize": 30000000,
|
"Url": "https://localhost:59579/fbHub",
|
||||||
"ApplicationMaxBufferSize": 30000000,
|
"TransportMaxBufferSize": 30000000,
|
||||||
"CloseTimeout": "00:00:10",
|
"ApplicationMaxBufferSize": 30000000,
|
||||||
"KeepAliveInterval": "00:01:00",
|
"CloseTimeout": "00:00:10",
|
||||||
"ServerTimeout": "00:03:00",
|
"KeepAliveInterval": "00:01:00",
|
||||||
"SkipNegotiation": true,
|
"ServerTimeout": "00:03:00",
|
||||||
"Transports": "WebSockets",
|
"SkipNegotiation": true,
|
||||||
"UseAutomaticReconnect": true,
|
"Transports": "WebSockets",
|
||||||
"UseStatefulReconnect": true
|
"UseAutomaticReconnect": true,
|
||||||
},
|
"UseStatefulReconnect": true
|
||||||
|
},
|
||||||
"AcBinaryHubProtocol": {
|
"AcBinaryHubProtocol": {
|
||||||
"ProtocolMode": "AsyncSegment",
|
"ProtocolMode": "AsyncSegment",
|
||||||
"BufferSize": 4096,
|
"BufferSize": 4096,
|
||||||
"WaitForFlush": true,
|
"WaitForFlush": false,
|
||||||
"FlushTimeout": "00:00:10"
|
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -79,8 +79,9 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- appsettings.json — embedded so MauiAppBuilder can load via GetManifestResourceStream -->
|
<!-- Shared appsettings.json linked from FruitBankHybrid.Shared, embedded so MauiAppBuilder can load via GetManifestResourceStream.
|
||||||
<EmbeddedResource Include="appsettings.json" />
|
LogicalName preserves the original manifest resource name so the loader in MauiProgram.cs doesn't need changes. -->
|
||||||
|
<EmbeddedResource Include="..\FruitBankHybrid.Shared\appsettings.json" Link="appsettings.json" LogicalName="FruitBankHybrid.appsettings.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,10 @@ namespace FruitBankHybrid
|
||||||
#endif
|
#endif
|
||||||
builder.Services.AddSingleton<IAcLogWriterClientBase, SignaRClientLogItemWriter>();
|
builder.Services.AddSingleton<IAcLogWriterClientBase, SignaRClientLogItemWriter>();
|
||||||
|
|
||||||
// Logger factory — the Logger itself is a thin wrapper; writers (the real sinks)
|
// Logger options + framework factory. LoggerClient instances are created per caller category,
|
||||||
// are DI singletons. Each call produces a fresh LoggerClient with the caller's categoryName.
|
// with AppType+LogLevel from appsettings and writers resolved from DI via IAcLogWriterClientBase.
|
||||||
// Mirrors the Microsoft ILoggerFactory / ILogger<T> pattern.
|
builder.Services.Configure<AcLoggerOptions>(builder.Configuration.GetSection("AyCode:Logger"));
|
||||||
builder.Services.AddSingleton<Func<string, AcLoggerBase>>(sp => categoryName => new LoggerClient(categoryName, sp.GetServices<IAcLogWriterClientBase>().ToArray()));
|
builder.Services.AddAcLoggerFactory<LoggerClient, IAcLogWriterClientBase>();
|
||||||
|
|
||||||
// Bind SignalR options from configuration.
|
// Bind SignalR options from configuration.
|
||||||
// Precedence: code default → appsettings.json (this line) → any later Configure<T> action.
|
// Precedence: code default → appsettings.json (this line) → any later Configure<T> action.
|
||||||
|
|
@ -68,16 +68,19 @@ namespace FruitBankHybrid
|
||||||
|
|
||||||
// SignalR HubConnectionBuilder — transient so each consumer gets a fresh builder to Build().
|
// SignalR HubConnectionBuilder — transient so each consumer gets a fresh builder to Build().
|
||||||
// All connection and protocol configuration flows from appsettings.json via IOptions<T>;
|
// All connection and protocol configuration flows from appsettings.json via IOptions<T>;
|
||||||
// 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<T>() registrations.
|
||||||
builder.Services.AddTransient<IHubConnectionBuilder>(sp =>
|
builder.Services.AddTransient<IHubConnectionBuilder>(sp =>
|
||||||
{
|
{
|
||||||
var loggerFactory = sp.GetRequiredService<Func<string, AcLoggerBase>>();
|
var loggerFactory = sp.GetRequiredService<Func<string, LoggerClient>>();
|
||||||
var connectionOpts = sp.GetRequiredService<IOptions<AcHubConnectionOptions>>().Value;
|
var connectionOpts = sp.GetRequiredService<IOptions<AcHubConnectionOptions>>().Value;
|
||||||
|
var protocolOpts = sp.GetRequiredService<IOptions<AcBinaryHubProtocolOptions>>().Value;
|
||||||
|
|
||||||
var logger = loggerFactory(nameof(FruitBankSignalRClient));
|
var logger = loggerFactory(nameof(FruitBankSignalRClient));
|
||||||
var hubBuilder = new HubConnectionBuilder().AddFruitBankDefaults(logger, connectionOpts);
|
var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOpts);
|
||||||
|
|
||||||
hubBuilder.AddAcBinaryProtocol(); // IOptions<AcBinaryHubProtocolOptions> from DI
|
hubBuilder.AddAcBinaryProtocol(protocolOpts);
|
||||||
return hubBuilder;
|
return hubBuilder;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue