Skip to content

feat(csharp): add C# code generation target#1098

Open
fdrstrok wants to merge 5 commits into
grafana:mainfrom
fdrstrok:feat/csharp-codegen
Open

feat(csharp): add C# code generation target#1098
fdrstrok wants to merge 5 commits into
grafana:mainfrom
fdrstrok:feat/csharp-codegen

Conversation

@fdrstrok

@fdrstrok fdrstrok commented May 1, 2026

Copy link
Copy Markdown

Summary

Adds C# as a new code-generation target to cog, sitting alongside Go, Java, PHP, Python, TypeScript, Terraform, JSON Schema and OpenAPI. The implementation closely mirrors the Java jenny — the closest existing OO/strongly-typed analogue — and produces idiomatic .NET 10 C# code with nullable reference types enabled.

This is the foundational work to enable a future C# Grafana Foundation SDK.

What's generated

For each schema, the new jenny emits:

  • Raw typesclass, enum and constant declarations under src/<NamespaceRoot>/<Package>/<Type>.cs. Default namespace root is Grafana.Foundation, configurable via namespace_root.
  • System.Text.Json converters (opt-in via generate_json_converters: true) — per-field [JsonPropertyName], per-class [JsonConverter(typeof(<T>JsonConverter))], per-string-enum [JsonConverter(typeof(JsonStringEnumConverter<T>))] + [JsonStringEnumMemberName(...)]. Disjunction shapes (refs/scalars/mixed) get bespoke JsonConverter<T> classes mirroring Java's couldBe semantics.
  • Fluent builders (gated on the global output.builders flag) — public class <Name>Builder : Cog.IBuilder<TargetType> with chainable PascalCase option methods, constructor + property fields, nil-checks for nested parents, builder-typed args via Cog.IBuilder<T> / List<Cog.IBuilder<T>> / Dictionary<string, Cog.IBuilder<T>> unfolded into plain collections via foreach + .Build(). Constraints (>=, <=, regex) throw System.ArgumentException.
  • Cog runtime — single Cog/IBuilder.cs with public interface IBuilder<out T> { T Build(); }. Skippable via skip_runtime: true (mutually exclusive with builders).

Composable slots render as Cog.Variants.<Variant> (the variants interface itself is a follow-up). The C# internal keyword is sidestepped via the verbatim identifier @internal for the wrapped instance field.

Pipeline config

output:
  languages:
    - csharp:
        namespace_root: 'Grafana.Foundation'   # default
        generate_json_converters: true
        generate_equality: false               # not yet implemented
        skip_runtime: false

The C# output is wired into config/foundation_sdk.tests.yaml so make tests / gen-tests exercises the full pipeline (16 generated .cs files committed under testdata/generated/src/).

Implementation notes

  • Layout: internal/jennies/csharp/ mirrors internal/jennies/java/. Templates live under templates/{types,marshalling,builders,runtime}/.
  • Type formatter: pre-formats every field/arg type in Go before rendering so the import map is fully populated when {{ .Imports }} is rendered. This is a deliberate deviation from Java (where formatType is called at render time and imports may be stale).
  • Cog references: written as Cog.X and resolved via parent-namespace lookup — no using Grafana.Foundation.Cog; is emitted.
  • Source layout: src/Grafana/Foundation/<Package>/<Type>.cs with a single .csproj for v1; per-package projects can be introduced later without touching paths or namespaces.

Each phase landed as a separate commit on this branch:

  1. 371db9ad — skeleton & wiring (Language interface, OutputLanguage.CSharp, pipeline switch)
  2. 8326264d — raw types jenny (89 golden files across 28 fixtures)
  3. 3fa01876 — System.Text.Json marshalling
  4. 909db276 — fluent builders + Cog.IBuilder<T> runtime (28 builder fixtures + smoke test)
  5. b092023d — SDK test pipeline wiring + docs

What's not in this PR (deferred)

  • Option converters (Java-style object-to-builder-source codegen, used for "dashboards as code" import). Disabled by default in Java too; will be a follow-up.
  • Equals / GetHashCode jenny behind generate_equality.
  • Cog.Variants.<X> interface generation — currently referenced but not emitted.
  • Generic-panel hierarchy (Java's <T extends FooBuilder<T>> for Dashboard.Panel / VizConfigKind / DataQueryKind).
  • Foundation-SDK sibling-repo wiring (examples/csharp, prepare-release.sh hooks). scripts/ci/build-csharp.sh is added as a placeholder.

Testing

  • go test ./... is green.
  • internal/jennies/csharp/{rawtypes,converters,builder,runtime}_test.go cover all jennies with golden files under testdata/jennies/{rawtypes,builders,serializers,deserializers}/.
  • End-to-end make gen-tests produces a clean testdata/generated/src/ tree (committed).
  • The generated C# has not been compiled with dotnet build in this repo (no .NET toolchain in CI yet); spot-check by hand looks good. A real csproj + dotnet build step belongs in the foundation-sdk CI alongside scripts/ci/build-csharp.sh.

@fdrstrok fdrstrok requested a review from a team as a code owner May 1, 2026 00:20
@cla-assistant

cla-assistant Bot commented May 1, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@cla-assistant

cla-assistant Bot commented May 1, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


Copilot seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

Phase 1 of C# code generation support: add the package skeleton
and wire it into the codegen pipeline. No jennies are registered
yet, so configuring a csharp output produces no files; this commit
only establishes the seam for subsequent phases (raw types,
JSON marshalling, builders).

- Add internal/jennies/csharp with Config, Language (Name/Jennies/
  CompilerPasses/NullableKinds) and a pascalCase helper. Defaults
  target .NET 10 with namespace root Grafana.Foundation and source
  layout src/Grafana/Foundation/<Package>/<Type>.cs.
- Register csharp.Config on codegen.OutputLanguage (yaml: csharp)
  and dispatch it from Pipeline.OutputLanguages().
- Compiler passes mirror the Java jenny as the closest analogue.
Phase 2 of C# code generation: emit one .cs file per top-level
schema object (struct, enum, ref-alias, intersection) plus a
single Constants.cs per package collecting concrete scalar
values. Output layout is src/<NamespaceRoot>/<Package>/<Type>.cs
with namespaces of the form <NamespaceRoot>.<Package>.

What is generated:

- Structs: a public class with public PascalCased fields, a
  parameterless constructor that assigns required-field defaults,
  and an all-args constructor.
- Refs at the top level: a thin subclass of the referenced type.
- Intersections: a subclass extending each referenced branch and
  declaring the union of fields not already inherited.
- Enums: a strongly-typed C# enum. Integer enums use explicit
  numeric values; string enums carry [EnumMember(Value="...")]
  attributes (consumed by the JsonStringEnumMemberConverter that
  Phase 3 will add).
- Concrete scalars: aggregated into a public static Constants
  class with `public const` fields.

Implementation notes:

- types.go contains a typeFormatter that renders ast.Type as a
  C# type expression (List<T>, Dictionary<K,V>, refs, scalars,
  enums, constant refs) and records the namespaces it touches on
  the per-file importMap.
- imports.go is a small importMap that emits sorted, deduplicated
  `using` directives and prefixes schema-package imports with the
  configured namespace root (System.* namespaces are passed
  through unchanged).
- Field types are pre-formatted in Go before the template runs so
  the importMap is fully populated by the time the {{ .Imports }}
  block is rendered.
- common/codejen.go: add `.cs` to the GeneratedCommentHeader
  switch so generated C# files get the "Code generated" header.
- Golden tests under testdata/jennies/rawtypes/*/CSharpRawTypes/
  cover every scenario the Java jenny is exercised against
  (scalars, enums, structs with defaults/optional/complex fields,
  refs, intersections, disjunctions-as-classes, intersections,
  arrays, maps, time hints, variants, dashboard, etc.).

Out of scope for this phase:
- System.Text.Json converters (Phase 3)
- Equals / GetHashCode overrides
- Builders, factories, runtime helpers (Phase 4)
Adds opt-in JSON marshalling support behind Config.GenerateJSONConverters.

- Raw types now decorate fields with [JsonPropertyName], string enums
  with [JsonConverter(typeof(JsonStringEnumConverter<T>))] and
  per-member [JsonStringEnumMemberName], and disjunction-derived
  structs with [JsonConverter(typeof(<T>JsonConverter))]. All
  attributes are gated on the new flag, leaving the plain-POCO
  baseline (Phase 2) unaffected.
- Disjunction-derived structs use nullable value-type fields and skip
  default assignments only when JSON marshalling is enabled, so the
  null-as-absent sentinel matches the converter's expectations.
- New Converters jenny emits <T>JsonConverter.cs for the three
  disjunction shapes (scalars, discriminated refs, scalars+refs).
  Numeric branches collapse into a single Number case dispatched via
  TryGetInt64; the mixed scalars+refs converter parses to a
  JsonDocument and tries each ref branch with a TryDeserialize<T>
  helper, falling back to an 'any' catch-all when present.
- Templates: new templates/marshalling/*.tmpl, enum/class templates
  extended with optional Imports/Attributes blocks.
- Tests: TestConverters_Generate added against
  testdata/jennies/serializers/. TestRawTypes_Generate goldens
  refreshed to drop the stale [EnumMember] attribute (System.Text.Json
  doesn't honour it without a converter).
Adds opt-in builder generation behind languages.Config.Builders.

Runtime
- New Runtime jenny emits <NamespaceRoot>/Cog/IBuilder.cs:
    public interface IBuilder<out T> { T Build(); }
  No further runtime is required for v1; converters live in Phase 5.

Builder jenny
- Mirrors the Java jenny: one CSharpBuilder file per ast.Builder at
  <ProjectPath>/<Pkg>/<Name>Builder.cs, implementing
  Cog.IBuilder<TargetType>.
- Per-option fluent methods (PascalCase), parameterless constructor
  with builder.Constructor.Args, property fields, constraints
  (>=/<=/regex via System.Text.RegularExpressions), nil-checks for
  nested parent initialisation, and builder-typed args/maps/arrays
  unfolded into plain List<T>/Dictionary<...> via foreach loops that
  call .Build() on each element.
- Builder-typed arg slots are formatted as Cog.IBuilder<T>,
  List<Cog.IBuilder<T>>, or Dictionary<string, Cog.IBuilder<T>>;
  composable slot variants render as Cog.Variants.<Variant>.
- internal is a C# keyword so the wrapped instance field uses the
  verbatim identifier @internal — keeps generated source readable
  and avoids forced renames at the call sites.
- Cog.* references rely on parent-namespace lookup
  (<NamespaceRoot>.Cog is a sibling of every package), so no extra
  using directives are emitted.

Plumbing
- types.go: added KindComposableSlot to formatFieldType (also
  refreshes one Phase-2 raw-types golden where Targets is now
  List<Cog.Variants.Dataquery> instead of List<object>); added
  builder helpers (formatBuilderFieldType, typeHasBuilder,
  resolvesToComposableSlot, formatAssignmentPath, formatFieldPath,
  formatPathIndex, formatRefType, emptyValueForTypeOpts).
- tmpl.go: embeds templates/builders + templates/runtime; registers
  panic-stubs for builder helpers in the formatting func map so the
  templates parse cleanly at init time and Builder.Generate
  overrides them per-call via Template.Funcs(...).
- jennies.go: registers Runtime + Builder when GenerateBuilders is
  set and SkipRuntime isn't.

Tests
- TestBuilder_Generate runs the standard goldens harness against
  testdata/jennies/builders/. All 28 fixtures regenerated.
- TestRuntime_Generate smoke-tests the IBuilder.cs runtime file.
- Adds a csharp output to config/foundation_sdk.tests.yaml so the
  end-to-end gen-tests run also exercises the C# jennies (raw types
  + JSON converters). Builders stay opt-in at the global level.
- Documents the csharp output in docs/pipelines/creating_pipeline.md
  alongside the other languages.
- Adds scripts/ci/build-csharp.sh as a placeholder build hook
  (mirrors build-java.sh) for the foundation-sdk repo to invoke.
@fdrstrok fdrstrok force-pushed the feat/csharp-codegen branch from b092023 to 6dcd121 Compare May 1, 2026 00:23
@fdrstrok

Copy link
Copy Markdown
Author

Friendly follow-up on this one 👋

Would you prefer:

  • proceeding with this PR as-is (with any tweaks you suggest),
  • me breaking it into smaller parts,
  • or closing/reworking it differently?

Happy to adjust — just want to align on the best next step for this repo.

@K-Phoen

K-Phoen commented Jun 16, 2026

Copy link
Copy Markdown
Member

Hello there!

Thanks for the PR, but while I'd love to have C# support in cog, my bandwidth is limited and I don't think now is a good time to add C# (and my C# knowledge is quite... dated 😅)

@konnta0

konnta0 commented Jun 24, 2026

Copy link
Copy Markdown

We have interest in using Foundation SDK from .NET, but maintaining a long-lived fork of both Cog and Foundation SDK would be difficult for adopters. From the project's perspective, what would need to be in place for C# support to become maintainable and potentially acceptable upstream?

@fdrstrok

Copy link
Copy Markdown
Author

Thanks @konnta0, that's the right question, and good to know about the bandwidth @K-Phoen. The aim is for this to be less work for you, not more.

The way I see it, C# support is two halves, and the first is already done in this PR.

The codegen half (done here). It follows the Java jenny closely and emits .NET with nullable reference types: raw types, opt-in System.Text.Json converters (including disjunction handling that mirrors Java's couldBe), fluent builders, and a small Cog.IBuilder runtime. Every jenny has golden-file tests, it's wired into the SDK test pipeline, and "go test ./..." is green. All of it sits under internal/jennies/csharp/ and is opt-in via pipeline config, so no existing target changes.

The maintainability half (what I'm signing up for):

  1. Add a .NET SDK to devbox and wire scripts/ci/build-csharp.sh to dotnet build the generated output. Once that's a required check, a red build is the signal and nobody has to read C# to review a change, same as gradle build does for Java.

  2. Do the grafana-foundation-sdk side: runtime package, examples/csharp, NuGet packaging, and the prepare-release.sh / run-examples.sh hooks so it rides the existing workflow.

  3. Land the deferred parity items as small follow-ups: option converters, Equals/GetHashCode, Cog.Variants emission, and the generic-panel hierarchy.

  4. Own it. A CODEOWNERS entry on internal/jennies/csharp/, keeping it in sync as the IR evolves, and triaging C# issues so they don't land on you. @konnta0, given your .NET use case, I'd be glad to have you co-maintain.

The branch is already split into reviewable commits (skeleton, raw types, marshalling, builders, pipeline wiring), so I can break it into smaller PRs if that's easier. And if you'd rather keep C# experimental and undocumented until the CI build and SDK wiring are proven, that's fine by me. It lets people like @konnta0 drop their fork without committing the project to anything early.

No rush on timing. Happy to start wherever lowers the risk most for you, whether that's the dotnet build check or splitting this up.

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.

4 participants