Skip to content

Conversation

fearthecowboy
Copy link
Contributor

@fearthecowboy fearthecowboy commented Sep 5, 2025

Description

Linear ticket: https://linear.app/buildwithfern/issue/FER-6267/fix-c-seed-failures

Fixes the C# generator failures for the examples fixture

Changes Made

The PR is broken into three commits:

  • Asis template changes
  • C# generator changes/dsg test generator change (two commits really...sigh)
  • regenerated examples - the best visible changes to the examples are in the
    seed/csharp-sdk/examples/readme-config/src/SeedApi.DynamicSnippets folder

Created disambiguation framework for generated C# types and namespaces:

The canonicalization/disambiguation framework tracks types and namespaces as they are
created and used, so that collisions can be detected early, and automatically adjust
the generated code to avoid conflicts. To this point, collsions in namespaces with names
of classes or conflicting namespaces will adjust the namespace by appending an underscore.

NOTE: this PR doesn't agressively add canonicalization to everywhere, it is being
incrementally added where changes are needed to support test cases. This can easily
be expanded in the future as needed.

It creates an in-memory registry of types, namespaces, typenames, and known identifiers
that can be checked against.

Notably:

  • Automatically adds the names of built-in types that are in implicitly included namespaces.
  • functions to identify if a typename is known or ambiguous.
  • functions to canonicalize ClassReference instances
  • functions to canonicalize namespaces (partial or full)
  • functions to resolve typenames and namespaces during collision avoidance.
  • functions to load/save the type registry so that the generator state can be preserved.
  • functions to load/save the generator state using dynamically imported file system functions
    with graceful fallback in browser environments.

To support typename/namespace disambiguation:

  • save the c# generator state at the end of creating the CSharpProject files so that
    the DSG can be aware of changed names to types and namespaces.

  • for each Class that gets created, the ClassReference for the type is tracked

  • rewrote ClassReference.writeInternal to handle dismbiguation when writing out the
    class reference into source code.

  • modified the Writer.stringifyImports be able to get the resolvedNamespace when writing
    out the using imports.

  • created a builtIn.ts file that contains the namespaces and typenames for c# built-in
    types so that the disambiguation can be aware of what names will be conflicting with
    built-in types. (this data was initially generated in c# using reflection)

    • created a static class called System that has properties for creating ClassReference
      instances for built-in types (will expand this later). This will help in the long term
      to ensure that the disambiguation code can identify what has been used.
  • Modified the dynamic snippet generation to load the generator state if it exists
    before generating any snippets.

Modified c# 'asis' templates:

  • Changed references to Exception to be globally explicit: global::System.Exception
    This was required to support models that use the name Exception and the new namespace
    disambiguation support in this PR isn't yet accessible to asis templates

  • modified the namespace declaration in the AsIs templates to get the test namespace
    from the variable context (testNamespace) rather than letting it string-smash the
    text together.

  • modified the getTestNamespace method to get the canonicalized namespace instead of
    blindly string-smashing the text together.

  • cleaned up the variable passing to the templates to allow additional properties to be
    passed in as template variables.

  • made the SdkGeneratorCLI build models before generating any other code to give types
    that are exposed to the user the least likely chance to be in conflict.

  • the SubPackageClientGenerator canonicalizes the ClassReference for the client class
    before generating the code.

  • the WrappedRequestGenerator canonicalizes the ClassReference for the request class
    before generating the code.

Resolved bugs and build errors that are exposed by the examples generator:

  • in the EndpointSnippetGenerator if a request body was marked optional and didn't have
    a value in the example, the generator would not generate a null (even though the parameter
    itself was not optional).

  • modified the DynamicSnippetsGeneratorContext to use the now-dynamically exapanding known
    identifier support.

  • fixed the DynamicTypeLiteralMapper to correctly generate the initializer for unions with
    discriminators. (it had been generating empty constructor calls)

  • the DynamicTypeLiteralMapper now knows when to fully qualify a type when it is using
    a known identifier.

  • When UnionGenerator and RootClientGenerator emits code to throw an Exception it
    now uses the fully qualified name of the Exception class when there is a name collision
    with a generated model.

  • fixed a bug in the HttpEndpointGenerator that would generate faulty code if the a response
    status code was duplicated

  • the XmlDocBlock file was getting a circular dependency, changed ClassReference to
    be type imported.

Minor quality changes:

  • fixed a c# warning when generating long types by changing the suffix to L instead of l

Current known cosmetic issues:

  • it appears that the JsonConverter type is being fully expanded because it thinks that
    there is an ambiant name collsion. Will be follow up.

  • nested types for Enums are being slightly more verbose when generating operator implicit
    methods. (they are including the enclosing type name in the method signature). Will follow up.

  • Updated versions.yml

Testing

  • regenerated seed fixtures
  • ran pnpm test:update
  • Given the size of this, I ended up running the test fixtures in a lot of different ways - seed:local and seed with --local and without) as during development I found a couple of cases where the behavior changed a bit between the two.


// persist the state of the generator to allow subsequent tools to know what was generated
await saveGeneratorState(
join(this.absolutePathToOutputDirectory, RelativeFilePath.of(".csharp-generator-state.json.user"))
Copy link
Member

Choose a reason for hiding this comment

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

what does the .user suffix mean here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

:D

I picked that because the .gitignore has an exclusion for that so that if the generator state was still on disk, the file wouldn't end up in the SDK PR when pushed up to github.

@@ -0,0 +1,593 @@
/**
* @fileoverview Type canonicalization utilities for C# code generation.
Copy link
Member

Choose a reason for hiding this comment

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

Looks like JSDoc mainly uses @file and @fileoverview is a synonym

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gah! Cursor betrayed me! (fixing...)

Copy link
Member

Choose a reason for hiding this comment

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

This module looks fantastic. Can we encapsulate most of this in a class and include tests for its functionality?
For now, we can keep storing the data in the module scope, but wrapped in an instance of the class.
In the future we could use a DI container to register and retrieve this data.

Comment on lines +246 to +256
// ensure that each response code is handled only once
const handled = new Set<number>();
for (const error of endpoint.errors) {
const errorDeclaration = this.context.ir.errors[error.error.errorId];
if (errorDeclaration == null || handled.has(errorDeclaration.statusCode)) {
continue;
}
handled.add(errorDeclaration.statusCode);
this.writeErrorCase(error, writer);
}

Copy link
Member

Choose a reason for hiding this comment

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

Nice!

Comment on lines +89 to +90
// remove the generator state from the directory, since it is no longer needed
unlink(generatorStatePath);
Copy link
Member

Choose a reason for hiding this comment

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

Is that necessary? I think it'd be easier to debug when not deleting this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking that cleaning up the file was prudent - but maybe not a big deal. I can remove that

Copy link
Member

Choose a reason for hiding this comment

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

It would be great if we could order the status code switch by the numerical value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok

Comment on lines +473 to +474
Metadata = new SeedExamples.Metadata(new Metadata.Html("metadata")),
CommonMetadata = new SeedExamples.Commons.Metadata
Copy link
Member

Choose a reason for hiding this comment

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

Why is FQN'ing necessary here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are two Metadata types - but you're correct it does seem like it's a smidgen more cautious here than was absolutely necessary.

I was thinking of following up later with a PR after doing some comparisons with dotnet format so I could find all the places that the generator is too cautious with using fully qualified names all over the place. There are a number of hardcoded assumptions sprinkled throughout the generator that it would be nice to nudge into using the canonicalization code and solve all at once.

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

Successfully merging this pull request may close these issues.

2 participants