Skip to content

Conversation

techouse
Copy link
Owner

@techouse techouse commented Aug 17, 2025

This pull request introduces comprehensive compatibility improvements for netstandard2.0, refactors utility and decoder logic for better maintainability, and optimizes handling of collections and string operations. The most important changes are grouped below.

.NET Standard 2.0 Compatibility

  • Added a polyfill for IsExternalInit in QsNet/Compat/IsExternalInit.cs to support init-only setters on netstandard2.0.
  • Refactored Decoder and Utils classes to use conditional compilation (#if NETSTANDARD2_0) for regex usage, string slicing, and encoding operations to ensure compatibility with netstandard2.0. [1] [2] [3]

Decoder Logic Enhancements

  • Updated key and value parsing in Decoder.cs to use compatible string slicing and encoding checks, including a custom case-insensitive string replace for query parameter normalization. [1] [2] [3] [4] [5] [6]
  • Improved handling of Latin1 encoding detection and usage, abstracting it via a helper property and function for cross-version compatibility. [1] [2]

Utility Method Refactoring

  • Optimized collection merging in Utils.cs by materializing enumerables to lists before processing, preventing multiple enumerations and improving performance. [1] [2] [3]
  • Refactored percent-encoding and decoding logic to use compatible string operations for both netstandard2.0 and newer frameworks, including handling of Unicode and hex escapes. [1] [2] [3] [4] [5]

ListFormat Extension Improvements

  • Refactored ListFormatExtensions to use static generator delegates for each format, reducing allocations and improving clarity. [1] [2]

Test Suite Cleanup

  • Removed obsolete or redundant test ToStringKeyedDictionary_Converts_Keys_To_String from UtilsTests.cs to streamline the test suite.

Summary by CodeRabbit

  • New Features
    • Added .NET Standard 2.0 support (including compatibility polyfills).
    • Introduced a regex-based delimiter option for splitting input.
  • Refactor
    • Improved encoding/decoding compatibility and performance across targets.
    • More robust handling of nested dictionaries/lists and circular references.
    • Minor optimizations to list-formatting behavior.
  • Tests
    • Removed a now‑redundant unit test.
  • Chores
    • Switched to multi-target builds (net8.0 and netstandard2.0).

@techouse techouse self-assigned this Aug 17, 2025
@techouse techouse added the enhancement New feature or request label Aug 17, 2025
Copy link

coderabbitai bot commented Aug 17, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds netstandard2.0 support via multi-targeting and NETSTANDARD2_0 conditional code: polyfill for init-only, regex/encoding/index-range fallbacks, decoder/utils adaptations, delimiter and list-format tweaks, removal of one unit test and an internal ToStringKeyedDictionary helper, and addition of ReferenceEqualityComparer and RegexDelimiter.

Changes

Cohort / File(s) Summary of changes
Project targeting
QsNet/QsNet.csproj
Switch to multi-targeting: net8.0;netstandard2.0.
Compatibility polyfill
QsNet/Compat/IsExternalInit.cs
Add NETSTANDARD2_0 IsExternalInit polyfill to enable init-only setters on netstandard2.0.
Decoder adaptations
QsNet/Internal/Decoder.cs
NETSTANDARD2_0 path: replace GeneratedRegex with cached Regex, add Latin-1 abstraction, replace range/index usages with Substring and index arithmetic, add ReplaceOrdinalIgnoreCase helper, and guard partial vs non-partial class declaration.
Utils refactor & NETSTANDARD2_0 paths
QsNet/Internal/Utils.cs
NETSTANDARD2_0 regex wrappers, materialize enumerables during merges, stable merge logic (srcList), ISO-8859-1 and unescape adjustments, remove private ToStringKeyedDictionary helper, add internal ReferenceEqualityComparer, and range/substring fallbacks.
Decode flow iteration
QsNet/Qs.cs
NETSTANDARD2_0-specific foreach without deconstruction (iterate kv pairs via kv.Key/kv.Value); overall decode/merge/compact flow preserved.
Models: charset validation
QsNet/Models/DecodeOptions.cs, QsNet/Models/EncodeOptions.cs
Charset validation made target-aware: on NETSTANDARD2_0 accept CodePage 28591 for Latin-1; other targets use Encoding.Latin1 equality.
Delimiter enhancements
QsNet/Models/Delimiter.cs
StringDelimiter Split adjusted for NETSTANDARD2_0 (single-char Split overload); new public RegexDelimiter(string Pattern) using compiled Regex added.
ListFormat optimization
QsNet/Enums/ListFormat.cs
Replace inline lambdas with cached static ListFormatGenerator fields; behavior unchanged.
Tests
QsNet.Tests/UtilsTests.cs
Remove ToStringKeyedDictionary_Converts_Keys_To_String unit test; other tests unchanged.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Qs as Qs.Decode
  participant Decoder
  participant Utils

  Caller->>Qs: Decode(query, options)
  Qs->>Decoder: ParseKeys(key,value,options)
  Decoder-->>Qs: parsed (IDictionary or value)
  Qs->>Utils: Merge(current, parsed, options)
  Utils-->>Qs: merged graph
  Qs->>Utils: Compact(merged)
  Utils-->>Qs: compacted graph
  Qs->>Utils: ToStringKeyDeepNonRecursive(compacted)
  Utils-->>Qs: Dictionary<string, object?>
  Qs-->>Caller: Result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50–75 minutes

Possibly related PRs

Poem

I nibble code like clover leaves,
Two targets now where one once weaved.
Regex tunnels and Latin‑1 breeze,
Brackets, splits, and subtle keys.
Hopping through builds old and new—hippity-hop, review! 🐇


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c47f371 and a4a8edb.

📒 Files selected for processing (1)
  • QsNet/Internal/Utils.cs (16 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/dotnetstandard2.0

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🔭 Outside diff range comments (2)
QsNet/Models/DecodeOptions.cs (1)

238-238: CopyWith sets Decoder to GetDecoder causing infinite recursion

Assigning Decoder = decoder ?? GetDecoder creates a self-referential delegate. GetDecoder checks if Decoder != null and then invokes Decoder, resulting in unbounded recursion.

Apply this diff to preserve existing custom decoders and avoid recursion (mirrors EncodeOptions.Encoder pattern):

-            Decoder = decoder ?? GetDecoder,
+            Decoder = decoder ?? Decoder,

Follow-up: Consider adding a unit test that calls new DecodeOptions().CopyWith() without a decoder and then asserts options.GetDecoder("x") delegates to Utils.Decode without recursion.

QsNet/Models/Delimiter.cs (1)

48-66: Fix duplicate ‘Pattern’ property and ensure Regex compiles against the final pattern

  • Declaring public string Pattern { get; init; } in the body conflicts with the auto-generated property from the primary constructor and will not compile.
  • _rx is initialized from Pattern at field-initializer time. If a consumer uses an object initializer to set Pattern, _rx will be compiled using the constructor argument, not the final value, causing a mismatch.

Apply this diff to remove the duplicate property and lazily initialize the regex from the finalized pattern:

 public sealed record RegexDelimiter(string Pattern) : IDelimiter
 {
-    private readonly Regex _rx = new(Pattern, RegexOptions.Compiled);
+    private Regex? _rx;
 
-    /// <summary>
-    ///     The regex pattern used for splitting the input string.
-    /// </summary>
-    public string Pattern { get; init; } = Pattern;
-
     /// <summary>
     ///     Splits the input string using the regex delimiter.
     /// </summary>
     /// <param name="input">The input string to split</param>
     /// <returns>A list of split strings</returns>
     public IEnumerable<string> Split(string input)
     {
-        return _rx.Split(input);
+        var rx = _rx ??= new Regex(Pattern, RegexOptions.Compiled);
+        return rx.Split(input);
     }
 }
🧹 Nitpick comments (3)
QsNet/Models/Delimiter.cs (1)

34-38: Prefer explicit array construction over collection expressions for broader compiler compatibility

The collection expression [Value] requires newer C# compilers. To avoid depending on the latest language features (especially important when multi-targeting), use the explicit array creation.

Apply this diff:

-#if NETSTANDARD2_0
-        return Value.Length == 1 ? input.Split(Value[0]) : input.Split([Value], StringSplitOptions.None);
-#else
+#if NETSTANDARD2_0
+        return Value.Length == 1
+            ? input.Split(Value[0])
+            : input.Split(new[] { Value }, StringSplitOptions.None);
+#else
         return input.Split(Value);
 #endif
QsNet/Internal/Decoder.cs (1)

99-107: Consider extracting the bracket token normalization logic.

The bracket token normalization (%5B[ and %5D]) appears in both conditional branches with different implementations. Consider extracting this into a helper method to reduce duplication and improve maintainability.

+    private static string NormalizeBracketTokens(string input)
+    {
+#if NETSTANDARD2_0
+        var result = ReplaceOrdinalIgnoreCase(input, "%5B", "[");
+        return ReplaceOrdinalIgnoreCase(result, "%5D", "]");
+#else
+        return input
+            .Replace("%5B", "[", StringComparison.OrdinalIgnoreCase)
+            .Replace("%5D", "]", StringComparison.OrdinalIgnoreCase);
+#endif
+    }
+
     internal static Dictionary<string, object?> ParseQueryStringValues(
         string str,
         DecodeOptions? options = null
     )
     {
         options ??= new DecodeOptions();
         var obj = new Dictionary<string, object?>();

-#if NETSTANDARD2_0
-        var cleanStr = options.IgnoreQueryPrefix ? str.TrimStart('?') : str;
-        cleanStr = ReplaceOrdinalIgnoreCase(cleanStr, "%5B", "[");
-        cleanStr = ReplaceOrdinalIgnoreCase(cleanStr, "%5D", "]");
-#else
-        var cleanStr = (options.IgnoreQueryPrefix ? str.TrimStart('?') : str)
-            .Replace("%5B", "[", StringComparison.OrdinalIgnoreCase)
-            .Replace("%5D", "]", StringComparison.OrdinalIgnoreCase);
-#endif
+        var cleanStr = options.IgnoreQueryPrefix ? str.TrimStart('?') : str;
+        cleanStr = NormalizeBracketTokens(cleanStr);
QsNet/Internal/Utils.cs (1)

135-138: Consider using TryAdd for consistency.

The code uses both ContainsKey check with indexer assignment and TryAdd. Consider using TryAdd consistently with appropriate handling.

-                    if (mutable.ContainsKey(j))
-                        mutable[j] = Merge(mutable[j], item, options);
-                    else
-                        mutable.Add(j, item);
+                    if (!mutable.TryAdd(j, item))
+                        mutable[j] = Merge(mutable[j], item, options);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 077ae31 and c47f371.

📒 Files selected for processing (10)
  • QsNet.Tests/UtilsTests.cs (0 hunks)
  • QsNet/Compat/IsExternalInit.cs (1 hunks)
  • QsNet/Enums/ListFormat.cs (2 hunks)
  • QsNet/Internal/Decoder.cs (10 hunks)
  • QsNet/Internal/Utils.cs (16 hunks)
  • QsNet/Models/DecodeOptions.cs (1 hunks)
  • QsNet/Models/Delimiter.cs (2 hunks)
  • QsNet/Models/EncodeOptions.cs (1 hunks)
  • QsNet/Qs.cs (2 hunks)
  • QsNet/QsNet.csproj (1 hunks)
💤 Files with no reviewable changes (1)
  • QsNet.Tests/UtilsTests.cs
🧰 Additional context used
🧬 Code Graph Analysis (4)
QsNet/Models/EncodeOptions.cs (1)
QsNet/Internal/Utils.cs (1)
  • Equals (1100-1103)
QsNet/Models/DecodeOptions.cs (1)
QsNet/Internal/Utils.cs (1)
  • Equals (1100-1103)
QsNet/Qs.cs (2)
QsNet/Internal/Decoder.cs (2)
  • Decoder (16-458)
  • Dictionary (91-218)
QsNet/Internal/Utils.cs (9)
  • Utils (20-1089)
  • Merge (69-249)
  • Dictionary (505-646)
  • Dictionary (803-809)
  • Dictionary (816-825)
  • Dictionary (832-844)
  • Dictionary (893-896)
  • Dictionary (898-964)
  • Dictionary (992-1088)
QsNet/Internal/Decoder.cs (3)
QsNet/Internal/Utils.cs (8)
  • Regex (36-39)
  • Regex (51-54)
  • GeneratedRegex (41-42)
  • GeneratedRegex (56-57)
  • Utils (20-1089)
  • Apply (690-698)
  • IsEmpty (724-734)
  • InterpretNumericEntities (741-796)
QsNet/Enums/Sentinel.cs (2)
  • GetEncoded (50-58)
  • ToString (65-68)
QsNet/Models/DecodeOptions.cs (1)
  • GetDecoder (178-181)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Coverage (merged)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Codacy Security Scan
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (12)
QsNet/Compat/IsExternalInit.cs (1)

1-11: Good polyfill for init-only setters on netstandard2.0

The NETSTANDARD2_0-gated IsExternalInit is minimal, correctly scoped, and avoids impacting other targets. LGTM.

QsNet/Enums/ListFormat.cs (1)

44-48: Precomputed generators remove per-call allocations and clarify intent

Using static readonly delegates avoids re-allocating lambdas on each call to GetGenerator. Mapping via the switch is clean and preserves semantics. LGTM.

Also applies to: 58-61

QsNet/QsNet.csproj (1)

3-3: LGTM! Multi-target configuration correctly enables .NET Standard 2.0 support.

The change from <TargetFramework> to <TargetFrameworks> with net8.0;netstandard2.0 appropriately supports backward compatibility while maintaining modern .NET 8.0 support.

QsNet/Qs.cs (1)

64-89: LGTM! Proper compatibility handling for .NET Standard 2.0.

The conditional compilation correctly handles the tuple deconstruction limitation in .NET Standard 2.0 by using explicit kv.Key and kv.Value access instead of the tuple deconstruction pattern (key, value). The logic remains identical between both code paths.

QsNet/Internal/Decoder.cs (4)

15-19: LGTM! Correct class declaration for .NET Standard 2.0 compatibility.

The conditional compilation properly handles the class declaration, making it static for .NET Standard 2.0 (which lacks source generators) and partial for newer targets to support GeneratedRegex.


27-36: LGTM! Well-implemented regex caching for .NET Standard 2.0.

The implementation correctly provides a cached regex instance for .NET Standard 2.0 while using the more efficient GeneratedRegex for newer targets. The RegexOptions.Compiled flag is appropriately used for performance.


38-50: LGTM! Clean abstraction for Latin-1 encoding across targets.

The implementation provides a clean abstraction for Latin-1 encoding differences between targets. The IsLatin1 helper method correctly identifies Latin-1 encoding using code page 28591 for .NET Standard 2.0 and direct Encoding.Latin1 comparison for newer targets.


432-456: LGTM! Efficient case-insensitive string replacement for .NET Standard 2.0.

The custom ReplaceOrdinalIgnoreCase implementation is well-optimized with minimal allocations. It correctly uses StringBuilder lazily (only when replacements are found) and handles all edge cases properly.

QsNet/Internal/Utils.cs (4)

19-23: LGTM! Proper conditional class declaration.

The conditional compilation correctly handles the class declaration for different targets.


119-124: LGTM! Excellent optimization to avoid multiple enumeration.

Materializing the source enumerable to a list is a good defensive practice that prevents multiple enumeration of potentially expensive sequences while maintaining the same behavior.


365-366: LGTM! Good defensive null handling.

Using format.GetValueOrDefault() and the non-null assertion with str! after the null/empty check improves code clarity and null safety.


1091-1109: LGTM! Well-implemented reference equality comparer.

The ReferenceEqualityComparer is correctly implemented as a sealed singleton with proper use of RuntimeHelpers.GetHashCode for consistent hash codes based on object identity rather than value equality. This is essential for tracking visited objects in circular reference scenarios.

@techouse techouse merged commit 2edd331 into main Aug 17, 2025
5 of 7 checks passed
@techouse techouse deleted the feat/dotnetstandard2.0 branch August 17, 2025 10:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant