feat(fundamentals): dynamic settings, IFinlyticLogger, live log streaming, and EF migration

This commit is contained in:
2026-08-15 21:29:50 +02:00
parent 3dbee36ca0
commit a1f2b888f6
7 changed files with 600 additions and 28 deletions
@@ -1,10 +1,12 @@
using FinlyticCore.Database;
using FinlyticCore.Entities.Settings;
using FinlyticFundamentals.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace FinlyticFundamentals.Database;
public class FundamentalsDbContext : DbContext
public class FundamentalsDbContext : DbContext, ISettingsDbContext
{
public FundamentalsDbContext(DbContextOptions<FundamentalsDbContext> options) : base(options)
{
@@ -23,7 +25,7 @@ public class FundamentalsDbContext : DbContext
modelBuilder.Entity<SettingEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.Key);
entity.HasIndex(e => e.Key).IsUnique();
});
modelBuilder.Entity<AssetDataEntity>(entity =>
@@ -93,3 +95,13 @@ public class FundamentalsDbContext : DbContext
});
}
}
public class FundamentalsDbContextFactory : IDesignTimeDbContextFactory<FundamentalsDbContext>
{
public FundamentalsDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<FundamentalsDbContext>();
optionsBuilder.UseNpgsql("Host=localhost;Database=fundamentals;Username=postgres;Password=postgres");
return new FundamentalsDbContext(optionsBuilder.Options);
}
}
@@ -0,0 +1,430 @@
// <auto-generated />
using System;
using FinlyticFundamentals.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FinlyticFundamentals.Migrations
{
[DbContext(typeof(FundamentalsDbContext))]
[Migration("20260815183935_AddDynamicSettings")]
partial class AddDynamicSettings
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<DateTime>("LastUpdatedUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("ServiceIdentifier")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ValueJson")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("DynamicSettings");
});
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetDataEntity", b =>
{
b.Property<string>("Isin")
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Isin");
b.ToTable("AssetData");
});
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetEventEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AssetDataIsin")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("AssetDataIsin");
b.ToTable("AssetEvents");
});
modelBuilder.Entity("FinlyticFundamentals.Entities.FundamentalDataEntity", b =>
{
b.Property<string>("Isin")
.HasColumnType("text");
b.Property<string>("AssetDataIsin")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ConsensusRating")
.HasColumnType("text");
b.Property<decimal?>("CurrentRatio")
.HasColumnType("numeric");
b.Property<decimal?>("DebtToEquity")
.HasColumnType("numeric");
b.Property<decimal?>("DilutedEps")
.HasColumnType("numeric");
b.Property<decimal?>("Ebitda")
.HasColumnType("numeric");
b.Property<decimal?>("EnterpriseValue")
.HasColumnType("numeric");
b.Property<decimal?>("EvToEbitda")
.HasColumnType("numeric");
b.Property<decimal?>("FiftyTwoWeekHigh")
.HasColumnType("numeric");
b.Property<decimal?>("FiftyTwoWeekLow")
.HasColumnType("numeric");
b.Property<decimal?>("ForwardDividendYield")
.HasColumnType("numeric");
b.Property<decimal?>("ForwardPe")
.HasColumnType("numeric");
b.Property<decimal?>("FreeCashFlow")
.HasColumnType("numeric");
b.Property<decimal?>("GrossProfit")
.HasColumnType("numeric");
b.Property<DateTime>("LastUpdatedUtc")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("MarketCap")
.HasColumnType("numeric");
b.Property<decimal?>("NetIncome")
.HasColumnType("numeric");
b.Property<decimal?>("OperatingCashFlow")
.HasColumnType("numeric");
b.Property<decimal?>("OperatingIncome")
.HasColumnType("numeric");
b.Property<decimal?>("PayoutRatio")
.HasColumnType("numeric");
b.Property<decimal?>("PegRatio")
.HasColumnType("numeric");
b.Property<decimal?>("PercentHeldByInsiders")
.HasColumnType("numeric");
b.Property<decimal?>("PercentHeldByInstitutions")
.HasColumnType("numeric");
b.Property<decimal?>("PriceTargetHigh")
.HasColumnType("numeric");
b.Property<decimal?>("PriceTargetLow")
.HasColumnType("numeric");
b.Property<decimal?>("PriceTargetMean")
.HasColumnType("numeric");
b.Property<decimal?>("PriceToBook")
.HasColumnType("numeric");
b.Property<decimal?>("PriceToSales")
.HasColumnType("numeric");
b.Property<decimal?>("ReturnOnAssets")
.HasColumnType("numeric");
b.Property<decimal?>("ReturnOnEquity")
.HasColumnType("numeric");
b.Property<decimal?>("RevenueGrowthYoY")
.HasColumnType("numeric");
b.Property<decimal?>("ShortPercentOfFloat")
.HasColumnType("numeric");
b.Property<decimal?>("ShortRatio")
.HasColumnType("numeric");
b.Property<decimal?>("TotalCash")
.HasColumnType("numeric");
b.Property<decimal?>("TotalDebt")
.HasColumnType("numeric");
b.Property<decimal?>("TotalRevenue")
.HasColumnType("numeric");
b.Property<decimal?>("TrailingPe")
.HasColumnType("numeric");
b.HasKey("Isin");
b.HasIndex("AssetDataIsin");
b.ToTable("FundamentalData");
});
modelBuilder.Entity("FinlyticFundamentals.Entities.KeyExecutiveEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AssetDataIsin")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Payment")
.IsRequired()
.HasColumnType("text");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0);
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("AssetDataIsin", "SortOrder");
b.ToTable("KeyExecutives");
});
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetDataEntity", b =>
{
b.OwnsOne("FinlyticFundamentals.Entities.TickerEntity", "PrimaryTicker", b1 =>
{
b1.Property<string>("AssetDataEntityIsin")
.HasColumnType("text");
b1.Property<string>("Exchange")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("")
.HasColumnName("PrimaryTickerExchange");
b1.Property<string>("Ticker")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("")
.HasColumnName("PrimaryTicker");
b1.HasKey("AssetDataEntityIsin");
b1.ToTable("AssetData");
b1.WithOwner()
.HasForeignKey("AssetDataEntityIsin");
});
b.OwnsMany("FinlyticFundamentals.Entities.TickerEntity", "AvailableTickers", b1 =>
{
b1.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b1.Property<string>("AssetDataIsin")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("Exchange")
.IsRequired()
.HasColumnType("text")
.HasColumnName("Exchange");
b1.Property<string>("Ticker")
.IsRequired()
.HasColumnType("text")
.HasColumnName("Ticker");
b1.HasKey("Id");
b1.HasIndex("AssetDataIsin");
b1.HasIndex("Ticker");
b1.ToTable("Tickers", (string)null);
b1.WithOwner()
.HasForeignKey("AssetDataIsin");
});
b.Navigation("AvailableTickers");
b.Navigation("PrimaryTicker")
.IsRequired();
});
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetEventEntity", b =>
{
b.HasOne("FinlyticFundamentals.Entities.AssetDataEntity", "AssetData")
.WithMany("AssetEvents")
.HasForeignKey("AssetDataIsin")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("FinlyticFundamentals.Entities.TickerEntity", "Ticker", b1 =>
{
b1.Property<Guid>("AssetEventEntityId")
.HasColumnType("uuid");
b1.Property<string>("Exchange")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("")
.HasColumnName("TickerExchange");
b1.Property<string>("Ticker")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("")
.HasColumnName("Ticker");
b1.HasKey("AssetEventEntityId");
b1.ToTable("AssetEvents");
b1.WithOwner()
.HasForeignKey("AssetEventEntityId");
});
b.Navigation("AssetData");
b.Navigation("Ticker")
.IsRequired();
});
modelBuilder.Entity("FinlyticFundamentals.Entities.FundamentalDataEntity", b =>
{
b.HasOne("FinlyticFundamentals.Entities.AssetDataEntity", "AssetData")
.WithMany("FundamentalData")
.HasForeignKey("AssetDataIsin")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("FinlyticFundamentals.Entities.TickerEntity", "Ticker", b1 =>
{
b1.Property<string>("FundamentalDataEntityIsin")
.HasColumnType("text");
b1.Property<string>("Exchange")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("")
.HasColumnName("TickerExchange");
b1.Property<string>("Ticker")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("")
.HasColumnName("Ticker");
b1.HasKey("FundamentalDataEntityIsin");
b1.ToTable("FundamentalData");
b1.WithOwner()
.HasForeignKey("FundamentalDataEntityIsin");
});
b.Navigation("AssetData");
b.Navigation("Ticker")
.IsRequired();
});
modelBuilder.Entity("FinlyticFundamentals.Entities.KeyExecutiveEntity", b =>
{
b.HasOne("FinlyticFundamentals.Entities.AssetDataEntity", "AssetData")
.WithMany("KeyExecutives")
.HasForeignKey("AssetDataIsin")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AssetData");
});
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetDataEntity", b =>
{
b.Navigation("AssetEvents");
b.Navigation("FundamentalData");
b.Navigation("KeyExecutives");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FinlyticFundamentals.Migrations
{
/// <inheritdoc />
public partial class AddDynamicSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_DynamicSettings_Key",
table: "DynamicSettings");
migrationBuilder.CreateIndex(
name: "IX_DynamicSettings_Key",
table: "DynamicSettings",
column: "Key",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_DynamicSettings_Key",
table: "DynamicSettings");
migrationBuilder.CreateIndex(
name: "IX_DynamicSettings_Key",
table: "DynamicSettings",
column: "Key");
}
}
}
@@ -47,7 +47,8 @@ namespace FinlyticFundamentals.Migrations
b.HasKey("Id");
b.HasIndex("Key");
b.HasIndex("Key")
.IsUnique();
b.ToTable("DynamicSettings");
});
+19 -13
View File
@@ -1,20 +1,26 @@
using FinlyticCore.Clients;
using FinlyticCore.Database;
using FinlyticCore.Services;
using FinlyticCore.Services.PlaywrightScrapper;
using FinlyticCore.Services.TradeRepublic;
using FinlyticCore.Services.Yahoo;
using Microsoft.EntityFrameworkCore;
using FinlyticFundamentals.Database;
using FinlyticFundamentals.Services;
using FinlyticFundamentals.Util;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var builder = Host.CreateApplicationBuilder(args);
// Register DB Context
// Register DB Context & ISettingsDbContext
builder.Services.AddDbContext<FundamentalsDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))
.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)));
builder.Services.AddScoped<ISettingsDbContext>(sp => sp.GetRequiredService<FundamentalsDbContext>());
// Register HTTP Clients
builder.Services.AddHttpClient<IYahooFinanceScraper, YahooFinanceScraper>()
@@ -25,22 +31,23 @@ builder.Services.AddHttpClient<IYahooFinanceScraper, YahooFinanceScraper>()
AllowAutoRedirect = true
});
builder.Services.AddSingleton<IPlaywrightBrowserFactory, PlaywrightBrowserFactory>();
builder.Services.AddSingleton<IPlaywrightExecutionService, PlaywrightExecutionService>();
builder.Services.AddTransient<IYahooFinanceHtmlClient, YahooFinanceHtmlClient<FundamentalsDbService, FundamentalsDbContext>>();
builder.Services.AddScoped<IPlaywrightBrowserFactory, PlaywrightBrowserFactory>();
builder.Services.AddScoped<IPlaywrightExecutionService, PlaywrightExecutionService>();
builder.Services.AddTransient<IYahooFinanceHtmlClient, YahooFinanceHtmlClient>();
// Register Application Services
builder.Services.AddSingleton<TradeRepublicClient>();
builder.Services.AddSingleton<ITradeRepublicService, TradeRepublicService>();
builder.Services.AddSingleton<FinlyticCore.Services.Yahoo.YahooFinanceClient>();
builder.Services.AddTransient<IFundamentalsDbService, FundamentalsDbService>();
builder.Services.AddScoped<TradeRepublicClient>();
builder.Services.AddScoped<ITradeRepublicService, TradeRepublicService>();
builder.Services.AddSingleton<YahooFinanceClient>();
builder.Services.AddScoped<IFundamentalsDbService, FundamentalsDbService>();
builder.Services.AddScoped<IYahooFinanceScraper, YahooFinanceScraper>();
builder.Services.AddScoped(typeof(ISettingsService<>), typeof(SettingsService<>));
builder.Services.AddScoped(typeof(IFinlyticLogger<,>), typeof(FinlyticLogger<,>));
builder.Services.AddSingleton<ISettingsService, SettingsService>();
builder.Services.AddSingleton(typeof(IFinlyticLogger<>), typeof(FinlyticLogger<>));
// Register MQTT Client (as a Hosted Service)
builder.Services.AddHostedService<FundamentalsMqttClient>();
builder.Services.AddSingleton<FundamentalsMqttClient>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<FundamentalsMqttClient>());
var host = builder.Build();
@@ -52,7 +59,6 @@ using (var scope = host.Services.CreateScope())
var context = scope.ServiceProvider.GetRequiredService<FundamentalsDbContext>();
await context.Database.MigrateAsync();
Console.WriteLine("Database migrations successfully executed for FinlyticFundamentals.");
}
catch (Exception ex)
{
@@ -10,6 +10,7 @@ using FinlyticCore.Dtos.Yahoo;
using FinlyticCore.Models.Settings;
using FinlyticCore.Services;
using FinlyticCore.Services.TradeRepublic;
using FinlyticCore.Services.Yahoo;
using FinlyticFundamentals.Database;
using FinlyticFundamentals.Entities;
using FinlyticFundamentals.Util;
@@ -39,13 +40,13 @@ public class FundamentalsDbService : IFundamentalsDbService
private readonly IServiceScopeFactory _scopeFactory;
private readonly IYahooFinanceScraper _scraper;
private readonly ITradeRepublicService _tradeRepublicService;
private readonly IFinlyticLogger<FundamentalsDbService, FundamentalsDbContext> _finlyticLogger;
private readonly IFinlyticLogger<FundamentalsDbService> _finlyticLogger;
public FundamentalsDbService(
IServiceScopeFactory scopeFactory,
IYahooFinanceScraper scraper,
ITradeRepublicService tradeRepublicService,
IFinlyticLogger<FundamentalsDbService, FundamentalsDbContext> finlyticLogger)
IFinlyticLogger<FundamentalsDbService> finlyticLogger)
{
_scopeFactory = scopeFactory;
_scraper = scraper;
@@ -71,7 +72,7 @@ public class FundamentalsDbService : IFundamentalsDbService
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<FundamentalsDbContext>();
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService<FundamentalsDbContext>>();
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
// 1. Dynamic Settings lesen
bool allowForceRefresh =
@@ -1,12 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FinlyticCore.Dtos;
using FinlyticCore.Dtos.Settings;
using FinlyticCore.Models;
using FinlyticCore.Services;
using FinlyticCore.Util;
using FinlyticFundamentals.Database;
using FinlyticFundamentals.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -60,7 +61,17 @@ public class FundamentalsMqttClient : ManagedMqttClient, IHostedService
await SubscribeAsync("services/request/fundamentals_Get/#");
await SubscribeAsync("services/request/events_GetAll/#");
await SubscribeAsync("services/request/events_GetByMonth/#");
await SubscribeAsync("services/request/fundamentals_settings_GetAll/#");
await SubscribeAsync("services/request/fundamentals_settings_Update/#");
await SubscribeAsync("services/request/health_Ping/#");
FinlyticLogBroadcaster.OnLogPublished = async (logDto) =>
{
if (IsConnected && string.Equals(logDto.ServiceName, "FinlyticFundamentals", StringComparison.OrdinalIgnoreCase))
{
await PublishAsync("finlytic/logs/FinlyticFundamentals", logDto);
}
};
}
/// <inheritdoc />
@@ -85,6 +96,14 @@ public class FundamentalsMqttClient : ManagedMqttClient, IHostedService
{
await OnEventsGetByMonthAsync(payload, correlationId);
}
else if (topic.StartsWith("services/request/fundamentals_settings_GetAll", StringComparison.OrdinalIgnoreCase))
{
await OnSettingsGetAllAsync(correlationId);
}
else if (topic.StartsWith("services/request/fundamentals_settings_Update", StringComparison.OrdinalIgnoreCase))
{
await OnSettingsUpdateAsync(payload, correlationId);
}
else if (topic.StartsWith("services/request/health_Ping", StringComparison.OrdinalIgnoreCase))
{
await OnHealthPingAsync(topic, correlationId);
@@ -93,8 +112,8 @@ public class FundamentalsMqttClient : ManagedMqttClient, IHostedService
private async Task OnFundamentalsGetAsync(string payload, string correlationId)
{
using var scope = _scopeFactory.CreateScope();
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient, FundamentalsDbContext>>();
await using var scope = _scopeFactory.CreateAsyncScope();
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient>>();
var dbService = scope.ServiceProvider.GetRequiredService<IFundamentalsDbService>();
if (string.IsNullOrWhiteSpace(payload))
@@ -129,8 +148,8 @@ public class FundamentalsMqttClient : ManagedMqttClient, IHostedService
private async Task OnEventsGetAllAsync(string correlationId)
{
using var scope = _scopeFactory.CreateScope();
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient, FundamentalsDbContext>>();
await using var scope = _scopeFactory.CreateAsyncScope();
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient>>();
var dbService = scope.ServiceProvider.GetRequiredService<IFundamentalsDbService>();
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticFundamentals] [MQTT_Client] Processing RPC events_GetAll request [CorrelationId: {CorrelationId}]", correlationId);
@@ -152,8 +171,8 @@ public class FundamentalsMqttClient : ManagedMqttClient, IHostedService
{
if (string.IsNullOrWhiteSpace(payload)) return;
using var scope = _scopeFactory.CreateScope();
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient, FundamentalsDbContext>>();
await using var scope = _scopeFactory.CreateAsyncScope();
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient>>();
var dbService = scope.ServiceProvider.GetRequiredService<IFundamentalsDbService>();
try
@@ -175,12 +194,78 @@ public class FundamentalsMqttClient : ManagedMqttClient, IHostedService
}
}
private async Task OnSettingsGetAllAsync(string correlationId)
{
await using var scope = _scopeFactory.CreateAsyncScope();
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient>>();
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticFundamentals] [Settings_GetAll] Retrieving all dynamic settings via reflection [CorrelationId: {CorrelationId}]", correlationId);
try
{
var settings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
var responseTopic = $"services/response/fundamentals_settings_GetAll/{correlationId}";
await PublishAsync(responseTopic, settings);
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticFundamentals] [Settings_GetAll] Published {Count} settings to '{ResponseTopic}'", settings.Count, responseTopic);
}
catch (Exception ex)
{
await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticFundamentals] [Settings_GetAll] Failed to retrieve settings.");
}
}
private async Task OnSettingsUpdateAsync(string payload, string correlationId)
{
if (string.IsNullOrWhiteSpace(payload)) return;
await using var scope = _scopeFactory.CreateAsyncScope();
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient>>();
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticFundamentals] [Settings_Update] Processing settings update RPC [CorrelationId: {CorrelationId}]", correlationId);
try
{
Dictionary<string, object?>? updates = null;
try
{
updates = JsonSerializer.Deserialize<Dictionary<string, object?>>(payload);
}
catch
{
var list = JsonSerializer.Deserialize<List<DynamicSettingDto>>(payload);
if (list != null)
{
updates = new Dictionary<string, object?>();
foreach (var item in list)
{
updates[item.Key] = item.Value;
}
}
}
if (updates != null && updates.Count > 0)
{
await settingsService.UpdateSettingsAsync(updates);
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticFundamentals] [Settings_Update] Successfully updated {Count} settings in database and cache.", updates.Count);
}
var currentSettings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
var responseTopic = $"services/response/fundamentals_settings_Update/{correlationId}";
await PublishAsync(responseTopic, currentSettings);
}
catch (Exception ex)
{
await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticFundamentals] [Settings_Update] Failed to update settings.");
}
}
private async Task OnHealthPingAsync(string topic, string correlationId)
{
if (topic.Contains("FinlyticFundamentals", StringComparison.OrdinalIgnoreCase) || !topic.Contains("/", StringComparison.OrdinalIgnoreCase))
{
using var scope = _scopeFactory.CreateScope();
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient, FundamentalsDbContext>>();
await using var scope = _scopeFactory.CreateAsyncScope();
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient>>();
var respTopic = $"services/response/health_Ping/{correlationId}";
await PublishAsync(respTopic, new ServiceHealthResponse("FinlyticFundamentals", "Online", DateTime.UtcNow, "Connected"));