Skip to content

Enhance C# generator with richer typing and data annotations#1067

Merged
stephentoub merged 6 commits intomainfrom
stephentoub/enhance-type-generators-rich-typing
Apr 13, 2026
Merged

Enhance C# generator with richer typing and data annotations#1067
stephentoub merged 6 commits intomainfrom
stephentoub/enhance-type-generators-rich-typing

Conversation

@stephentoub
Copy link
Copy Markdown
Collaborator

The C# code generator (scripts/codegen/csharp.ts) emits types from JSON Schema but was leaving a lot of type information on the table -- using double for all numerics, string for dates and durations, no validation attributes, and concrete collection types in public APIs. This PR teaches the generator to produce idiomatic, well-annotated C# and applies the same patterns to the hand-written public API surface.

What changed

Richer type mappings in the generator:

  • integer -> long (JSON Schema integers can be up to 2^53, so int would be lossy)
  • number -> double (unchanged, but now distinct from integer)
  • format: "date-time" -> DateTimeOffset
  • format: "uuid" -> Guid
  • format: "duration" on numeric fields -> TimeSpan with a [JsonConverter] that converts milliseconds (the runtime's convention for duration fields differs from JSON Schema's ISO 8601 string definition -- see comment on isDurationProperty())

Data annotation attributes:

  • [Range] from minimum/maximum/exclusiveMinimum/exclusiveMaximum
  • [RegularExpression] from pattern
  • [Url] and [StringSyntax(StringSyntaxAttribute.Uri)] from format: "uri"
  • [StringSyntax(StringSyntaxAttribute.Regex)] from format: "regex"
  • [MinLength]/[MaxLength] from minLength/maxLength
  • [Base64String] from contentEncoding: "base64" or format: "byte" (no current schema fields use this yet, but the generator is wired up)

Collection interfaces in the public API:

  • All List<T> properties/returns/parameters -> IList<T>
  • All Dictionary<K,V> -> IDictionary<K,V>
  • Applied in both generated code and hand-written files (Types.cs, Client.cs, Session.cs)
  • Collection properties use lazy initialization (field ??= [] / field ??= new Dictionary<>()) instead of eager allocation

New file: dotnet/src/MillisecondsTimeSpanConverter.cs -- a standalone JsonConverter<TimeSpan> that serializes TimeSpan as milliseconds. Marked [EditorBrowsable(Never)] since it's an implementation detail.

Things to note

  • The session-events schema is relatively sparse -- it only has format: "date-time" and format: "uri", so SessionEvents.cs gets fewer annotations than Rpc.cs (which has ranges, patterns, durations, etc.).
  • Test changes are mechanical: explicit IList<T> types where Task<T> invariance requires it, new Dictionary<>() where [] doesn't work for IDictionary, and .FirstOrDefault() replacing List.Find().
  • The [DefaultValue] and [AllowedValues] attributes were investigated but not added -- the schemas have almost no default values and string enums are already mapped to C# enum types.

- integer -> long, number -> double (was double for both)
- format: date-time -> DateTimeOffset, uuid -> Guid, duration -> TimeSpan
- Add MillisecondsTimeSpanConverter for TimeSpan JSON serialization
- Emit [Range], [RegularExpression], [Url], [MinLength], [MaxLength]
- Emit [StringSyntax(Uri)], [StringSyntax(Regex)], [Base64String]
- Change all public collections from concrete to interface types
  (List<T> -> IList<T>, Dictionary<K,V> -> IDictionary<K,V>)
- Lazy-initialize collection properties via field ??= pattern

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@stephentoub stephentoub requested a review from a team as a code owner April 12, 2026 17:46
Copilot AI review requested due to automatic review settings April 12, 2026 17:46
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR upgrades the C# code generator and the .NET SDK surface to emit/consume richer types and more idiomatic collections, improving schema fidelity and public API flexibility across generated RPC/session-event models and hand-written types.

Changes:

  • Updated scripts/codegen/csharp.ts to map additional schema formats/types (e.g., integer→long, uri annotations) and to emit data-annotation attributes + duration handling hooks.
  • Switched many .NET public APIs/models from List<T>/Dictionary<K,V> to IList<T>/IDictionary<K,V> with lazy initialization patterns.
  • Added a TimeSpan JSON converter (milliseconds-based) and updated generated code + tests to match the new collection shapes.
Show a summary per file
File Description
scripts/codegen/csharp.ts Adds richer type mapping, annotation emission, and collection interface generation in the C# generator.
dotnet/src/MillisecondsTimeSpanConverter.cs Introduces a JsonConverter<TimeSpan> for millisecond-encoded durations.
dotnet/src/Types.cs Updates hand-written SDK types to use IList/IDictionary and lazy init; adjusts copy semantics.
dotnet/src/Client.cs Adjusts client API signatures to return/accept IList<T> and updates caching logic accordingly.
dotnet/src/Session.cs Updates internal request/response DTOs to use interface collections.
dotnet/src/Generated/SessionEvents.cs Regenerates session event models with updated integer typing and URI annotations.
dotnet/src/Generated/Rpc.cs Regenerates RPC models to use interface collections and updated numeric typing where applicable.
dotnet/test/SessionTests.cs Updates list search to LINQ (FirstOrDefault) to match interface collection return types.
dotnet/test/ElicitationTests.cs Uses concrete dictionary construction where interface-typed properties can’t use target-typed new().
dotnet/test/ClientTests.cs Updates explicit types to IList<T> where needed for invariance/compatibility.

Copilot's findings

Comments suppressed due to low confidence (1)

dotnet/src/Types.cs:1932

  • Same as SessionConfig: cloning McpServers via new Dictionary<string, McpServerConfig>(other.McpServers) drops any custom comparer on the original dictionary. If key comparison semantics matter (e.g., case-insensitive server names), preserve the comparer when possible (or explicitly define/normalize key casing).
        InfiniteSessions = other.InfiniteSessions;
        McpServers = other.McpServers is not null
            ? new Dictionary<string, McpServerConfig>(other.McpServers)
            : null;
  • Files reviewed: 9/11 changed files
  • Comments generated: 4

@github-actions

This comment has been minimized.

Preserve dictionary comparer in SessionConfig/ResumeSessionConfig
Clone() by checking for Dictionary<> and passing its Comparer.
Fix byok.md to use Task.FromResult<IList<ModelInfo>>() for the
updated delegate signature.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Use Range(typeof(long), ...) overload since RangeAttribute has no
long constructor. Add 'using GitHub.Copilot.SDK' to Rpc.cs header
so MillisecondsTimeSpanConverter resolves when duration fields exist.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

The Go SDK uses per-event-type data structs, so response.Data is a
SessionEventData interface. Access Content by type-asserting to
*copilot.AssistantMessageData.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Rpc.cs is in namespace GitHub.Copilot.SDK.Rpc, a child of
GitHub.Copilot.SDK, so types from the parent namespace resolve
automatically without an explicit using directive.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review ✅

All 13 changed files are scoped to the .NET SDK (dotnet/src/, dotnet/test/) plus the C# code generator and docs — no Python, Go, or TypeScript SDK code is modified.

Each improvement in this PR is either .NET-idiomatic or already handled correctly in the other SDKs:

Change Go Python TypeScript
integerlong (was double) Already uses int64 quicktype generates int No int/float distinction in TS ✓
format: uuidGuid string (idiomatic; no stdlib UUID type) ✓ str (idiomatic) ✓ string (idiomatic) ✓
format: date-timeDateTimeOffset Already uses time.Time quicktype generates datetime ✓ Library-handled ✓
format: durationTimeSpan Future gap: would get raw int64 ms Similar future gap Similar future gap
IList/IDictionary in public API .NET-specific idiom Not applicable Not applicable
Data annotations ([Range], [Url], ...) .NET-specific Not applicable Not applicable

The format: durationTimeSpan mapping is future-proofing the generator; no currently-generated properties actually use it (no TimeSpan fields appear in Rpc.cs or SessionEvents.cs). When duration fields do appear in the schema, it would be worth ensuring the Go generator maps them to time.Duration as well — but that's a separate concern.

No cross-SDK consistency issues with this PR. 👍

Generated by SDK Consistency Review Agent for issue #1067 · ● 784.8K ·

@stephentoub stephentoub merged commit ca93613 into main Apr 13, 2026
27 checks passed
@stephentoub stephentoub deleted the stephentoub/enhance-type-generators-rich-typing branch April 13, 2026 00:42
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.

2 participants