feat(csharp): add C# support to verification and code generation#252
feat(csharp): add C# support to verification and code generation#252apoorv7g wants to merge 2 commits into
Conversation
- Introduced C# as a new target for code generation and verification processes. - Updated Docker configurations to include C# build and verification steps. - Enhanced the CSharpVisitor to support new features and ensure compatibility with the Concerto runtime. - Added tests to verify C# code generation and compilation using dotnet. - Updated package.json and workflows to integrate C# verification into the existing framework. This commit expands the capabilities of the project by enabling C# code generation and verification, ensuring a consistent development experience across multiple languages. Signed-off-by: Apoorv <130035517+APOORV7G@users.noreply.github.com>
- Changed the base image for the C# target from the .NET SDK to Node.js. - Added installation command for the .NET SDK using Alpine package manager. This update ensures the C# build environment is correctly set up for verification processes. Signed-off-by: Apoorv <130035517+APOORV7G@users.noreply.github.com>
|
You're making good progress @apoorv7g, although I think that I've misled you on the "Concerto Runtime" aspect. There's a difference between using the metamodel type definitions, and requiring the dotnet (de)serialization logic. For example, look at the concerto-playground, and see how many of the code targets actually include multiple files. In the csharp generator we shouldn't branch based on the presence of the runtime, it's up to the bundling application whether they want to use the runtime version of the metamodel, or just generate it again locally. Either way the csharp output should be the same? @ekarademir @DianaLease what are your thoughts? I also spotted a couple of breaking changes to the csharp output, so we'll need a major version bump when this is released. |
|
@mttrbrts Got it, Thanks. A follow up, For C# verification, should we compile generated output with the And how do I go about it, in what direction do you want me proceed ? I am a little lost. |
|
@apoorv7g I thought we discussed that if there are compile errors, we'd keep the errors for now and get the CI pipeline running with errors, not blocking, and address the errors later on. Was that not possible? |
ekarademir
left a comment
There was a problem hiding this comment.
Let's try to get the pipeline in place even if the compilation fails. Fixing the compilation errors won't be easy it looks like. So let's remove the "fixes" and just add the pipeline, making it non-blocking for merges.
| if (!otherModelFile) { | ||
| // Couldn't resolve the other model file. | ||
| parameters.fileWriter.writeLine(0, `using ${namespacePrefix}${namespace};`); | ||
| parameters.fileWriter.writeLine(0, `using ${namespacePrefix}${this.toCSharpNamespace(namespace, parameters)};`); |
There was a problem hiding this comment.
It was not a bug, it was additional check added for verification which enabled compilation without NuGet Package. The NuGet Package when used to compile the generated C# code gave the below errors:
error CS0234: The type or namespace name 'Type' does not exist in the namespace 'AccordProject.Concerto'
error CS0234: The type or namespace name 'ConcertoConverterFactorySystem' does not exist in the namespace 'AccordProject.Concerto'
Now I don't exactly know the root cause so as a workaround I added the check.
| * @returns {boolean} true when Concerto runtime attributes and namespaces should be emitted | ||
| * @private | ||
| */ | ||
| useConcertoRuntime(parameters) { |
There was a problem hiding this comment.
Looking at how this switch is being used, I'm not sure useConcertoRuntime is the correct naming here. You are removing references to the metamodel types. Emitted types by this library should work with those types.
There was a problem hiding this comment.
Okay, I understand, we can change the name. For verification, what’s the preferred setup? generate all metamodel files like playground, add a pinned NuGet reference, or both?
@ekarademir Apologies, Based on our discussion, my interpretation was to skip the six non necessary
Yes, based on the current conversation under this PR, I will focus on only adding the pipeline for |
|
For documentation purposes, #259 has added |
C# Code Generation and Verification
This document describes the C# verification work introduced in commit
fcaabbf(feat(csharp): add C# support to verification and code generation). It explains what changed, why each decision was made, and how the pieces fit together.The goal is to guarantee that generated C# from Concerto models compiles successfully under .NET 8, using the same verification pattern already established for TypeScript (
tsc) and JSON Schema (ajv).Table of contents
High-level summary
CSharpVisitorgained optional Concerto runtime emission, C# reserved-keyword escaping in namespaces, and fixes for DateTime/enum defaultstest/verification/csharp.compile.test.js- generates C# and runsdotnet buildfor 7 model fixturesverification/docker/csharp/image; base image symlink fix so CLI uses checkout codegenverification/templates/Verify.csproj- minimal net8.0 project for compile checks.github/workflows/verify-codegen.ymlmatrix extended withcsharpnpm run verify:docker:csharpand related entries inpackage.jsonC# as a codegen target already existed in this repository (see prior commits such as
#231,#234,#236). This change adds automated verification so regressions are caught in CI and locally, mirroring the TypeScript/JSON Schema pipeline from PR#246.Verification architecture
Verification follows a generate → compile pipeline:
There are two entry points that share concepts but differ in how codegen is invoked:
test/verification/csharp.compile.test.js)CSharpVisitorimport fromlib/npm run test:verify, pre-push local devverification/docker/csharp/entrypoint.sh)concerto compile --target CSharpCLInpm run verify:docker:csharp, GitHub ActionsBoth paths copy the same
Verify.csprojtemplate and rundotnet build.Shared test cases
Model fixtures are defined once in
test/verification/cases.js:metamodelconcerto compile --metamodelhr_basehr_base.ctobase(C# keyword)hr_integrationhr_base.cto+hr.ctostringlengthstringlength.ctomodel-basemodel-base.ctoagreementagreement.ctocircularcircular.ctoEnvironment flags applied to all cases (via
applyVerificationEnv()):ENABLE_MAP_TYPE=trueIMPORT_ALIASING=truePer-target skips are supported via
testCase.skip(string or{ csharp: 'reason' }).Docker corpus (currently smaller)
verification/corpus/manifest.jsoncurrently lists only the metamodel case. Docker CI validates the CLI path against the largest built-in model. The Mocha suite exercises the broader fixture set. Expanding the Docker corpus to mirrorcases.jsis a natural follow-up.CSharpVisitor changes
File:
lib/codegen/fromcto/csharp/csharpvisitor.jsThese changes support both production codegen (with optional Concerto .NET runtime) and verification (standalone POJOs that compile without external NuGet packages).
1. Optional Concerto runtime (
useConcertoRuntime)Problem: Generated C# previously always emitted attributes and types from the
AccordProject.ConcertoNuGet package:[AccordProject.Concerto.Type(...)][AccordProject.Concerto.Identifier()][AccordProject.Concerto.ConcertoConverterFactorySystem]using AccordProject.Concerto.Metamodelforconcerto.decoratortypesVerification builds use a bare
.csprojwith no package references. Referencing those types caused hundreds ofCS0234errors.Solution: Introduced
useConcertoRuntime(parameters):useConcertoRuntime: trueuseConcertoRuntime: falseorCSHARP_USE_CONCERTO_RUNTIME=falseSystem.Text.Jsonattributes where applicableRuntime-gated emission sites:
[AccordProject.Concerto.Type]and converter attributes[AccordProject.Concerto.Identifier()]concerto.decoratornamespace →AccordProject.Concerto.Metamodelmapping@DotNetNamespacedecorator handling (see below)Why default to runtime on: Existing CLI and production consumers expect Concerto-aware output. Verification explicitly opts out rather than changing the default developer experience.
2.
@DotNetNamespacedecorator scopeBefore: If a model file had
@DotNetNamespace("..."), that value was always used as the .NET namespace.After: The decorator is honoured only when
useConcertoRuntimeis true. In verification mode, namespaces are derived from the Concerto namespace viatoCSharpNamespace(), keeping output predictable and independent of decorator metadata that may target the runtime package layout.3. C# reserved keywords in namespace segments (
toCase)Problem: The Concerto namespace
org.acme.hr.basecontainsbase, a C# keyword.namespace org.acme.hr.base;is invalid C#.Solution: In
toCase(), non-PascalCase namespace components that matchreservedKeywordsare prefixed with_:Important distinction:
_base)Namespace = "org.acme.hr.base",$classFQNs) keep the original Concerto namespaceSnapshot updates in
test/codegen/__snapshots__/codegen.js.snapreflect this for thehr_base/hr_integrationfixtures.4. Unresolved import namespaces
When an imported namespace cannot be resolved to a model file, the
usingdirective now appliestoCSharpNamespace()so keyword escaping is consistent:`using ${namespacePrefix}${this.toCSharpNamespace(namespace, parameters)};`5. DateTime default literals
Problem: DateTime field defaults were emitted as raw strings, which is not valid C# for
System.DateTimeproperties.Solution:
formatDefaultLiteral()now handlesDateTime:6. Enum default literals
Problem: Enum defaults used
camelCasefor member names, which could produce invalid or inconsistent C# enum member references.Solution: Enum default values now use
toCSharpIdentifier()for both the enum type name and member name, respecting PascalCase settings and keyword escaping consistently with the rest of the visitor.Local Mocha verification tests
File:
test/verification/csharp.compile.test.jsDesign
Mirrors
test/verification/typescript.compile.test.js:cases.jsinto aModelManagerCSharpVisitorwithFileWriterinto a temp directoryVerify.csprojinto that directorydotnet build Verify.csproj --nologo -v qKey parameters
.NET SDK detection
Tests gracefully skip when .NET 8 is not installed locally:
dotnet --versiondotnet --list-sdksfor an8.xSDKit.skiprather than failing the suiteThis avoids blocking contributors who work on non-C# parts of the repo without the SDK installed.
Timeout
Suite timeout is
120000ms (2 minutes) becausedotnet buildon first run may restore tooling and compile multiple files.Docker-based verification (CI)
Docker verification ensures the Concerto CLI path (
concerto compile --target CSharp) produces compilable output, not just the programmatic visitor API used in Mocha.File layout
C# target Dockerfile
Why Alpine +
dotnet8-sdkpackage: Keeps the image small while matching the net8.0 target inVerify.csproj. The base image is Node Alpine; C# verification adds only the SDK layer.Why
CSHARP_USE_CONCERTO_RUNTIME=falsein the image: Docker uses the CLI, which does not passuseConcertoRuntime: falseas a parameter. The environment variable is the supported override for CLI-driven builds.C# entrypoint flow
For each case in
manifest.json:run-case.sh metamodel CSharp csharp /work/metamodel.csfiles were generatedVerify.csprojinto the output directorydotnet build /work/metamodel/Verify.csproj --nologo -v qBase Dockerfile: checkout codegen symlink (critical fix)
The base image installs
@accordproject/concerto-cliglobally and symlinks@accordproject/concerto-codegento the PR checkout at/opt/concerto-codegen.Problem discovered during development: Symlinking only the global
node_modules/@accordproject/concerto-codegenis insufficient. Node resolves dependencies fromconcerto-cli's nestednode_modulesfirst, so the CLI was still using the bundled 4.1.x visitor (which always emitted runtime attributes).Fix: Replace both copies:
This ensures PRs test the branch under review, not the published npm package - the same intent as the original TypeScript/JSON Schema verification design.
scripts/verification/docker-run.jsExtended
TARGETSarray:Builds base image → target image → runs container with corpus and work volume mounts. Supports filtering:
node scripts/verification/docker-run.js csharp.verification/docker/targets.jsonMetadata describing each verification target (CLI name, toolchain, verify command). Not consumed by build scripts today - it documents the intended layout for humans and future automation.
All targets share the same Node base image (
concerto-verify-base, built fromnode:20-alpine). Target-specific tools are added in per-target Dockerfiles:typescriptnpm install -g typescripttsc --noEmitjsonschemanpm install -g ajv-cliajv compilecsharpapk add dotnet8-sdkdotnet buildC# entry:
Why not
mcr.microsoft.com/dotnet/sdk:8.0-alpine? An earlier draft listed the official Microsoft SDK image as the C#baseImage. That would require a separate image without Node/concerto-cli, or a much larger multi-stage setup. The implemented design extends the shared Node base (same as TypeScript and JSON Schema) and installs .NET via Alpine'sdotnet8-sdkpackage for layer reuse and a single concerto-cli install.baseImagereflects that shared foundation;toolInstallrecords how .NET is added.Verify.csproj template
File:
verification/templates/Verify.csprojMinimal SDK-style project used only to answer: does the generated C# compile?
Design decisions
TargetFrameworknet8.0dotnet8-sdkNullabledisableNoWarn CS8618NoWarn CS8632string?on optional fields; with nullable disabled,?annotations trigger CS8632TreatWarningsAsErrorsfalseWhy not add
AccordProject.ConcertoNuGet for verification? That would only validate the runtime-enabled path. The dual-modeuseConcertoRuntimedesign intentionally supports both standalone POJOs and runtime-integrated output; verification focuses on the standalone path because it has no external dependencies and catches the widest set of C# syntax errors.CI workflow and npm scripts
GitHub Actions (
.github/workflows/verify-codegen.yml)Matrix job
verify-${{ matrix.target }}now includes:Each target:
BASE_IMAGEbuild argverification/work-<target>/artifacts on failurefail-fast: falseso one target failing does not cancel the others.npm scripts (
package.json)(
verify:docker:typescriptandverify:docker:jsonschemawere already present.)Snapshot test updates
File:
test/codegen/__snapshots__/codegen.js.snapUpdated C# snapshots for the HR fixtures after reserved-keyword namespace escaping:
namespace org.acme.hr.base;namespace org.acme.hr._base;using org.acme.hr.base;using org.acme.hr._base;org.acme.hr.base.Addressorg.acme.hr._base.AddressConcerto metadata strings (
Namespace = "org.acme.hr.base",_classFQNs) are unchanged.Note: When updating snapshots, run the full format test or apply targeted edits. Running
mocha --updateSnapshotwith--grepon a single format can remove snapshots for tests that did not execute.Running verification locally
Mocha (requires .NET 8 SDK)
npm run test:verify # or C# only: npx mocha test/verification/csharp.compile.test.js --timeout 120000Docker (requires Docker)
npm run verify:docker:csharp # or all targets: npm run verify:dockerRebuild images after changing
verification/docker/base/DockerfileorVerify.csproj.Manual CLI check
Known limitations and follow-ups
Two codegen invocation paths - Mocha passes
useConcertoRuntime: falseexplicitly; Docker relies onCSHARP_USE_CONCERTO_RUNTIME=false. Both must stay aligned.Runtime-enabled path is not compile-verified - Adding
AccordProject.ConcertotoVerify.csprojwould enable testing runtime output separately; not in scope for initial verification.Nullable warnings suppressed, not fixed in codegen - Optional fields emit
?annotations regardless of project nullable setting. A future improvement could align codegen nullable syntax with project settings.Author Checklist
--signoffoption of git commit.mainfromfork:branchname