Skip to content

feat(csharp): read idempotency-key generation config from the IR#17010

Open
devin-ai-integration[bot] wants to merge 4 commits into
mainfrom
devin/1783699276-csharp-idempotency-key
Open

feat(csharp): read idempotency-key generation config from the IR#17010
devin-ai-integration[bot] wants to merge 4 commits into
mainfrom
devin/1783699276-csharp-idempotency-key

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Description

Switches the C# SDK generator from its own auto-generate-idempotency-key config flag to reading the centralized sdkConfig.idempotencyKeyGeneration block from the IR (added + threaded by the CLI in #17018, published as @fern-fern/ir-sdk@67.11.0).

The behavior is unchanged from a caller's perspective: when idempotency-key generation is enabled, the generated SDK attaches a UUIDv4 idempotency-key header on eligible HTTP methods, unless the caller supplies one. The difference is where the config lives: enablement, header name, and the eligible-method set now come from the IR instead of being defined per-generator and hardcoded to POST/PUT. This is the C# generator half of the IR centralization (PR2); #17018 was PR1, and #17007 is the TypeScript reference.

// before: per-generator flag + hardcoded methods
settings.autoGenerateIdempotencyKey && (method === "POST" || method === "PUT")

// after: read from IR
const gen = this.ir.sdkConfig.idempotencyKeyGeneration;
const headerName = gen?.headerName ?? "Idempotency-Key";
const enabled = gen != null && gen.methods.includes(endpoint.method);

Changes Made

  • GeneratorContext.ts: added shouldAutoGenerateIdempotencyKey(endpoint) (true iff ir.sdkConfig.idempotencyKeyGeneration != null and .methods includes the endpoint's method) and getIdempotencyKeyGenerationHeaderName() (from .headerName, defaulting to Idempotency-Key). Enablement + eligible methods + header name all come from the IR.
  • Header injection unchanged in shape across all four header-builder paths (HttpEndpointGenerator, WrappedEndpointRequest, ReferencedEndpointRequest, BytesOnlyEndpointRequest): a fallback .Add(<headerName>, global::System.Guid.NewGuid().ToString()) emitted before the declared idempotency headers and request-option headers so a caller-supplied value wins.
  • Removed the per-generator auto-generate-idempotency-key config plumbing (custom-config schema key + autoGenerateIdempotencyKey threading through generation-info). The CLI now owns the auto-generate-idempotency-key config key and threads it into the IR.
  • Bumped @fern-fern/ir-sdk to 67.11.0 (contains idempotencyKeyGeneration with methods) and reconciled pnpm-lock.yaml.
  • fix(seed): made the CLI-only-key threader (getIdempotencyKeyGenerationFromGeneratorConfig) fall back to the resolved generatorInvocation.config when raw is absent, so the seed harness's synthetic invocations thread the IR field. Production (where raw is always populated) is unchanged. Added unit tests. (Brought in verbatim from the TS reference branch.)
  • Added an unreleased feat changelog entry, reworded to describe the IR-driven approach.
  • Updated README.md generator (if applicable) — N/A

Seed fixture proof

  • idempotency-headers fixture (no-custom-config baseline + auto-generate-idempotency-key) drives the feature via the CLI key (auto-generate-idempotency-key: true); regenerated flag-on output injects the fallback UUID header on the declared POST endpoint, flag-off has none.
  • exhaustive/auto-generate-idempotency-key variant proves the standalone path across the full API.
  • Committed fixture .fern/metadata.json normalized to canonical field order with invokedBy: ci / ciProvider: github to match sibling committed fixtures.

Injection confirmation:

seed/csharp-sdk/idempotency-headers/auto-generate-idempotency-key/src/SeedIdempotencyHeaders/Payment/PaymentClient.cs:27:            .Add("Idempotency-Key", global::System.Guid.NewGuid().ToString())

Testing

  • Unit tests added/updated — csharp-base, csharp-codegen, csharp-sdk vitest suites pass; commons idempotency-threading tests pass.
  • Manual testing completed — pnpm seed test --generator csharp-sdk --fixture idempotency-headers --local --skip-scripts → 2/2 pass; --fixture exhaustive → 8/8 pass. biome format clean.

Held for Twilio design-doc sign-off before merge.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-07-13T05:08:13Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 76s (n=5) 115s (n=5) 66s -10s (-13.2%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-07-13T05:08:13Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-07-13 22:36 UTC

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration Bot force-pushed the devin/1783699276-csharp-idempotency-key branch from 61bd617 to 9444cd4 Compare July 10, 2026 21:29
@devin-ai-integration devin-ai-integration Bot changed the title feat(csharp): auto-generate idempotency key for POST/PUT feat(csharp): read idempotency-key generation config from the IR Jul 10, 2026
devin-ai-integration Bot and others added 2 commits July 13, 2026 17:36
…ve idempotency resolver conflict in favor of main (global api.settings support)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

3 similar comments
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

var _headers = await new SeedExhaustive.Core.HeadersBuilder.Builder()
.Add(_client.Options.Headers)
.Add(_client.Options.AdditionalHeaders)
.Add("Idempotency-Key", global::System.Guid.NewGuid().ToString())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we generate a C# extension method that is called AddIdempotencyHeader(), so the magic string "Idempotency-Key" is in a single location, and the new guid code as well?
This could be a separate file that is generated dynamically.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — generated a dedicated IdempotencyHeaderExtensions core file (emitted only when the IR enables auto-generation) so the "Idempotency-Key" magic string and the new-guid code live in a single generated location:

internal static class IdempotencyHeaderExtensions
{
    private const string IdempotencyKeyHeaderName = "Idempotency-Key";

    internal static HeadersBuilder.Builder AddIdempotencyHeader(this HeadersBuilder.Builder builder)
    {
        return builder.Add(IdempotencyKeyHeaderName, global::System.Guid.NewGuid().ToString());
    }
}

All four request-builder paths (HttpEndpointGenerator, WrappedEndpointRequest, ReferencedEndpointRequest, BytesOnlyEndpointRequest) now call .AddIdempotencyHeader() instead of repeating .Add("Idempotency-Key", global::System.Guid.NewGuid().ToString()). The configured header name (custom names from the IR) is baked into IdempotencyKeyHeaderName. Merge order is preserved (.AddIdempotencyHeader() before the caller/declared headers), so a caller-supplied value still wins.

…IdempotencyHeader helper

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant