Code Emission and Special Types
OpenApiV3SchemaEmitter is the heart of Track B. It walks the merged UnifiedSchema from Part 5, emits one POCO per type, calls BuilderEmitter.Emit() from Builder.SourceGenerator.Lib for the matching fluent builder, and handles every K8s-specific quirk along the way: vendor extensions, special types, discriminated unions, nullability rules, naming conventions.
This is the longest chapter in the series. There's a lot to cover, but the structure is repeatable: for each schema feature, what the OpenAPI spec says, how the emitter handles it, and what the generated C# looks like.
Naming conventions
Locked up front in the plan and applied uniformly:
| Concept | Convention | Example |
|---|---|---|
| Core API types | V<major>{Kind} |
V1Pod, V1Deployment, V2HorizontalPodAutoscaler |
| Builder | {Type}Builder |
V1PodBuilder, V1DeploymentSpecBuilder |
| CRD types | V<major>{Alpha|Beta}<n>{Kind} |
V1Alpha1Rollout, V1Beta1ScaledObject |
| Enums | {Kind}{Field} |
PodRestartPolicy, ServiceType |
The leading V<n> prefix matches KubernetesClient/csharp and the OpenAPI spec naming, so users moving between the two libraries don't have to context-switch.
Namespace layout
Kubernetes.Dsl (root, neutral types, IKubernetesObject)
Kubernetes.Dsl.Api.Core.V1 (core/v1 - Pod, Service, ConfigMap, Secret)
Kubernetes.Dsl.Api.Apps.V1 (apps/v1 - Deployment, StatefulSet, DaemonSet)
Kubernetes.Dsl.Api.Networking.V1 (networking.k8s.io/v1 - Ingress, NetworkPolicy)
Kubernetes.Dsl.Api.Rbac.V1 (rbac.authorization.k8s.io/v1)
Kubernetes.Dsl.Api.Batch.V1 (batch/v1 - Job, CronJob)
Kubernetes.Dsl.Api.Autoscaling.V2 (autoscaling/v2 - HPA)
Kubernetes.Dsl.Api.Meta.V1 (apimachinery.pkg.apis.meta.v1 - flattened)
Kubernetes.Dsl.Crds.ArgoProj.V1Alpha1 (CRDs - one namespace per group)
Kubernetes.Dsl.Crds.MonitoringCoreOs.V1 (Prometheus Operator)
Kubernetes.Dsl.Crds.Keda.V1Alpha1
Kubernetes.Dsl.Crds.CertManager.V1
Kubernetes.Dsl.Crds.Gatekeeper.V1
Kubernetes.Dsl.Crds.Litmus.V1Alpha1
Kubernetes.Dsl.Crds.Istio.SecurityV1Beta1
Kubernetes.Dsl.Yaml (KubernetesYamlReader/Writer + converters)
Kubernetes.Dsl.Cli (BinaryWrapper-generated kubectl client)
Kubernetes.Dsl.Analyzers (KUB001-KUB099)Kubernetes.Dsl (root, neutral types, IKubernetesObject)
Kubernetes.Dsl.Api.Core.V1 (core/v1 - Pod, Service, ConfigMap, Secret)
Kubernetes.Dsl.Api.Apps.V1 (apps/v1 - Deployment, StatefulSet, DaemonSet)
Kubernetes.Dsl.Api.Networking.V1 (networking.k8s.io/v1 - Ingress, NetworkPolicy)
Kubernetes.Dsl.Api.Rbac.V1 (rbac.authorization.k8s.io/v1)
Kubernetes.Dsl.Api.Batch.V1 (batch/v1 - Job, CronJob)
Kubernetes.Dsl.Api.Autoscaling.V2 (autoscaling/v2 - HPA)
Kubernetes.Dsl.Api.Meta.V1 (apimachinery.pkg.apis.meta.v1 - flattened)
Kubernetes.Dsl.Crds.ArgoProj.V1Alpha1 (CRDs - one namespace per group)
Kubernetes.Dsl.Crds.MonitoringCoreOs.V1 (Prometheus Operator)
Kubernetes.Dsl.Crds.Keda.V1Alpha1
Kubernetes.Dsl.Crds.CertManager.V1
Kubernetes.Dsl.Crds.Gatekeeper.V1
Kubernetes.Dsl.Crds.Litmus.V1Alpha1
Kubernetes.Dsl.Crds.Istio.SecurityV1Beta1
Kubernetes.Dsl.Yaml (KubernetesYamlReader/Writer + converters)
Kubernetes.Dsl.Cli (BinaryWrapper-generated kubectl client)
Kubernetes.Dsl.Analyzers (KUB001-KUB099)apimachinery.pkg.apis.meta.v1 is flattened to Kubernetes.Dsl.Api.Meta.V1 because the original Go-derived path is hostile to read.
Nullability and required: mapping
OpenAPI marks required fields with a top-level required: [...] array per type. The C# mapping:
| OpenAPI shape | C# property | Builder behavior |
|---|---|---|
required |
non-nullable, no ? |
Build() returns Result.Fail if not set |
| optional | nullable T? |
omitted if null in YAML output |
required + has default |
non-nullable, initialized to default | builder pre-sets it |
nullable: true |
nullable T? even if required |
round-trips an explicit null |
This is the rule that shapes every generated POCO. Stated here, applied in every example.
A complete generated POCO: V1Pod
// <auto-generated/> Source: kubernetes/api/core/v1/Pod (k8s 1.31)
namespace Kubernetes.Dsl.Api.Core.V1;
[KubernetesResource(ApiVersion = "v1", Kind = "Pod", Group = "")]
[SinceVersion("1.0")]
public sealed partial class V1Pod : IKubernetesObject<V1ObjectMeta>
{
public string ApiVersion { get; set; } = "v1";
public string Kind { get; set; } = "Pod";
// required:[metadata, spec] in OpenAPI -> non-nullable
public V1ObjectMeta Metadata { get; set; } = new();
public V1PodSpec Spec { get; set; } = new();
// status is optional and never serialized on write (author-side library)
[YamlMember(SerializeAs = SerializeAs.OmitOnWrite)]
public V1PodStatus? Status { get; set; }
}// <auto-generated/> Source: kubernetes/api/core/v1/Pod (k8s 1.31)
namespace Kubernetes.Dsl.Api.Core.V1;
[KubernetesResource(ApiVersion = "v1", Kind = "Pod", Group = "")]
[SinceVersion("1.0")]
public sealed partial class V1Pod : IKubernetesObject<V1ObjectMeta>
{
public string ApiVersion { get; set; } = "v1";
public string Kind { get; set; } = "Pod";
// required:[metadata, spec] in OpenAPI -> non-nullable
public V1ObjectMeta Metadata { get; set; } = new();
public V1PodSpec Spec { get; set; } = new();
// status is optional and never serialized on write (author-side library)
[YamlMember(SerializeAs = SerializeAs.OmitOnWrite)]
public V1PodStatus? Status { get; set; }
}The [KubernetesResource] attribute carries the discriminator info that powers the type registry (see "Type registry" below). The [YamlMember(SerializeAs = OmitOnWrite)] is a custom attribute consumed by the K8s YAML writer (Part 7) — status is what the API server fills in, never something an author writes.
A complete generated builder: V1PodBuilder
// <auto-generated/> via Builder.SourceGenerator.Lib
namespace Kubernetes.Dsl.Api.Core.V1;
public sealed partial class V1PodBuilder : AbstractBuilder<V1Pod>
{
private V1ObjectMeta? _metadata;
private V1PodSpec? _spec;
public V1PodBuilder WithMetadata(Action<V1ObjectMetaBuilder> configure)
{
var b = new V1ObjectMetaBuilder();
configure(b);
_metadata = b.Build().Value;
return this;
}
public V1PodBuilder WithSpec(Action<V1PodSpecBuilder> configure)
{
var b = new V1PodSpecBuilder();
configure(b);
_spec = b.Build().Value;
return this;
}
protected override Result<V1Pod> BuildCore()
{
if (_metadata is null || string.IsNullOrEmpty(_metadata.Name))
return Result.Fail<V1Pod>("KUB002: V1Pod.metadata.name is required");
if (_spec is null)
return Result.Fail<V1Pod>("KUB002: V1Pod.spec is required");
return Result.Ok(new V1Pod { Metadata = _metadata, Spec = _spec });
}
}// <auto-generated/> via Builder.SourceGenerator.Lib
namespace Kubernetes.Dsl.Api.Core.V1;
public sealed partial class V1PodBuilder : AbstractBuilder<V1Pod>
{
private V1ObjectMeta? _metadata;
private V1PodSpec? _spec;
public V1PodBuilder WithMetadata(Action<V1ObjectMetaBuilder> configure)
{
var b = new V1ObjectMetaBuilder();
configure(b);
_metadata = b.Build().Value;
return this;
}
public V1PodBuilder WithSpec(Action<V1PodSpecBuilder> configure)
{
var b = new V1PodSpecBuilder();
configure(b);
_spec = b.Build().Value;
return this;
}
protected override Result<V1Pod> BuildCore()
{
if (_metadata is null || string.IsNullOrEmpty(_metadata.Name))
return Result.Fail<V1Pod>("KUB002: V1Pod.metadata.name is required");
if (_spec is null)
return Result.Fail<V1Pod>("KUB002: V1Pod.spec is required");
return Result.Ok(new V1Pod { Metadata = _metadata, Spec = _spec });
}
}AbstractBuilder<T>, Result<T>, the validation pipeline — all of these are reused unchanged from Builder.SourceGenerator.Lib. The emitter calls BuilderEmitter.Emit(BuilderEmitModel) directly, the same way GitLab.Ci.Yaml.SourceGenerator does (verified at GitLabCiBundleGenerator.cs:60-86). No two-stage SG. No [Builder] attribute on the generated POCO.
OpenAPI v3 Kubernetes vendor extensions
This is the genuinely hard part. K8s OpenAPI v3 carries Kubernetes-specific vendor extensions that change emission behavior. The emitter has to recognize and act on each one.
| Extension | Where it appears | Emitter behavior |
|---|---|---|
x-kubernetes-group-version-kind |
Top-level on each resource type | Drives the type registry. Value is [{group, version, kind}] — used to build KubernetesTypeRegistry.g.cs and to generate KubectlResourceCatalog.g.cs for Track A's resource enums |
x-kubernetes-list-type |
Array properties | atomic → List<T> (replace semantics); set → HashSet<T>; map → Dictionary<K,T> keyed by x-kubernetes-list-map-keys. Affects builder WithXxxItem() semantics |
x-kubernetes-list-map-keys |
Array properties (with list-type: map) |
The key field(s) for the map projection. Builder emits WithXxxByKey(key, configure) instead of WithXxxItem |
x-kubernetes-patch-merge-key |
Array properties | Drives strategic-merge-patch behavior in Track A's kubectl patch overload. Recorded as [PatchMergeKey("name")] on the property |
x-kubernetes-patch-strategy |
Array properties | merge/replace — paired with patch-merge-key. Recorded as [PatchStrategy("merge")] |
x-kubernetes-int-or-string |
Scalar properties | Override the OpenAPI type — emit as IntOrString struct regardless of the declared type: |
x-kubernetes-preserve-unknown-fields |
Object properties | Emit JsonElement (runtime.RawExtension shape). Used heavily in CRDs and customResourceDefinition.spec.versions[*].schema |
x-kubernetes-embedded-resource |
Object properties | The property is itself an IKubernetesObject. Emit as IKubernetesObject interface instead of a concrete type |
discriminator |
oneOf schemas |
Used to generate the discriminated-union sealed-record hierarchy below |
nullable: true |
Any property | Override required: — emit as T? even when required, so explicit null round-trips |
default: ... |
Any property | Initialize the C# property to the default; builder pre-sets it |
format: int-or-string |
Scalar | Same as x-kubernetes-int-or-string (older spelling) |
format: byte |
String property | Emit as byte[] with base64 converter |
format: date-time |
String property | DateTimeOffset |
x-kubernetes-group-version-kind is the most consequential — it drives both the type registry and Track A's resource catalog, and is the bridge artifact between the two tracks.
Special types
These get their own first-class treatment because they appear everywhere and trip people up.
IntOrString
// Kubernetes.Dsl/IntOrString.cs (hand-written, shipped in Lib)
public readonly struct IntOrString : IEquatable<IntOrString>
{
private readonly int? _int;
private readonly string? _str;
public bool IsInt => _int.HasValue;
public bool IsString => _str is not null;
public int IntValue => _int ?? throw new InvalidOperationException("Not an int");
public string StringValue => _str ?? throw new InvalidOperationException("Not a string");
public static IntOrString From(int value) => new(value, null);
public static IntOrString From(string value) => new(null, value);
public static implicit operator IntOrString(int value) => From(value);
public static implicit operator IntOrString(string value) => From(value);
private IntOrString(int? i, string? s) { _int = i; _str = s; }
// Equals, GetHashCode, ToString, ...
}// Kubernetes.Dsl/IntOrString.cs (hand-written, shipped in Lib)
public readonly struct IntOrString : IEquatable<IntOrString>
{
private readonly int? _int;
private readonly string? _str;
public bool IsInt => _int.HasValue;
public bool IsString => _str is not null;
public int IntValue => _int ?? throw new InvalidOperationException("Not an int");
public string StringValue => _str ?? throw new InvalidOperationException("Not a string");
public static IntOrString From(int value) => new(value, null);
public static IntOrString From(string value) => new(null, value);
public static implicit operator IntOrString(int value) => From(value);
public static implicit operator IntOrString(string value) => From(value);
private IntOrString(int? i, string? s) { _int = i; _str = s; }
// Equals, GetHashCode, ToString, ...
}Implicit conversions let users write Port = 8080 or Port = "http" without ceremony. The YAML writer (Part 7) round-trips both forms.
Quantity
// Kubernetes.Dsl/Quantity.cs (hand-written, shipped in Lib)
public readonly struct Quantity : IEquatable<Quantity>
{
public string CanonicalForm { get; } // "500m", "512Mi", "10Gi", "1.5", "2"
public static Quantity Cpu(string value) => new(value);
public static Quantity Memory(string value) => new(value);
public static Quantity Storage(string value) => new(value);
public static Quantity Parse(string s) { /* parses "500m", "1.5Gi", etc. */ }
public static implicit operator Quantity(string value) => Parse(value);
private Quantity(string canonical) { CanonicalForm = canonical; }
}// Kubernetes.Dsl/Quantity.cs (hand-written, shipped in Lib)
public readonly struct Quantity : IEquatable<Quantity>
{
public string CanonicalForm { get; } // "500m", "512Mi", "10Gi", "1.5", "2"
public static Quantity Cpu(string value) => new(value);
public static Quantity Memory(string value) => new(value);
public static Quantity Storage(string value) => new(value);
public static Quantity Parse(string s) { /* parses "500m", "1.5Gi", etc. */ }
public static implicit operator Quantity(string value) => Parse(value);
private Quantity(string canonical) { CanonicalForm = canonical; }
}Quantity.Cpu("500m"), Quantity.Memory("512Mi"), Quantity.Storage("10Gi") — readable at the call site, type-checked, no stringly-typed quantities.
Duration and Time
| OpenAPI type | C# type |
|---|---|
metav1.Time |
DateTimeOffset (RFC 3339) |
metav1.MicroTime |
DateTimeOffset |
metav1.Duration |
TimeSpan (custom converter for Go duration syntax: 30s, 5m) |
RawExtension
// runtime.RawExtension - CRDs embed arbitrary content
// Emitted as System.Text.Json.JsonElement to preserve fidelity
public sealed partial class V1CustomResourceValidation
{
[SinceVersion("1.16")]
public JsonElement? OpenAPIV3Schema { get; set; }
}// runtime.RawExtension - CRDs embed arbitrary content
// Emitted as System.Text.Json.JsonElement to preserve fidelity
public sealed partial class V1CustomResourceValidation
{
[SinceVersion("1.16")]
public JsonElement? OpenAPIV3Schema { get; set; }
}We don't lose fidelity. Users who need to introspect the embedded schema use JsonElement's walker API.
Discriminated unions for oneOf
The Pod Volume type has ~30 mutually exclusive sources (emptyDir, configMap, secret, persistentVolumeClaim, hostPath, etc.). The OpenAPI spec models this as a flat object with all 30 fields optional and an unwritten "exactly one must be set" rule. The emitter strategy:
- Emit the POCO with all 30 nullable fields (round-trip fidelity).
- Emit a sealed
VolumeSourcediscriminated wrapper with one factory per variant. - Emit a builder that exposes
WithSource(VolumeSource.EmptyDir())/WithSource(VolumeSource.ConfigMap("my-config")). - Builder validation rejects multi-source assignment with
KUB001. - The analyzer (Part 11) flags raw POCO assignments that set more than one (
KUB001again, but at the call site).
// <auto-generated/> oneOf wrapper for Pod.spec.volumes[*]
namespace Kubernetes.Dsl.Api.Core.V1;
public abstract record VolumeSource
{
public static VolumeSource EmptyDir(Quantity? sizeLimit = null) => new EmptyDirSource(sizeLimit);
public static VolumeSource ConfigMap(string name) => new ConfigMapSource(name);
public static VolumeSource Secret(string secretName) => new SecretSource(secretName);
public static VolumeSource PersistentVolumeClaim(string claimName) => new PvcSource(claimName);
public static VolumeSource HostPath(string path, HostPathType type) => new HostPathSource(path, type);
// ... 25 more variants
internal abstract void ApplyTo(V1Volume target);
}
public sealed record EmptyDirSource(Quantity? SizeLimit) : VolumeSource
{
internal override void ApplyTo(V1Volume target)
=> target.EmptyDir = new V1EmptyDirVolumeSource { SizeLimit = SizeLimit };
}
public sealed record ConfigMapSource(string Name) : VolumeSource
{
internal override void ApplyTo(V1Volume target)
=> target.ConfigMap = new V1ConfigMapVolumeSource { Name = Name };
}
// ...// <auto-generated/> oneOf wrapper for Pod.spec.volumes[*]
namespace Kubernetes.Dsl.Api.Core.V1;
public abstract record VolumeSource
{
public static VolumeSource EmptyDir(Quantity? sizeLimit = null) => new EmptyDirSource(sizeLimit);
public static VolumeSource ConfigMap(string name) => new ConfigMapSource(name);
public static VolumeSource Secret(string secretName) => new SecretSource(secretName);
public static VolumeSource PersistentVolumeClaim(string claimName) => new PvcSource(claimName);
public static VolumeSource HostPath(string path, HostPathType type) => new HostPathSource(path, type);
// ... 25 more variants
internal abstract void ApplyTo(V1Volume target);
}
public sealed record EmptyDirSource(Quantity? SizeLimit) : VolumeSource
{
internal override void ApplyTo(V1Volume target)
=> target.EmptyDir = new V1EmptyDirVolumeSource { SizeLimit = SizeLimit };
}
public sealed record ConfigMapSource(string Name) : VolumeSource
{
internal override void ApplyTo(V1Volume target)
=> target.ConfigMap = new V1ConfigMapVolumeSource { Name = Name };
}
// ...Same pattern for Probe (httpGet / tcpSocket / exec / grpc), SecurityContext, VolumeMount, LifecycleHandler. The discriminator is the field name that's set; the emitter generates the sealed-record hierarchy from the parent's oneOf schema.
Type registry generator
The YAML reader (Part 7) needs (apiVersion, kind) → Type dispatch to deserialize multi-doc streams without knowing the types in advance. The registry is generated from the same OpenAPI parse pass:
// Kubernetes.Dsl/KubernetesTypeRegistry.g.cs (auto-generated)
namespace Kubernetes.Dsl;
public static partial class KubernetesTypeRegistry
{
private static readonly Dictionary<(string apiVersion, string kind), Type> _map = new()
{
[("v1", "Pod")] = typeof(Api.Core.V1.V1Pod),
[("v1", "Service")] = typeof(Api.Core.V1.V1Service),
[("v1", "ConfigMap")] = typeof(Api.Core.V1.V1ConfigMap),
[("apps/v1", "Deployment")] = typeof(Api.Apps.V1.V1Deployment),
[("apps/v1", "StatefulSet")] = typeof(Api.Apps.V1.V1StatefulSet),
[("argoproj.io/v1alpha1", "Rollout")] = typeof(Crds.ArgoProj.V1Alpha1.V1Alpha1Rollout),
// ... one entry per x-kubernetes-group-version-kind across all schemas
};
public static Type? Resolve(string apiVersion, string kind)
=> _map.TryGetValue((apiVersion, kind), out var t) ? t : null;
}// Kubernetes.Dsl/KubernetesTypeRegistry.g.cs (auto-generated)
namespace Kubernetes.Dsl;
public static partial class KubernetesTypeRegistry
{
private static readonly Dictionary<(string apiVersion, string kind), Type> _map = new()
{
[("v1", "Pod")] = typeof(Api.Core.V1.V1Pod),
[("v1", "Service")] = typeof(Api.Core.V1.V1Service),
[("v1", "ConfigMap")] = typeof(Api.Core.V1.V1ConfigMap),
[("apps/v1", "Deployment")] = typeof(Api.Apps.V1.V1Deployment),
[("apps/v1", "StatefulSet")] = typeof(Api.Apps.V1.V1StatefulSet),
[("argoproj.io/v1alpha1", "Rollout")] = typeof(Crds.ArgoProj.V1Alpha1.V1Alpha1Rollout),
// ... one entry per x-kubernetes-group-version-kind across all schemas
};
public static Type? Resolve(string apiVersion, string kind)
=> _map.TryGetValue((apiVersion, kind), out var t) ? t : null;
}One entry per resource type, drawn from x-kubernetes-group-version-kind. CRDs participate in the same registry — Argo Rollouts, Prometheus Operator, KEDA, all of them get rows.
Version metadata emitter
// <auto-generated/> from k8s OpenAPI 1.27, 1.28, 1.29, 1.30, 1.31
namespace Kubernetes.Dsl;
public static class KubernetesApiVersions
{
public const string V1_27 = "1.27";
public const string V1_28 = "1.28";
public const string V1_29 = "1.29";
public const string V1_30 = "1.30";
public const string V1_31 = "1.31";
public static readonly KubernetesVersion Latest = new(1, 31, 0);
}
// Per-property promotion tracking emitted as attributes on each property:
// autoscaling/v2 HPA.behavior -> [SinceVersion("1.18")]
// flowcontrol.apiserver/v1 -> [SinceVersion("1.29")]
// PodSecurityPolicy -> [UntilVersion("1.25"), Deprecated]
// extensions/v1beta1 Ingress -> [UntilVersion("1.22"), Removed]// <auto-generated/> from k8s OpenAPI 1.27, 1.28, 1.29, 1.30, 1.31
namespace Kubernetes.Dsl;
public static class KubernetesApiVersions
{
public const string V1_27 = "1.27";
public const string V1_28 = "1.28";
public const string V1_29 = "1.29";
public const string V1_30 = "1.30";
public const string V1_31 = "1.31";
public static readonly KubernetesVersion Latest = new(1, 31, 0);
}
// Per-property promotion tracking emitted as attributes on each property:
// autoscaling/v2 HPA.behavior -> [SinceVersion("1.18")]
// flowcontrol.apiserver/v1 -> [SinceVersion("1.29")]
// PodSecurityPolicy -> [UntilVersion("1.25"), Deprecated]
// extensions/v1beta1 Ingress -> [UntilVersion("1.22"), Removed]CRDs get their own version constants:
public static class ArgoRolloutsBundleVersions
{
public const string V1_5_0 = "argo-rollouts/v1.5.0";
public const string V1_6_0 = "argo-rollouts/v1.6.0";
public const string V1_7_0 = "argo-rollouts/v1.7.0";
public const string V1_7_2 = "argo-rollouts/v1.7.2";
public static readonly string Latest = V1_7_2;
}public static class ArgoRolloutsBundleVersions
{
public const string V1_5_0 = "argo-rollouts/v1.5.0";
public const string V1_6_0 = "argo-rollouts/v1.6.0";
public const string V1_7_0 = "argo-rollouts/v1.7.0";
public const string V1_7_2 = "argo-rollouts/v1.7.2";
public static readonly string Latest = V1_7_2;
}These constants can be used in the user's [KubernetesBundle].TargetClusterCompatibility instead of magic strings.
Per-file emission strategy
obj/Generated/Kubernetes.Dsl/
├── Models/
│ ├── Core.V1.Pod.g.cs (one type per file)
│ ├── Core.V1.PodSpec.g.cs (nested types live in their own file)
│ ├── Core.V1.Container.g.cs
│ ├── Core.V1.VolumeSource.g.cs (the discriminated union)
│ └── ...
├── Builders/
│ ├── Core.V1.PodBuilder.g.cs
│ ├── Core.V1.PodSpecBuilder.g.cs
│ └── ...
├── Crds/
│ ├── ArgoProj.V1Alpha1.Rollout.g.cs
│ └── ...
└── _Manifest.g.cs (registry of every generated type)obj/Generated/Kubernetes.Dsl/
├── Models/
│ ├── Core.V1.Pod.g.cs (one type per file)
│ ├── Core.V1.PodSpec.g.cs (nested types live in their own file)
│ ├── Core.V1.Container.g.cs
│ ├── Core.V1.VolumeSource.g.cs (the discriminated union)
│ └── ...
├── Builders/
│ ├── Core.V1.PodBuilder.g.cs
│ ├── Core.V1.PodSpecBuilder.g.cs
│ └── ...
├── Crds/
│ ├── ArgoProj.V1Alpha1.Rollout.g.cs
│ └── ...
└── _Manifest.g.cs (registry of every generated type)One file per type. Predictable. Diffable. Each AddSource call is independent, so Roslyn deduplicates unchanged emissions across builds. This is critical for v0.4 performance (Part 8).
What the emitter does not do
- Does not emit
[Builder]attributes on POCOs. It callsBuilderEmitter.Emit(BuilderEmitModel)directly as a library function in the same SG pass. No two-stage SG. - Does not generate runtime client code. That's Track A's
KubectlClient. Part 10. - Does not validate against a live cluster. The emitter is pure: schemas in, C# files out.
- Does not emit
statusbuilder methods.Statusproperties are author-side metadata only and never get fluent setters. (See[YamlMember(SerializeAs = OmitOnWrite)]above.)
Previous: Part 5: Multi-Version Schema Merging — Across Both Core and CRDs Next: Part 7: K8s YAML Serialization — Multi-Doc, Discriminator, Round-Trip