feat(csharp): add C# code generation target#1098
Conversation
|
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.
b092023 to
6dcd121
Compare
|
Friendly follow-up on this one 👋 Would you prefer:
Happy to adjust — just want to align on the best next step for this repo. |
|
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 😅) |
|
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? |
|
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):
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. |
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:
class,enumand constant declarations undersrc/<NamespaceRoot>/<Package>/<Type>.cs. Default namespace root isGrafana.Foundation, configurable vianamespace_root.System.Text.Jsonconverters (opt-in viagenerate_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 bespokeJsonConverter<T>classes mirroring Java'scouldBesemantics.output.buildersflag) —public class <Name>Builder : Cog.IBuilder<TargetType>with chainable PascalCase option methods, constructor + property fields, nil-checks for nested parents, builder-typed args viaCog.IBuilder<T>/List<Cog.IBuilder<T>>/Dictionary<string, Cog.IBuilder<T>>unfolded into plain collections viaforeach + .Build(). Constraints (>=,<=, regex) throwSystem.ArgumentException.Cog/IBuilder.cswithpublic interface IBuilder<out T> { T Build(); }. Skippable viaskip_runtime: true(mutually exclusive with builders).Composable slots render as
Cog.Variants.<Variant>(the variants interface itself is a follow-up). The C#internalkeyword is sidestepped via the verbatim identifier@internalfor the wrapped instance field.Pipeline config
The C# output is wired into
config/foundation_sdk.tests.yamlsomake tests/gen-testsexercises the full pipeline (16 generated.csfiles committed undertestdata/generated/src/).Implementation notes
internal/jennies/csharp/mirrorsinternal/jennies/java/. Templates live undertemplates/{types,marshalling,builders,runtime}/.{{ .Imports }}is rendered. This is a deliberate deviation from Java (whereformatTypeis called at render time and imports may be stale).Cog.Xand resolved via parent-namespace lookup — nousing Grafana.Foundation.Cog;is emitted.src/Grafana/Foundation/<Package>/<Type>.cswith a single.csprojfor v1; per-package projects can be introduced later without touching paths or namespaces.Each phase landed as a separate commit on this branch:
371db9ad— skeleton & wiring (Languageinterface,OutputLanguage.CSharp, pipeline switch)8326264d— raw types jenny (89 golden files across 28 fixtures)3fa01876— System.Text.Json marshalling909db276— fluent builders +Cog.IBuilder<T>runtime (28 builder fixtures + smoke test)b092023d— SDK test pipeline wiring + docsWhat's not in this PR (deferred)
Equals/GetHashCodejenny behindgenerate_equality.Cog.Variants.<X>interface generation — currently referenced but not emitted.<T extends FooBuilder<T>>forDashboard.Panel/VizConfigKind/DataQueryKind).examples/csharp,prepare-release.shhooks).scripts/ci/build-csharp.shis added as a placeholder.Testing
go test ./...is green.internal/jennies/csharp/{rawtypes,converters,builder,runtime}_test.gocover all jennies with golden files undertestdata/jennies/{rawtypes,builders,serializers,deserializers}/.make gen-testsproduces a cleantestdata/generated/src/tree (committed).dotnet buildin this repo (no .NET toolchain in CI yet); spot-check by hand looks good. A realcsproj+dotnet buildstep belongs in the foundation-sdk CI alongsidescripts/ci/build-csharp.sh.