# C# port
A first-class full-stack target on .NET 8. Loader + conformance + EF Core codegen
+ render engine + the `meta` CLI all ship. Targets EF Core + ASP.NET Core +
Postgres + Npgsql.
## Install
Published to [NuGet](https://www.nuget.org/packages/MetaObjects) at `0.24.5` — four
packages, version-locked to the C# port version:
```xml
```
Install the CLI as a .NET tool:
```bash
dotnet tool install --global MetaObjects.Cli
dotnet meta # bare invocation prints the usage banner
```
The C# CLI is invoked as `dotnet meta` (command `dotnet-meta`), or run directly
from the repo via `dotnet run --project server/csharp/MetaObjects.Cli`. It is
deliberately **not** a bare `meta` executable — that name belongs to the
canonical Node `meta` CLI, which owns schema + TS codegen (ADR-0015).
## Configure
Drop metadata under `metadata/`:
```jsonc
// metadata/meta.blog.json
{ "metadata.root": {
"package": "acme::blog",
"children": [
{ "object.entity": {
"name": "Author",
"children": [
{ "source.rdb": { "@table": "authors" } },
{ "field.long": { "name": "id" } },
{ "field.string": { "name": "name", "@required": true, "@maxLength": 200 } },
{ "field.string": { "name": "bio", "@maxLength": 2000 } },
{ "identity.primary": { "@fields": "id", "@generation": "increment" } }
]
}}
]
}}
```
### Custom providers (optional)
If your app needs a metamodel subtype the core doesn't ship, declare an
`IMetaDataTypeProvider` and compose it into the registry before loading:
```csharp
using MetaObjects;
using MetaObjects.Loader;
var registry = Provider.ComposeRegistry(new IMetaDataTypeProvider[] {
CoreTypes.CoreTypesProvider,
yourProvider, // adds your custom subtype/attrs
});
var loader = MetaDataLoader.FromDirectory("./metadata", registry);
```
The provider object has the same four-member contract (`Id`, `Dependencies`,
`Description`, `RegisterTypes(registry)`) as TS / Python. Composition errors
surface `ERR_PROVIDER_DUPLICATE_ID`, `ERR_PROVIDER_MISSING_DEPENDENCY`,
`ERR_PROVIDER_DEPENDENCY_CYCLE` — codes match the cross-port contract. See
[`../features/extending-with-providers.md`](../features/extending-with-providers.md)
for the full reference and
[`../recipes/extending-metaobjects-with-providers.md`](../recipes/extending-metaobjects-with-providers.md)
for a worked example.
## Generate
```bash
# Generate EF Core entities + AppDbContext + CRUD minimal-API routes
dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog
# Drift-check templates against payloads (FR-004)
dotnet meta verify ./metadata --templates ./prompts
```
Schema migrations are owned by the Node `meta` CLI (ADR-0015) — the C# CLI is
`gen` + `verify` only.
`gen` also accepts `--template-spec ` (+ `--template-root `, default
`templates`) — the declarative Mustache template-codegen surface; see
[Declarative template-codegen](#declarative-template-codegen---template-spec) below.
The codegen emits:
- `Author.g.cs` — class per entity (a mutable attributed POCO, not a record).
- `AppDbContext.g.cs` — `DbSet`, projection `.ToView()`, `@storage` owned
types via `OwnsOne` (single) / `OwnsMany(...).ToJson(...)` (`@isArray` array-of-VO),
enum-as-string via `HasConversion()`.
- `AuthorRoutes.g.cs` — CRUD minimal-API endpoints.
- `AuthorFilterAllowlist.g.cs` — the server-side filter/sort allowlist feeding the
generated list handler.
## Use
```csharp
// Program.cs
using Acme.Blog;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext(opts =>
opts.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
app.MapAuthorRoutes(); // generated — GET/POST/PUT/DELETE on /api/authors
app.Run();
```
EF Core does the rest — the generated entities and `AppDbContext` are plain EF Core with
no MetaObjects types in them. Note the one exception: generated **routes** emit
`using MetaObjects.Codegen.Runtime;` for the shared filter/sort helpers, so a project that
generates routes references `MetaObjects.Codegen` at runtime. Entities-and-DbContext-only
projects do not.
**Consumer dependencies.** The generated `AppDbContext` and the `Program.cs`
wiring above use EF Core (`AddDbContext`, `DbContext`, `UseNpgsql`), which
MetaObjects does not pull in for you — add the two EF Core NuGet packages to
the consuming app: `Microsoft.EntityFrameworkCore` and
`Npgsql.EntityFrameworkCore.PostgreSQL`.
```csharp
// Optional handwritten service over the generated DbContext
public class AuthorService(AppDbContext db)
{
public Task> ListAsync() => db.Authors.ToListAsync();
public async Task CreateAsync(string name, string? bio = null)
{
var author = new Author { Name = name, Bio = bio };
db.Authors.Add(author);
await db.SaveChangesAsync();
return author.Id;
}
}
```
## FR-004 — render
```csharp
using MetaObjects.Render;
var provider = new FilesystemProvider("./prompts");
var payload = new WelcomePayload(
DisplayName: "Ada",
PostCount: 12,
Posts: new[] { new PostSummary("Hello") });
string output = Renderer.Render(new RenderRequest {
Ref = "lobby/welcome",
Payload = payload,
Provider = provider,
Format = "xml",
});
```
`Verify` in `MetaObjects.Render` drift-checks every `template.*` against its
`@payloadRef`. Wire it into your CI step or invoke `dotnet meta verify` directly.
## FR-006 — response parsing
`OutputParserGenerator` (in `MetaObjects.Codegen`) emits one
`.response.cs` file per responding `template.prompt` — one declaring
`@responseRef`. The static `Parser` class follows the BCL `Parse`/`TryParse`
dual-API convention — `Parse` throws on bad input, `TryParse` returns a bool plus an
out-error string.
ADR-0052: the shape parsed INTO is `@responseRef`, never `@payloadRef` (which types the
request the prompt renders outbound), and `template.output` gets no parser at all. This
port's records are VALUE-OBJECT-named, so the response record simply IS the VO's record —
no second naming convention. The strict tier is JSON-only: an `@responseFormat: xml`
reply gets the tolerant extract and nothing strict.
```csharp
// generated/NpcResponse.response.cs
public static class NpcResponseParser
{
private static readonly JsonSerializerOptions Options = new()
{
PropertyNameCaseInsensitive = false,
};
/// malformed JSON or schema mismatch.
public static NpcResponse Parse(string text) =>
JsonSerializer.Deserialize(text, Options)
?? throw new JsonException("deserialized to null");
public static bool TryParse(string text,
[NotNullWhen(true)] out NpcResponse? value,
[NotNullWhen(false)] out string? error) { ... }
}
```
The `[NotNullWhen]` attributes mean nullable-flow analysis lets you use `npc`
without a null-check after a `true` return, and `error` without one after `false`.
Consumer wiring:
```csharp
string llmResponse = await myLlmClient.CompleteAsync(promptText);
// Throwing path
var npc = NpcResponseParser.Parse(llmResponse);
// TryParse for explicit error handling
if (NpcResponseParser.TryParse(llmResponse, out var npc, out var error))
return Ok(npc);
else
return BadRequest(new { error });
```
`MetaObjects.Render`'s `Verify` walks both template subtypes, catching payload ↔
template drift at build time.
Cross-port design is at [ADR-0010](../../spec/decisions/ADR-0010-template-output-parser-codegen.md);
the feature reference is at
[`features/templates-and-payloads.md`](../features/templates-and-payloads.md#response-parsing-fr-006).
**Consumer dependency.** `System.Text.Json` ships in the .NET 8 BCL — no
NuGet package to add. The generated parser uses the strict (case-sensitive)
default options.
## Declarative template-codegen (`--template-spec`)
Beyond the built-in EF Core / routes suite, `dotnet meta gen` runs **declarative
Mustache template generators** from a JSON template-spec — the cross-port contract
shared with the Python port (see
[`docs/features/codegen-concepts.md`](../features/codegen-concepts.md#declarative-template-scopes)
and the neutral data dict in
[`docs/features/codegen-data-shapes.md`](../features/codegen-data-shapes.md)):
```bash
dotnet meta gen ./metadata --out ./Generated \
--template-spec ./template-spec.json --template-root ./templates
```
```jsonc
// template-spec.json — the cross-port shape
{ "generators": [
{ "name": "entity-doc",
"scope": "perEntity", // perEntity | perPackage | perModel
"outputPattern": "{package}/{Name}.md",
"template": "entity-doc", // resolved under --template-root
"format": "markdown" } // optional; a registered escaper format
]}
```
Each spec entry derives the neutral template data dict for its scope
(`MetaObjects.Codegen.TemplateCodegen.TemplateData`) and names each file via the
`outputPattern` placeholders (`{name}`, `{Name}`, `{package}`). The named generators
are **appended** to the default suite and gated byte-identical against the shared
`fixtures/template-codegen-conformance/` corpus. A `target` field is rejected (C# has
no output-target concept); a bad template ref or wrong `--template-root` surfaces as a
clean error, not a stack trace. For output to be regenerable, the **template** must emit
the `@generated` header itself (the write path refuses to overwrite files lacking it).
## Angular 18 frontend
C# 12 / .NET 8 backends pair cleanly with an Angular 18 client built from
the universal `@metaobjectsdev/angular` runtime + `@metaobjectsdev/codegen-ts-angular`
codegen packages — which are **source-only today, not published to npm** (build
them from the TS workspace; see the
[recipe](../recipes/csharp-angular18.md)). The generated ASP.NET Minimal API routes (from
`MetaObjects.Codegen` `RoutesGenerator`) speak the same URL grammar
and wire format the Angular client expects — no special-casing.
End-to-end recipe — CORS wiring, dev-server port conventions, base-URL
configuration, the `dotnet meta gen` command sequence that emits both halves —
lives at [`docs/recipes/csharp-angular18.md`](../recipes/csharp-angular18.md).
Today's `RoutesGenerator` honours pagination (`?limit`/`?offset`), sort
(`?sort=:asc|desc` against a static per-entity allowlist), and the
`?withCount=1` envelope (`{ rows, total }`) that the Angular grid hook
always sends. Filter operators (`eq` / `ne` / `gt` / `gte` / `lt` / `lte`
/ `in` / `like` / `isNull`) per [`api-contract.md`](../features/api-contract.md)
ship too — the generated `FilterAllowlist` (`FilterAllowlistGenerator`)
feeds `FilterParser.Parse` + `EfCoreFilterDispatch.ApplyFilter`, both wired
directly into the generated list handler. The one real gap: read-only
projections (`source.rdb @kind: view/...`) don't get filter routes today — see
[`server/csharp/MetaObjects.Codegen/Generators/KNOWN_GAPS.md`](../../server/csharp/MetaObjects.Codegen/Generators/KNOWN_GAPS.md).
## Capability snapshot
| Feature | Status |
|---|---|
| Entities + fields | Yes |
| Relationships + FK | Yes (EF Core + Postgres FK clause) |
| Source kinds (table / view / storedProc) | `table` + `view` fully shipped; `storedProc` / `tableFunction` / `materializedView` partial |
| `field.currency` / `field.enum` / `field.object` + `@storage` | Yes (incl. EF Core `OwnsOne` for `flattened`; `OwnsMany(...).ToJson(...)` for `@isArray` array-of-VO jsonb) |
| Templates + render (FR-004) | Yes (`MetaObjects.Render`) |
| Output parser codegen (FR-006) | Yes (`OutputParserGenerator` — `Parse`/`TryParse` BCL pattern) |
| Payload-VO codegen | Yes (`MetaObjects.Codegen`) |
| Declarative template-codegen | Yes — `dotnet meta gen --template-spec` (scope perEntity/perPackage/perModel + outputPattern; the cross-port JSON contract shared with Python) |
| Migrations | Owned by the Node `meta` CLI (ADR-0015) — no C# migrate surface |
| Drift verify | `dotnet meta verify` (template drift, FR-004) |
| Runtime metadata | Loader API + render engine; ObjectManager-style runtime tier on the roadmap |
## Conformance status
Per-corpus pass counts move every release — see
[`docs/CONFORMANCE.md`](../CONFORMANCE.md) for the current, authoritative
per-port numbers (metamodel, YAML, render, verify, persistence, API
contract). C# is green across all six active corpora today.
## See also
- [`server/csharp/README.md`](../../server/csharp/README.md) — module-level overview
- [`docs/features/`](../features/) — every feature shows the C# output inline
- [`docs/superpowers/specs/2026-05-20-csharp-rdb-persistence-design.md`](../superpowers/specs/2026-05-20-csharp-rdb-persistence-design.md)