feat(core): add shared DTOs, MqttTopics constants, DatabaseBootstrapper, and ManagedMqttClient extensions

This commit is contained in:
2026-08-24 21:35:24 +02:00
parent 6ab84fe1de
commit 44b161d509
39 changed files with 2545 additions and 709 deletions
@@ -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