feat(core): add shared DTOs, MqttTopics constants, DatabaseBootstrapper, and ManagedMqttClient extensions
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Dtos.Logging;
|
||||
using FinlyticCore.Models.Settings;
|
||||
@@ -77,31 +79,58 @@ public class FinlyticLogger<TContextClass> : IFinlyticLogger<TContextClass>
|
||||
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
|
||||
}
|
||||
|
||||
// Matches a structured-logging placeholder like "{CorrelationId}" or "{Score:F1}" - named-placeholder
|
||||
// syntax as consumed by ILogger.Log's message templates, NOT .NET's positional composite-format syntax
|
||||
// ("{0}", "{1}") that string.Format expects.
|
||||
private static readonly Regex PlaceholderPattern = new(@"\{([^{}:]+)(:[^{}]+)?\}", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Substitutes every named placeholder in <paramref name="message"/> with the corresponding entry of
|
||||
/// <paramref name="args"/>, in order of appearance - the same positional mapping
|
||||
/// <c>ILogger.LogInformation(message, args)</c> itself performs internally for structured-logging message
|
||||
/// templates. <c>string.Format(message, args)</c> (the previous implementation) expects numeric
|
||||
/// placeholders ("{0}") instead, throws a <see cref="FormatException"/> on a named one like
|
||||
/// "{CorrelationId}", and the broadcast silently fell back to the raw, unsubstituted template - which is
|
||||
/// exactly what showed up in the live log console instead of the real value.
|
||||
/// </summary>
|
||||
private static string FormatLogMessage(string message, object[]? args)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message) || args == null || args.Length == 0) return message;
|
||||
|
||||
int argIndex = 0;
|
||||
return PlaceholderPattern.Replace(message, match =>
|
||||
{
|
||||
if (argIndex >= args.Length) return match.Value;
|
||||
|
||||
var value = args[argIndex++];
|
||||
var formatSpec = match.Groups[2].Value; // e.g. ":F2", or "" when the template has no format spec.
|
||||
|
||||
if (!string.IsNullOrEmpty(formatSpec) && value is IFormattable formattable)
|
||||
{
|
||||
try
|
||||
{
|
||||
return formattable.ToString(formatSpec.TrimStart(':'), CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// Fall through to a plain ToString() rather than losing the value entirely.
|
||||
}
|
||||
}
|
||||
|
||||
return value?.ToString() ?? "null";
|
||||
});
|
||||
}
|
||||
|
||||
private void DispatchBroadcast(SettingKey<bool> channelKey, LogLevel level, string message, Exception? exception, params object[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
string formattedMsg = args != null && args.Length > 0 ? string.Format(message, args) : message;
|
||||
FinlyticLogBroadcaster.Broadcast(new LogMessageDto(
|
||||
Timestamp: DateTime.UtcNow,
|
||||
ServiceName: ServiceName,
|
||||
Channel: channelKey.Name,
|
||||
Level: level.ToString(),
|
||||
Message: formattedMsg,
|
||||
Exception: exception?.ToString()
|
||||
));
|
||||
}
|
||||
catch
|
||||
{
|
||||
FinlyticLogBroadcaster.Broadcast(new LogMessageDto(
|
||||
Timestamp: DateTime.UtcNow,
|
||||
ServiceName: ServiceName,
|
||||
Channel: channelKey.Name,
|
||||
Level: level.ToString(),
|
||||
Message: message,
|
||||
Exception: exception?.ToString()
|
||||
));
|
||||
}
|
||||
FinlyticLogBroadcaster.Broadcast(new LogMessageDto(
|
||||
Timestamp: DateTime.UtcNow,
|
||||
ServiceName: ServiceName,
|
||||
Channel: channelKey.Name,
|
||||
Level: level.ToString(),
|
||||
Message: FormatLogMessage(message, args),
|
||||
Exception: exception?.ToString()
|
||||
));
|
||||
}
|
||||
|
||||
#region Debug
|
||||
|
||||
@@ -108,11 +108,10 @@ public class SettingsService : ISettingsService
|
||||
IEnumerable<Type>? customKeyHolders = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var holderTypes = new List<Type> { typeof(CoreSettingKeys) };
|
||||
if (customKeyHolders != null)
|
||||
{
|
||||
holderTypes.AddRange(customKeyHolders);
|
||||
}
|
||||
var isCustomScoped = customKeyHolders != null && customKeyHolders.Any();
|
||||
var holderTypes = isCustomScoped
|
||||
? customKeyHolders!.ToList()
|
||||
: new List<Type> { typeof(CoreSettingKeys) };
|
||||
|
||||
var resultList = new List<DynamicSettingDto>();
|
||||
var seenKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -154,34 +153,37 @@ public class SettingsService : ISettingsService
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Prüfen, ob in der DB weitere gespeicherte Settings existieren, die nicht im Code deklariert sind
|
||||
try
|
||||
// 2. Prüfen, ob in der DB weitere gespeicherte Settings existieren (nur wenn nicht strikt auf custom KeyHolders begrenzt)
|
||||
if (!isCustomScoped)
|
||||
{
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
var dbContext = scope.ServiceProvider.GetService<ISettingsDbContext>();
|
||||
if (dbContext != null)
|
||||
try
|
||||
{
|
||||
var dbSettings = await dbContext.DynamicSettings.AsNoTracking().ToListAsync(cancellationToken);
|
||||
foreach (var dbSetting in dbSettings)
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
var dbContext = scope.ServiceProvider.GetService<ISettingsDbContext>();
|
||||
if (dbContext != null)
|
||||
{
|
||||
if (!seenKeys.Contains(dbSetting.Key))
|
||||
var dbSettings = await dbContext.DynamicSettings.AsNoTracking().ToListAsync(cancellationToken);
|
||||
foreach (var dbSetting in dbSettings)
|
||||
{
|
||||
seenKeys.Add(dbSetting.Key);
|
||||
var (inferredVal, inferredType) = InferJsonValueAndType(dbSetting.ValueJson);
|
||||
resultList.Add(new DynamicSettingDto(
|
||||
Key: dbSetting.Key,
|
||||
Value: inferredVal,
|
||||
Type: inferredType,
|
||||
Description: FormatDescriptionFromKey(dbSetting.Key),
|
||||
UpdatedAt: dbSetting.LastUpdatedUtc
|
||||
));
|
||||
if (!seenKeys.Contains(dbSetting.Key))
|
||||
{
|
||||
seenKeys.Add(dbSetting.Key);
|
||||
var (inferredVal, inferredType) = InferJsonValueAndType(dbSetting.ValueJson);
|
||||
resultList.Add(new DynamicSettingDto(
|
||||
Key: dbSetting.Key,
|
||||
Value: inferredVal,
|
||||
Type: inferredType,
|
||||
Description: FormatDescriptionFromKey(dbSetting.Key),
|
||||
UpdatedAt: dbSetting.LastUpdatedUtc
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "[SettingsService] Error reading database settings during GetAllRegisteredSettingsAsync.");
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "[SettingsService] Error reading database settings during GetAllRegisteredSettingsAsync.");
|
||||
}
|
||||
}
|
||||
|
||||
return resultList.OrderBy(s => s.Key).ToList();
|
||||
|
||||
@@ -8,7 +8,7 @@ using FinlyticCore.Dtos.Fundamentals;
|
||||
using FinlyticCore.Dtos.Yahoo;
|
||||
using FinlyticCore.Models.Settings;
|
||||
using FinlyticCore.Services;
|
||||
using FinlyticCore.Utils;
|
||||
using FinlyticCore.Util;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace FinlyticCore.Services.Yahoo;
|
||||
|
||||
Reference in New Issue
Block a user