Skip to content

Json connector settings#5413

Merged
w1am merged 11 commits intomasterfrom
w1am/json-connector-settings
Jan 8, 2026
Merged

Json connector settings#5413
w1am merged 11 commits intomasterfrom
w1am/json-connector-settings

Conversation

@w1am
Copy link
Copy Markdown
Contributor

@w1am w1am commented Dec 17, 2025

Auto-created Ticket

#5414

PR Type

Enhancement


Description

  • Add JSON configuration support to connector settings

  • Implement protobuf Struct conversion utilities

  • Support both legacy and new configuration formats

  • Extend connector settings with structured configuration


Diagram Walkthrough

flowchart LR
  A["IConfiguration"] -->|"ToProtobufStruct()"| B["Protobuf Struct"]
  B -->|"FlattenStruct()"| C["Dictionary"]
  D["CreateConnector Command"] -->|"configuration field"| B
  E["ReconfigureConnector Command"] -->|"configuration field"| B
  B -->|"ConnectorSettings.From()"| F["ConnectorSettings"]
  F -->|"AsConfiguration()"| A
  G["SerilogSink"] -->|"supports both"| H["Legacy + New Config"]
Loading

File Walkthrough

Relevant files
Enhancement
8 files
ConfigurationExtensions.cs
Add configuration to protobuf conversion utilities             
+124/-0 
ConnectorSettings.cs
Extend connector settings with struct support                       
+15/-1   
ConnectorSettingsExtensions.cs
Add MapField to configuration conversion                                 
+14/-0   
SerilogSink.cs
Support new configuration format alongside legacy               
+11/-6   
ConnectorsCommandApplication.cs
Update command handlers to use new configuration                 
+2/-2     
ConnectorQueries.cs
Update queries to return protobuf struct configuration     
+6/-16   
commands.proto
Add configuration field to command messages                           
+8/-5     
queries.proto
Replace settings map with configuration struct                     
+4/-2     
Tests
2 files
ConfigurationExtensionsTests.cs
Test configuration and protobuf struct conversions             
+265/-0 
ConnectorSettingsTests.cs
Test connector settings creation and conversion                   
+249/-0 
Configuration changes
1 files
ManagementPlaneWireUp.cs
Configure gRPC JSON transcoding for struct handling           
+2/-0     

@w1am w1am requested a review from a team as a code owner December 17, 2025 13:58
@qodo-code-review
Copy link
Copy Markdown
Contributor

qodo-code-review Bot commented Dec 17, 2025

PR Compliance Guide 🔍

(Compliance updated until commit 2454861)

Below is a summary of compliance checks for this PR:

Security Compliance
Denial of service

Description: Converting arbitrarily deep or large configurations into protobuf Struct and flattening
them recursively can be exploited to cause excessive CPU/memory usage or stack overflow
(DoS) if untrusted configuration content reaches these utilities.
ConfigurationExtensions.cs [15-101]

Referred Code
    public Struct ToProtobufStruct() {
	    var protoStruct = new Struct();

	    foreach (var child in configuration.GetChildren()) {
		    var value = ConvertSectionToValue(child);

		    if (value != null)
			    protoStruct.Fields[child.Key] = value;
	    }

	    return protoStruct;

	    static Value? ConvertSectionToValue(IConfigurationSection section) {
		    var children = section.GetChildren().ToList();

		    if (children.Count == 0)
			    // parse leaf node, otherwise return null to omit empty keys
			    return !string.IsNullOrEmpty(section.Value)
				    ? ParseValue(section.Value)
				    : null;



 ... (clipped 66 lines)
Ticket Compliance
🟡
🎫 #5414
🟢 Implement utilities to convert between IConfiguration, protobuf Struct, and flat string
dictionaries.
Extend ConnectorSettings to support creation from protobuf Struct, while falling back to
the legacy flat settings format for backward compatibility.
Update commands to add a structured configuration field to CreateConnector and
ReconfigureConnector.
Maintain backward compatibility by supporting both legacy and new configuration formats in
connectors like SerilogSink.
🔴 Update query responses to use a structured configuration field instead of the flat
settings map.
Codebase Duplication Compliance
🟢
No codebase code duplication found New Components Detected (Top 5):
- ConfigurationExtensionsTests
- should_convert_flat_configuration_to_protobuf_struct
- should_convert_nested_configuration_to_protobuf_struct
- should_convert_array_configuration_to_protobuf_list
- should_omit_null_and_empty_values
Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Unhandled value case: FlattenValue does not handle Value.KindOneofCase.None, which can lead to silent data loss
when flattening Protobuf Values that are unset/unknown.

Referred Code
private static void FlattenValue(Value value, string key, Dictionary<string, string?> dictionary) {
    switch (value.KindCase) {
        case Value.KindOneofCase.NullValue:
            dictionary[key] = null;
            break;

        case Value.KindOneofCase.NumberValue:
            dictionary[key] = value.NumberValue.ToString(CultureInfo.InvariantCulture);
            break;

        case Value.KindOneofCase.StringValue:
            dictionary[key] = value.StringValue;
            break;

        case Value.KindOneofCase.BoolValue:
            dictionary[key] = value.BoolValue.ToString();
            break;

        case Value.KindOneofCase.StructValue:
            FlattenStruct(value.StructValue, key, dictionary);
            break;


 ... (clipped 9 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Potential secret exposure: GetConfiguration returns the fully unprotected connector configuration as a
google.protobuf.Struct, which may expose secrets unless authorization and/or
sensitive-field redaction is guaranteed by surrounding context not visible in this diff.

Referred Code
    public async Task<GetConnectorConfigurationResult> GetConfiguration(GetConnectorSettings query, CancellationToken cancellationToken) {
        var snapshot = await LoadSnapshot(cancellationToken);

        var connector = snapshot.Connectors.FirstOrDefault(x => x.ConnectorId == query.ConnectorId);

        if (connector is null)
            throw new DomainExceptions.EntityNotFound("Connector", query.ConnectorId);

        var unprotected = await DataProtector.Unprotect(connector.Settings.ToConfiguration(), cancellationToken);

        return new GetConnectorConfigurationResult {
	        Configuration           = unprotected.ToProtobufStruct(),
	        ConfigurationUpdateTime = connector.SettingsUpdateTime,
        };
    }

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

Previous compliance checks

Compliance check up to commit 48d713a
Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🟢
🎫 #5414
🟢 Implement utilities to convert between IConfiguration, protobuf Struct, and flat
dictionaries (including nested structures/arrays).
Extend ConnectorSettings to support creation from protobuf Struct with fallback to legacy
flat settings.
Add a `configuration` field to `CreateConnector` and `ReconfigureConnector` commands.
Replace flat settings map with structured `configuration` field in query responses.
Maintain backward compatibility with legacy configuration format in connectors (e.g.,
SerilogSink).
Codebase Duplication Compliance
🟢
No codebase code duplication found New Components Detected (Top 5):
- ConfigurationExtensionsTests
- should_convert_flat_configuration_to_protobuf_struct
- should_convert_nested_configuration_to_protobuf_struct
- should_convert_array_configuration_to_protobuf_list
- should_omit_null_and_empty_values
Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Unhandled decode failure: Open calls DecodeLegacyConfiguration() directly so an invalid base64/JSON legacy
configuration could throw and prevent the sink from opening unless upstream validation is
guaranteed to run.

Referred Code
if (options.HasLegacyConfiguration) {
    var serilogConfiguration = await options.DecodeLegacyConfiguration();
    Logger = new SwitchableLogger(loggerConfiguration
        .ReadFrom.Configuration(serilogConfiguration)
        .CreateLogger());
}
else if (context.Configuration.GetSection("Serilog").Exists()) {
    Logger = new SwitchableLogger(loggerConfiguration
        .ReadFrom.Configuration(context.Configuration)
        .CreateLogger());
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Legacy config validation: Legacy configuration is accepted as a base64 JSON string and decoded in Open without local
validation/guardrails, so safe handling depends on external enforcement of
SerilogSinkValidator.

Referred Code
if (options.HasLegacyConfiguration) {
    var serilogConfiguration = await options.DecodeLegacyConfiguration();
    Logger = new SwitchableLogger(loggerConfiguration
        .ReadFrom.Configuration(serilogConfiguration)
        .CreateLogger());
}
else if (context.Configuration.GetSection("Serilog").Exists()) {
    Logger = new SwitchableLogger(loggerConfiguration
        .ReadFrom.Configuration(context.Configuration)
        .CreateLogger());
}

Learn more about managing compliance generic rules or creating your own custom rules

Compliance check up to commit db65d7d
Security Compliance
Untrusted Serilog config

Description: User-supplied (legacy) Base64 JSON is decoded and passed into
loggerConfiguration.ReadFrom.Configuration(...), which can allow an attacker with access
to connector settings to trigger resource exhaustion (very large Base64/JSON causing high
memory/CPU usage) and potentially abuse Serilog configuration to enable dangerous
sinks/enrichers (e.g., writing to arbitrary file paths or network targets) depending on
what Serilog packages are available.
SerilogSink.cs [60-110]

Referred Code
if (options.HasLegacyConfiguration) {
    var serilogConfiguration = await options.DecodeLegacyConfiguration();
    Logger = new SwitchableLogger(loggerConfiguration
        .ReadFrom.Configuration(serilogConfiguration)
        .CreateLogger());
}
else if (context.Configuration.GetSection("Serilog").Exists()) {
    Logger = new SwitchableLogger(loggerConfiguration
        .ReadFrom.Configuration(context.Configuration)
        .CreateLogger());
}
else {
    Logger = new SwitchableLogger(loggerConfiguration
        .WriteTo.Console(
            outputTemplate: "[{Timestamp:HH:mm:ss:fff} {Level:u3}] ({ThreadId:000}) {SourceContext} {Message:lj}{NewLine}{Exception}",
            theme: AnsiConsoleTheme.Literate,
            applyThemeToRedirectedOutput: true
        )
        .CreateLogger());
}



 ... (clipped 30 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
🟢
No codebase code duplication found New Components Detected (Top 5):
- ConfigurationExtensionsTests
- should_convert_flat_configuration_to_protobuf_struct
- should_convert_nested_configuration_to_protobuf_struct
- should_convert_array_configuration_to_protobuf_list
- should_omit_null_and_empty_values
Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Unsafe array parsing: Array detection/order uses int.Parse(c.Key) which can throw (e.g., very large numeric
keys) instead of safely parsing and handling invalid/overflowing indices.

Referred Code
    // Determine if the section represents an array
    if (children.All(c => int.TryParse(c.Key, out _))) {
	    return Value
		    .ForList(children.OrderBy(c => int.Parse(c.Key))
		    .Select(ConvertSectionToValue)
		    .OfType<Value>().ToArray());
    }

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Culture-sensitive parsing: ParseValue uses culture-dependent parsing for int/long and DateTime, which can lead to
inconsistent interpretation of externally supplied configuration values across locales.

Referred Code
internal static Value ParseValue(string value) {
    if (bool.TryParse(value, out var boolValue))
        return Value.ForBool(boolValue);

    if (int.TryParse(value, out var intValue))
        return Value.ForNumber(intValue);

    if (long.TryParse(value, out var longValue))
        return Value.ForNumber(longValue);

    if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
        return Value.ForNumber(doubleValue);

    if (DateTime.TryParse(value, out var dateValue))
        return Value.ForString(dateValue.ToString("O"));

    if (Guid.TryParse(value, out var guidValue))
        return Value.ForString(guidValue.ToString());

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Admin ops logging: The PR changes how CreateConnector/ReconfigureConnector settings are derived (now
including cmd.Configuration), but the diff does not show whether these administrative
actions are audit-logged with sufficient user/outcome context.

Referred Code
    IEventStore store
) :
    base(cmd => cmd.ConnectorId, ConnectorsFeatureConventions.Streams.ManagementStreamTemplate, store) {
    OnAny<CreateConnector>((connector, cmd) => {
        connector.EnsureIsNew();

        var settings = ConnectorSettings
            .From(cmd.Configuration, cmd.Settings, cmd.ConnectorId)
            .EnsureValid(validateSettings)
            .Protect(protectSettings)
            .AsDictionary();

        CheckAccess(settings, licenseService);

        configureConnectorStreams(cmd.ConnectorId);

        return [
            new ConnectorCreated {
                ConnectorId = cmd.ConnectorId,
                Name        = cmd.Name,
                Settings    = { settings },


 ... (clipped 32 lines)

Learn more about managing compliance generic rules or creating your own custom rules

@qodo-code-review
Copy link
Copy Markdown
Contributor

qodo-code-review Bot commented Dec 17, 2025

PR Code Suggestions ✨

Latest suggestions up to 2454861

CategorySuggestion                                                                                                                                    Impact
Incremental [*]
Allow returning both payloads

Refactor the logic to correctly handle requests for both include_configuration
and include_settings. The current implementation exclusively returns
configuration, silently ignoring the request for settings. The fix ensures both
can be returned in a single response.

src/Connectors/KurrentDB.Connectors/Planes/Management/Queries/ConnectorQueries.cs [71-102]

 async (conn, _) => {
-	if (query.HasIncludeConfiguration) {
-		var config = query.IncludeConfiguration
-			? (await DataProtector.Unprotect(conn.Settings.ToConfiguration(), ct)).ToProtobufStruct()
-			: new Struct();
+	var includeSettings = query is { HasIncludeSettings: true, IncludeSettings: true };
+	var includeConfig   = query is { HasIncludeConfiguration: true, IncludeConfiguration: true };
+	var hasConfigFlag   = query.HasIncludeConfiguration;
 
-		return conn.With(x => {
-			x.Settings.Clear();
-			x.Configuration = config;
-			x.ConfigurationUpdateTime = x.SettingsUpdateTime;
-			x.SettingsUpdateTime = null;
-			return x;
-		});
+	IConfiguration? unprotected = null;
+	if (includeSettings || includeConfig) {
+		unprotected = await DataProtector.Unprotect(conn.Settings.ToConfiguration(), ct);
 	}
 
-	if (query is { HasIncludeSettings: true, IncludeSettings: true }) {
-		var unprotected = await DataProtector.Unprotect(conn.Settings.ToConfiguration(), ct);
-
-		return conn.With(x => {
-			x.Settings.Clear();
+	return conn.With(x => {
+		// Settings
+		x.Settings.Clear();
+		if (includeSettings && unprotected is not null) {
 			x.Settings.Add(unprotected.AsEnumerable()
 				.Where(s => s.Value != null)
 				.ToDictionary(s => s.Key, s => s.Value!));
-			return x;
-		});
-	}
+		}
 
-	return conn.With(x => {
-		x.Settings.Clear();
+		// Configuration (only touch these fields if the flag is present)
+		if (hasConfigFlag) {
+			x.Configuration = includeConfig && unprotected is not null
+				? unprotected.ToProtobufStruct()
+				: new Struct();
+
+			x.ConfigurationUpdateTime = x.SettingsUpdateTime;
+
+			// Only hide the legacy timestamp when we are not returning settings
+			if (!includeSettings)
+				x.SettingsUpdateTime = null;
+		}
+
 		return x;
 	});
 };
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a logic flaw where requesting both configuration and settings results in only configuration being returned. This behavior contradicts the API's apparent capability and leads to silent data loss for the client. The proposed fix correctly handles all combinations of flags, improving the correctness and usability of the API.

Medium
Make validation properly await async

Replace the async void custom validator with CustomAsync to ensure the
asynchronous operation is properly awaited by FluentValidation, preventing
potential race conditions and unhandled exceptions.

src/Connectors/KurrentDB.Connectors/Infrastructure/Connect/Components/Connectors/SerilogSink.cs [129-137]

 When(x => x.HasLegacySettings,
     () => RuleFor(x => x)
-        .Custom(async void (options, ctx) => {
+        .CustomAsync(async (options, ctx, ct) => {
             try {
                 await options.DecodeLegacySettings();
             } catch (Exception) {
                 ctx.AddFailure(new Failures.ConfigurationEncodingFailure());
             }
         }));
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that using async void with FluentValidation's Custom method can lead to unobserved exceptions and race conditions. Proposing CustomAsync is the correct fix to ensure asynchronous validation is handled properly.

Medium
Possible issue
Fix optional flag false semantics

Modify the condition for including connector configuration to check if
IncludeConfiguration is explicitly true, not just present. This fixes a bug
where a false value caused incorrect data manipulation and prevented fallback
logic.

src/Connectors/KurrentDB.Connectors/Planes/Management/Queries/ConnectorQueries.cs [71-102]

 	    async (conn, _) => {
-		    if (query.HasIncludeConfiguration) {
-			    var config = query.IncludeConfiguration
-				    ? (await DataProtector.Unprotect(conn.Settings.ToConfiguration(), ct)).ToProtobufStruct()
-				    : new Struct();
+		    if (query is { HasIncludeConfiguration: true, IncludeConfiguration: true }) {
+			    var unprotected = await DataProtector.Unprotect(conn.Settings.ToConfiguration(), ct);
+			    var config = unprotected.ToProtobufStruct();
 
 			    return conn.With(x => {
 				    x.Settings.Clear();
 				    x.Configuration = config;
 				    x.ConfigurationUpdateTime = x.SettingsUpdateTime;
 				    x.SettingsUpdateTime = null;
 				    return x;
 			    });
 		    }
 
 		    if (query is { HasIncludeSettings: true, IncludeSettings: true }) {
 			    var unprotected = await DataProtector.Unprotect(conn.Settings.ToConfiguration(), ct);
 
 			    return conn.With(x => {
 				    x.Settings.Clear();
 				    x.Settings.Add(unprotected.AsEnumerable()
 					    .Where(s => s.Value != null)
 					    .ToDictionary(s => s.Key, s => s.Value!));
 				    return x;
 			    });
 		    }
 
 		    return conn.With(x => {
 			    x.Settings.Clear();
 			    return x;
 		    });
 	    };
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a logical bug where sending include_configuration=false would incorrectly clear settings and timestamps, and prevent other flags like include_settings from being processed. This is a critical fix for the API's behavior.

Medium
Preserve empty values during flattening

Add a case for Value.KindOneofCase.None in the FlattenValue method's switch
statement to prevent silent data loss when encountering default or empty Value
instances during configuration flattening.

src/Connectors/KurrentDB.Connectors/Infrastructure/ConfigurationExtensions.cs [72-101]

 private static void FlattenValue(Value value, string key, Dictionary<string, string?> dictionary) {
     switch (value.KindCase) {
+        case Value.KindOneofCase.None:
         case Value.KindOneofCase.NullValue:
             dictionary[key] = null;
             break;
 
         case Value.KindOneofCase.NumberValue:
             dictionary[key] = value.NumberValue.ToString(CultureInfo.InvariantCulture);
             break;
 
         case Value.KindOneofCase.StringValue:
             dictionary[key] = value.StringValue;
             break;
 
         case Value.KindOneofCase.BoolValue:
             dictionary[key] = value.BoolValue.ToString();
             break;
 
         case Value.KindOneofCase.StructValue:
             FlattenStruct(value.StructValue, key, dictionary);
             break;
 
         case Value.KindOneofCase.ListValue:
             for (var i = 0; i < value.ListValue.Values.Count; i++) {
                 var itemKey = $"{key}:{i}";
                 FlattenValue(value.ListValue.Values[i], itemKey, dictionary);
             }
             break;
     }
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a missing case in the switch statement that could lead to data being silently dropped during the flattening process. Adding a case for Value.KindOneofCase.None makes the conversion more robust and prevents potential data loss.

Low
  • Update

Previous suggestions

Suggestions up to commit f01b59a
CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix invalid dictionary projection

Correct the invalid lambda syntax in the ToDictionary call to kvp =>
(string?)kvp.Value to fix a compilation error.

src/Connectors/KurrentDB.Connectors/Planes/Control/Model/ConnectorSettingsExtensions.cs [10-13]

 public static IConfiguration ToConfiguration(this MapField<string, string> source) {
-	var dictionary = source.ToDictionary(kvp => kvp.Key, string? (kvp) => kvp.Value);
+	var dictionary = source.ToDictionary(kvp => kvp.Key, kvp => (string?)kvp.Value);
 	return new ConfigurationBuilder().AddInMemoryCollection(dictionary).Build();
 }
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies invalid C# lambda syntax (string? (kvp) => kvp.Value) that would cause a compilation error and provides a valid fix.

High
Preserve array indices in lists

Fix a bug in array conversion where null entries were dropped, causing index
shifting. The proposed change preserves array indices by inserting NullValue
placeholders for missing items.

src/Connectors/KurrentDB.Connectors/Infrastructure/ConfigurationExtensions.cs [37-42]

 if (children.All(c => long.TryParse(c.Key, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))) {
-	return Value
-		.ForList(children.OrderBy(c => long.Parse(c.Key, NumberStyles.Integer, CultureInfo.InvariantCulture))
-		.Select(ConvertSectionToValue)
-		.OfType<Value>().ToArray());
+	var indexed = children
+		.Select(c => (Index: long.Parse(c.Key, NumberStyles.Integer, CultureInfo.InvariantCulture), Section: c))
+		.OrderBy(x => x.Index)
+		.ToList();
+
+	var max = indexed[^1].Index;
+	var values = new Value[(int)(max + 1)];
+
+	foreach (var (idx, child) in indexed) {
+		values[idx] = ConvertSectionToValue(child) ?? Value.ForNull();
+	}
+
+	for (var i = 0; i < values.Length; i++) {
+		values[i] ??= Value.ForNull();
+	}
+
+	return Value.ForList(values);
 }
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a bug where OfType<Value>() would drop null entries from an array, causing index shifting and data loss for sparse arrays. The fix ensures index preservation, which is critical for correctness.

Medium
General
Omit empty nested objects

Avoid emitting an empty Struct for nested objects where all children are null or
empty. Instead, return null so the parent key is omitted, preventing issues with
configuration fallback logic.

src/Connectors/KurrentDB.Connectors/Infrastructure/ConfigurationExtensions.cs [44-54]

 // Create a Struct for nested objects
 var structValue = new Struct();
 
 foreach (var child in children) {
 	var childValue = ConvertSectionToValue(child);
 
 	if (childValue is not null)
 		structValue.Fields[child.Key] = childValue;
 }
 
-return Value.ForStruct(structValue);
+return structValue.Fields.Count == 0 ? null : Value.ForStruct(structValue);
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that returning an empty Struct for an object with no valid properties can lead to incorrect behavior, especially with the new logic that prefers configuration over settings. The fix to return null prevents this issue.

Medium
Suggestions up to commit db65d7d
CategorySuggestion                                                                                                                                    Impact
High-level
Re-evaluate custom type parsing logic

The ConfigurationExtensions.ParseValue method should be modified to treat all
configuration values as strings. This change avoids potential data corruption
from incorrect automatic type inference, aligning with IConfiguration's
string-based nature.

Examples:

src/Connectors/KurrentDB.Connectors/Infrastructure/ConfigurationExtensions.cs [103-123]
    internal static Value ParseValue(string value) {
        if (bool.TryParse(value, out var boolValue))
            return Value.ForBool(boolValue);

        if (int.TryParse(value, out var intValue))
            return Value.ForNumber(intValue);

        if (long.TryParse(value, out var longValue))
            return Value.ForNumber(longValue);


 ... (clipped 11 lines)

Solution Walkthrough:

Before:

internal static Value ParseValue(string value) {
    if (bool.TryParse(value, out var boolValue))
        return Value.ForBool(boolValue);

    if (double.TryParse(value, ..., out var doubleValue))
        return Value.ForNumber(doubleValue);

    if (DateTime.TryParse(value, out var dateValue))
        return Value.ForString(dateValue.ToString("O"));

    // ... other type checks ...

    return Value.ForString(value);
}

After:

internal static Value ParseValue(string value) {
    // Always treat configuration values as strings to avoid incorrect type inference.
    // The consumer of the configuration is responsible for parsing.
    return Value.ForString(value);
}
Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies a significant design flaw in the ParseValue method, where automatic type inference can lead to data misinterpretation and subtle bugs, making it a critical issue.

High
General
Replace async void with Task

Replace the async void delegate in the FluentValidation Custom rule with
CustomAsync and an async Task delegate to handle asynchronous validation and
exceptions correctly.

src/Connectors/KurrentDB.Connectors/Infrastructure/Connect/Components/Connectors/SerilogSink.cs [128-138]

 public SerilogSinkValidator() {
-    When(x => x.HasLegacyConfiguration,
-        () => RuleFor(x => x)
-            .Custom(async void (options, ctx) => {
-                try {
-                    await options.DecodeLegacyConfiguration();
-                } catch (Exception) {
-                    ctx.AddFailure(new Failures.ConfigurationEncodingFailure());
-                }
-            }));
+    When(x => x.HasLegacyConfiguration, () =>
+        RuleFor(x => x).CustomAsync(async (options, ctx, ct) => {
+            try {
+                await options.DecodeLegacyConfiguration();
+            } catch (Exception) {
+                ctx.AddFailure(new Failures.ConfigurationEncodingFailure());
+            }
+        }));
 }
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that using async void in a Custom validator can lead to unhandled exceptions and proposes using CustomAsync which is the correct and safer approach for asynchronous validation logic.

Medium

@w1am w1am force-pushed the w1am/json-connector-settings branch 2 times, most recently from 48d713a to 5790408 Compare December 18, 2025 11:03
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Dec 18, 2025

Deploying eventstore with  Cloudflare Pages  Cloudflare Pages

Latest commit: ddcd05d
Status: ✅  Deploy successful!
Preview URL: https://dbb3ff6a.eventstore.pages.dev
Branch Preview URL: https://w1am-json-connector-settings.eventstore.pages.dev

View logs

w1am and others added 2 commits January 5, 2026 11:30
Co-authored-by: ragingkore <ragingkore@users.noreply.github.com>
@w1am w1am force-pushed the w1am/json-connector-settings branch from ddcd05d to a6d6dd2 Compare January 5, 2026 07:30
Copy link
Copy Markdown
Contributor

@RagingKore RagingKore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@w1am w1am merged commit 688ff86 into master Jan 8, 2026
141 of 143 checks passed
@w1am w1am deleted the w1am/json-connector-settings branch January 8, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants