Skip to content

Consolidate parameter extraction - Remove IParameterTypeExtractor#1136

Merged
christianhelle merged 4 commits into
mainfrom
feat/parameter-extraction-consolidation
Jun 14, 2026
Merged

Consolidate parameter extraction - Remove IParameterTypeExtractor#1136
christianhelle merged 4 commits into
mainfrom
feat/parameter-extraction-consolidation

Conversation

@christianhelle

@christianhelle christianhelle commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Changes

Commit 1: Remove CanExtract() from IParameterTypeExtractor

  • Remove unused \CanExtract(OpenApiParameterKind)\ method from interface and all 4 implementations
  • Remove \CanExtract_*\ tests from FormParameterExtractorTests

Commit 2: Convert QueryParameterExtractor to IParameterTypeExtractor

  • Make QueryParameterExtractor a class implementing IParameterTypeExtractor
  • Move dynamic querystring logic (UseDynamicQuerystringParameters) into Extract()
  • Expose DynamicQuerystringParameterType and DynamicQuerystringCode properties

Commit 3: Delete ParameterExtractor.cs and rewrite tests

  • Delete the entire ParameterExtractor.cs (reflection compatibility shim with 20+ private relay methods)
  • Rewrite ParameterExtractorPrivateCoverageTests to test through public ParameterShared and ParameterAggregator APIs instead of BindingFlags.NonPublic reflection
  • Update IdentifierCorrectnessTests to use ParameterShared directly

Verification

  • All 2148 tests pass (down from 2150 due to removal of CanExtract tests)
  • Build succeeds with 0 errors
  • No behavioral changes — the extractors produce identical output, verified by existing scenario tests

User Stories Addressed

  1. ✅ New extractors only need to implement IParameterTypeExtractor and register in GetDefaultExtractors()
  2. ✅ Query parameters testable through same IParameterTypeExtractor seam
  3. ✅ ExtractParameters() is now a uniform pipeline: every kind uses GetExtractor().Extract(...)
  4. ✅ Tests use public/internal API, no BindingFlags.NonPublic required
  5. ✅ CanExtract() removed — no dead code paths in the interface

Summary by CodeRabbit

Summary

  • Refactor
    • Streamlined internal parameter extraction by removing redundant “can extract” checks across route, header, form, and body parameters.
    • Refined query parameter generation so dynamic query string parameter support is applied consistently (including appropriate fallback behavior).
  • Tests
    • Updated and expanded parameter extraction tests to match the new query/dynamic-query handling and default-value formatting expectations.

…ic API

- Delete ParameterExtractor.cs (reflection compatibility shim)
- Update IdentifierCorrectnessTests to use ParameterShared directly
- Rewrite ParameterExtractorPrivateCoverageTests to test through public
  ParameterShared and ParameterAggregator APIs instead of reflection
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e6613296-a268-41ef-8da2-1eca3ab15563

📥 Commits

Reviewing files that changed from the base of the PR and between ffa3bd6 and 4f65ae4.

📒 Files selected for processing (1)
  • src/Refitter.Tests/Parameters/ParameterExtractorPrivateCoverageTests.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Refitter.Tests/Parameters/ParameterExtractorPrivateCoverageTests.cs

📝 Walkthrough

Walkthrough

This PR refactors parameter extraction logic by removing the CanExtract(...) kind-filtering pattern from extractors, converting QueryParameterExtractor to instance-based extraction with integrated dynamic querystring support, and updating ParameterAggregator to delegate extraction. Tests are rewritten to use public APIs directly instead of reflection.

Changes

Parameter Extraction Architecture Refactoring

Layer / File(s) Summary
Remove kind-filtering contract from interface and extractors
src/Refitter.Core/IParameterTypeExtractor.cs, src/Refitter.Core/BodyParameterExtractor.cs, src/Refitter.Core/FormParameterExtractor.cs, src/Refitter.Core/HeaderParameterExtractor.cs, src/Refitter.Core/RouteParameterExtractor.cs, src/Refitter.Tests/FormParameterExtractorTests.cs
Removed CanExtract(OpenApiParameterKind kind) method from IParameterTypeExtractor interface and all concrete extractor implementations. Deleted corresponding CanExtract unit tests from FormParameterExtractorTests. Extractors now use instance-based Extract(...) without kind-filtering dispatch.
Refactor QueryParameterExtractor to instance-based extraction
src/Refitter.Core/QueryParameterExtractor.cs
Converted from internal static class with ExtractSimple(...) to internal instance class implementing IParameterTypeExtractor. Added DynamicQuerystringParameterType and DynamicQuerystringCode properties. New public Extract(...) method integrates dynamic querystring generation (via DynamicQuerystringParameterBuilder when enabled with 2+ query parameters) and simple per-parameter extraction, returning appropriately nullable wrapper or individual parameter declarations.
Update ParameterAggregator to delegate query extraction
src/Refitter.Core/ParameterAggregator.cs
ExtractParameters now instantiates QueryParameterExtractor, sets DynamicQuerystringParameterType, and delegates query extraction to its Extract(...) method. Removed old in-aggregator ExtractQueryParameters(...) logic. Updated GetDefaultExtractors to explicitly include QueryParameterExtractor alongside RouteParameterExtractor.
Replace reflection-based tests with direct ParameterShared calls
src/Refitter.Tests/Parameters/ParameterExtractorPrivateCoverageTests.cs
Refactored test methods to call ParameterShared public helpers directly instead of using reflection: GetDefaultValueForParameter, FormatDefaultValue, EscapeString, FormatNumericValue, IsNumericType, GetAliasAsAttribute, GetCSharpType, GetIntegerTypeName, GetArrayType, ReplaceUnsafeCharacters. Tests validate the same behaviors through now-public API surface. Removed InvokePrivate reflection helper.
Update query extraction and aggregation tests to use new instance APIs
src/Refitter.Tests/Parameters/ParameterExtractorPrivateCoverageTests.cs
Renamed and rewrote GetQueryParameters_... tests to QueryParameterExtractor_..., instantiating extractor directly and calling Extract(...). Updated ParameterAggregator_ExtractParameters_... and non-mutation tests to use public instance API instead of reflection. Tests verify dynamic querystring wrapper generation controlled by UseDynamicQuerystringParameters and nullable parameter handling. Added new tests for empty queries and single-parameter fallback behavior.
Update IdentifierCorrectnessTests to use direct API
src/Refitter.Tests/Scenarios/IdentifierCorrectnessTests.cs
Refactored GetDefaultValueForParameter_Uses_Sanitized_Variable_Name_Fallback test to call ParameterShared.GetDefaultValueForParameter directly instead of using reflection to invoke private static ParameterExtractor.GetDefaultValueForParameter.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • christianhelle/refitter#1125: Continues the parameter-extractor refactoring by removing CanExtract(...) and updating ParameterAggregator/QueryParameterExtractor to use the new instance-based dynamic querystring extraction flow.

Poem

🐰 No more kind-checking guards,
Just ask extractors to extract—
QueryParam takes the stage with flair,
Dynamic wrappers built with care,
And tests now dance with public APIs! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Consolidate parameter extraction - Remove IParameterTypeExtractor' accurately describes the main objective of removing the CanExtract() method and consolidating the parameter extraction architecture.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/parameter-extraction-consolidation

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/Refitter.Core/ParameterAggregator.cs (1)

33-36: ⚖️ Poor tradeoff

Mutable state on shared extractor instance may cause issues under concurrent use.

GetExtractor<QueryParameterExtractor>() returns the same instance from the shared extractors list. Setting DynamicQuerystringParameterType on this shared instance before calling Extract() means concurrent calls to ExtractParameters on the same ParameterAggregator could race, causing one call's type name to be overwritten by another.

If ParameterAggregator is always instantiated per-operation or the codebase is single-threaded, this is not an immediate problem. However, the pattern couples extraction correctness to external synchronization guarantees.

🔧 Alternative: instantiate a fresh QueryParameterExtractor per call
         // Query parameters (with dynamic querystring support)
-        var queryExtractor = GetExtractor<QueryParameterExtractor>();
+        var queryExtractor = new QueryParameterExtractor();
         queryExtractor.DynamicQuerystringParameterType = dynamicQuerystringParameterType;
         allParameters.AddRange(queryExtractor.Extract(operationModel, operation, settings));
         dynamicQuerystringParameters = queryExtractor.DynamicQuerystringCode;

This would make QueryParameterExtractor no longer need to be in GetDefaultExtractors(), simplifying the design.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Refitter.Core/ParameterAggregator.cs` around lines 33 - 36, The shared
QueryParameterExtractor instance is mutated via its
DynamicQuerystringParameterType before calling Extract(), which can race when
ExtractParameters is called concurrently; fix by creating a fresh extractor per
call instead of using GetExtractor<QueryParameterExtractor>() — e.g. replace the
GetExtractor<QueryParameterExtractor>() call with new QueryParameterExtractor(),
set its DynamicQuerystringParameterType, call Extract(operationModel, operation,
settings) and read DynamicQuerystringCode from that instance; also remove
QueryParameterExtractor from GetDefaultExtractors() (or keep it but note it’s no
longer used) so the extractor is not treated as shared mutable state.
src/Refitter.Core/QueryParameterExtractor.cs (1)

20-23: 💤 Low value

Consider guarding against null DynamicQuerystringParameterType before entering dynamic extraction.

When settings.UseDynamicQuerystringParameters is true and there are 2+ query parameters, ExtractDynamic is called. Line 32 uses DynamicQuerystringParameterType! with a null-forgiving operator, but the property is nullable and the branch condition doesn't verify it's set.

Currently ParameterAggregator always sets this property before calling Extract(), so production usage is safe. However, if QueryParameterExtractor is instantiated and used directly (e.g., in tests) without setting DynamicQuerystringParameterType, this could pass a null to DynamicQuerystringParameterBuilder.Build().

🛡️ Optional: Add a guard clause
         if (settings.UseDynamicQuerystringParameters && queryParameters.Count >= 2)
+        {
+            if (string.IsNullOrWhiteSpace(DynamicQuerystringParameterType))
+                throw new InvalidOperationException($"{nameof(DynamicQuerystringParameterType)} must be set before extracting dynamic querystring parameters.");
             return ExtractDynamic(queryParameters, settings);
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Refitter.Core/QueryParameterExtractor.cs` around lines 20 - 23, The code
calls ExtractDynamic when settings.UseDynamicQuerystringParameters is true but
does not verify settings.DynamicQuerystringParameterType is non-null; update
QueryParameterExtractor so either the initial branch checks that
settings.DynamicQuerystringParameterType != null before calling ExtractDynamic
(falling back to ExtractSimple or throwing a clear ArgumentException), or add a
null-check at the start of ExtractDynamic that throws an
ArgumentException/InvalidOperationException with a descriptive message;
reference the nullable property Settings.DynamicQuerystringParameterType, the
method QueryParameterExtractor.ExtractDynamic, and the public entry
QueryParameterExtractor.Extract so callers (including tests) get a clear failure
rather than passing null into DynamicQuerystringParameterBuilder.Build().
src/Refitter.Tests/Parameters/ParameterExtractorPrivateCoverageTests.cs (1)

261-275: ⚡ Quick win

Strengthen these delegation tests with behavior-level assertions.

Both tests can pass even if sanitization/reordering regresses (NotBeNullOrWhiteSpace / NotBeNull are too permissive). Please assert concrete invariants so failures are diagnostic.

Suggested test hardening
 public void ReplaceUnsafeCharacters_Delegates_To_ParameterShared()
 {
     var result = ParameterShared.ReplaceUnsafeCharacters("unsafe-name!");
-    result.Should().NotBeNullOrWhiteSpace();
+    result.Should().NotBeNullOrWhiteSpace();
+    result.Should().NotContain("-");
+    result.Should().NotContain("!");
+    result.Should().NotBe("unsafe-name!");
 }

 [Test]
 public void ReOrderNullableParameters_Delegates_To_OptionalParameterReorderer()
 {
     var parameterModels = new List<CSharpParameterModel>();
     var result = OptionalParameterReorderer.Reorder(
         new List<string> { "string a", "int? b = default" },
         new RefitGeneratorSettings(),
-        parameterModels);
+        parameterModels).ToList();

-    result.Should().NotBeNull();
+    result.Should().ContainInOrder("string a", "int? b = default");
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Refitter.Tests/Parameters/ParameterExtractorPrivateCoverageTests.cs`
around lines 261 - 275, Replace the weak assertions with behavior-level checks:
for ParameterShared.ReplaceUnsafeCharacters assert the returned string is not
null/whitespace AND matches a safe-character pattern (e.g. Regex.IsMatch(result,
"^[A-Za-z0-9_\\-]+$")) and specifically does not contain the original unsafe
char(s) (e.g. Assert.False(result.Contains("!"))), and for
OptionalParameterReorderer.Reorder assert the returned sequence orders
nullable/optional params after non-optional ones (e.g. the "int? b = default"
entry appears at the end) and that the passed-in parameterModels is
updated/consistent with the returned list (e.g. count and mapped names/types
match expected), using ParameterShared.ReplaceUnsafeCharacters and
OptionalParameterReorderer.Reorder as the referenced symbols to locate the tests
to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/Refitter.Core/ParameterAggregator.cs`:
- Around line 33-36: The shared QueryParameterExtractor instance is mutated via
its DynamicQuerystringParameterType before calling Extract(), which can race
when ExtractParameters is called concurrently; fix by creating a fresh extractor
per call instead of using GetExtractor<QueryParameterExtractor>() — e.g. replace
the GetExtractor<QueryParameterExtractor>() call with new
QueryParameterExtractor(), set its DynamicQuerystringParameterType, call
Extract(operationModel, operation, settings) and read DynamicQuerystringCode
from that instance; also remove QueryParameterExtractor from
GetDefaultExtractors() (or keep it but note it’s no longer used) so the
extractor is not treated as shared mutable state.

In `@src/Refitter.Core/QueryParameterExtractor.cs`:
- Around line 20-23: The code calls ExtractDynamic when
settings.UseDynamicQuerystringParameters is true but does not verify
settings.DynamicQuerystringParameterType is non-null; update
QueryParameterExtractor so either the initial branch checks that
settings.DynamicQuerystringParameterType != null before calling ExtractDynamic
(falling back to ExtractSimple or throwing a clear ArgumentException), or add a
null-check at the start of ExtractDynamic that throws an
ArgumentException/InvalidOperationException with a descriptive message;
reference the nullable property Settings.DynamicQuerystringParameterType, the
method QueryParameterExtractor.ExtractDynamic, and the public entry
QueryParameterExtractor.Extract so callers (including tests) get a clear failure
rather than passing null into DynamicQuerystringParameterBuilder.Build().

In `@src/Refitter.Tests/Parameters/ParameterExtractorPrivateCoverageTests.cs`:
- Around line 261-275: Replace the weak assertions with behavior-level checks:
for ParameterShared.ReplaceUnsafeCharacters assert the returned string is not
null/whitespace AND matches a safe-character pattern (e.g. Regex.IsMatch(result,
"^[A-Za-z0-9_\\-]+$")) and specifically does not contain the original unsafe
char(s) (e.g. Assert.False(result.Contains("!"))), and for
OptionalParameterReorderer.Reorder assert the returned sequence orders
nullable/optional params after non-optional ones (e.g. the "int? b = default"
entry appears at the end) and that the passed-in parameterModels is
updated/consistent with the returned list (e.g. count and mapped names/types
match expected), using ParameterShared.ReplaceUnsafeCharacters and
OptionalParameterReorderer.Reorder as the referenced symbols to locate the tests
to change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d951231f-7100-4ef6-8907-19bf6f5e1250

📥 Commits

Reviewing files that changed from the base of the PR and between 7d03c6a and ffa3bd6.

📒 Files selected for processing (11)
  • src/Refitter.Core/BodyParameterExtractor.cs
  • src/Refitter.Core/FormParameterExtractor.cs
  • src/Refitter.Core/HeaderParameterExtractor.cs
  • src/Refitter.Core/IParameterTypeExtractor.cs
  • src/Refitter.Core/ParameterAggregator.cs
  • src/Refitter.Core/ParameterExtractor.cs
  • src/Refitter.Core/QueryParameterExtractor.cs
  • src/Refitter.Core/RouteParameterExtractor.cs
  • src/Refitter.Tests/FormParameterExtractorTests.cs
  • src/Refitter.Tests/Parameters/ParameterExtractorPrivateCoverageTests.cs
  • src/Refitter.Tests/Scenarios/IdentifierCorrectnessTests.cs
💤 Files with no reviewable changes (7)
  • src/Refitter.Core/IParameterTypeExtractor.cs
  • src/Refitter.Core/FormParameterExtractor.cs
  • src/Refitter.Core/BodyParameterExtractor.cs
  • src/Refitter.Tests/FormParameterExtractorTests.cs
  • src/Refitter.Core/HeaderParameterExtractor.cs
  • src/Refitter.Core/ParameterExtractor.cs
  • src/Refitter.Core/RouteParameterExtractor.cs

@codecov

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.46154% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.57%. Comparing base (78d783b) to head (4f65ae4).
⚠️ Report is 19 commits behind head on main.

Files with missing lines Patch % Lines
src/Refitter.Core/QueryParameterExtractor.cs 85.71% 0 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1136      +/-   ##
==========================================
- Coverage   94.73%   94.57%   -0.17%     
==========================================
  Files          39       38       -1     
  Lines        2658     2597      -61     
==========================================
- Hits         2518     2456      -62     
+ Misses         46       44       -2     
- Partials       94       97       +3     
Flag Coverage Δ
unittests 94.57% <88.46%> (-0.17%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@christianhelle christianhelle self-assigned this Jun 14, 2026
@christianhelle christianhelle added enhancement New feature, bug fix, or request .NET Pull requests that contain changes to .NET code labels Jun 14, 2026
@christianhelle christianhelle changed the title feat: consolidate parameter extraction — remove phantom IParameterTypeExtractor seam Consolidate parameter extraction - remove phantom IParameterTypeExtractor seam Jun 14, 2026
@christianhelle christianhelle changed the title Consolidate parameter extraction - remove phantom IParameterTypeExtractor seam Consolidate parameter extraction - Remove IParameterTypeExtractor Jun 14, 2026
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature, bug fix, or request .NET Pull requests that contain changes to .NET code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant