eng/packages/http-client-csharp-provisioning/docs/design.md
The Azure.Provisioning.* libraries provide a C# infrastructure-as-code experience for Azure resources. Each library wraps an Azure Resource Manager (ARM) resource type with ProvisionableResource subclasses that expose BicepValue<T> properties, enabling users to declare Azure resources in C# and compile them to Bicep templates.
The current generator (sdk/provisioning/Generator/src/) is a standalone .NET console application that:
Azure.ResourceManager.* packages in its .csproj, loading compiled assemblies at runtime.<summary> tags on ARM methods to find version strings like 2023-01-01.Specification class per service — each service (Storage, KeyVault, etc.) needs a *Specification.cs file that registers the entry-point type, applies customizations (remove properties, rename models, add naming constraints, define RBAC roles), and is hard-coded in Program.cs.src/Generated/*.cs files — writes ProvisionableResource subclasses, model classes, enums, role definitions, and Bicep schema files.| Problem | Impact |
|---|---|
| NuGet dependency lag | The generator reads from published NuGet packages, not source. When an mgmt package is updated, the provisioning generator cannot pick up changes until a new NuGet version is published and referenced. |
| Hand-written Specification per service | Every new provisioning library requires a developer to: (1) create a Specification class, (2) add the NuGet reference, (3) register in Program.cs. This does not scale. |
| Reflection is fragile | The generator relies on internal patterns of the ARM SDK (e.g., finding CreateOrUpdate methods on ArmCollection types). Changes to ARM SDK codegen patterns silently break provisioning generation. |
| No TypeSpec integration | As management plane SDKs migrate from Swagger/AutoRest to TypeSpec, the TypeSpec toolchain has richer semantic information (resource types, API versions, property metadata) that is lost when going through NuGet binaries. |
| Version discovery is indirect | API versions are extracted from XML doc comment strings using regex, which is brittle and sometimes incorrect (e.g., preview versions leaking through). |
| No automation | Generating a provisioning library is a fully manual process with no CI/CD integration. |
Four provisioning libraries now have TypeSpec-based mgmt counterparts:
| Provisioning Package | Mgmt Package | TypeSpec Spec Path |
|---|---|---|
Azure.Provisioning.AppConfiguration | Azure.ResourceManager.AppConfiguration | specification/appconfiguration/resource-manager/... |
Azure.Provisioning.KeyVault | Azure.ResourceManager.KeyVault | specification/keyvault/KeyVault.Management |
Azure.Provisioning.Kubernetes | Azure.ResourceManager.Kubernetes | specification/hybridkubernetes/... |
Azure.Provisioning.SignalR | Azure.ResourceManager.SignalR | specification/signalr/resource-manager/... |
As more management SDKs migrate to TypeSpec, this number will grow rapidly. Now is the right time to build a generator that integrates into the TypeSpec toolchain.
Azure.Provisioning.* library with minimal manual intervention.http-client-csharp → http-client-csharp-mgmt → provisioning) so that provisioning generation can be triggered alongside mgmt SDK generation.Azure.Provisioning.* libraries. Existing users should not experience breaking changes.The provisioning generator is built as a new TypeSpec emitter package that extends the management emitter — a new layer in the existing emitter stack:
@typespec/http-client-csharp (core)
↑
@azure-typespec/http-client-csharp (Azure base)
↑
@azure-typespec/http-client-csharp-mgmt (ARM management)
↑
@azure-typespec/http-client-csharp-provisioning (NEW — provisioning)
How it works:
$onEmit() like the mgmt emitter.ProvisioningGenerator, extending ManagementClientGenerator) that produces the provisioning .cs files.Why this approach:
tsp compile alongside mgmt SDK generation.Challenges to address:
ProvisionableResource subclasses, not ArmClient/ArmResource types. This requires significant divergence in the C# generator's TypeFactory and OutputLibrary.@typespec/http-client-csharp (core emitter)
↑
@azure-typespec/http-client-csharp (Azure base emitter)
↑
@azure-typespec/http-client-csharp-mgmt (ARM management emitter)
↑
@azure-typespec/http-client-csharp-provisioning (provisioning emitter)
Emitter side (TypeScript):
$onEmit(), setting generator-name to ProvisioningGenerator.Generator side (C#):
ProvisioningGenerator extends ManagementClientGenerator.TypeFactory to produce provisioning-specific output types (ProvisionableResource, BicepValue<T> properties, etc.).OutputLibrary to control which files are generated (provisioning classes instead of ARM client classes).┌─────────────────────────────────────────────────────────────┐
│ TypeSpec Definitions │
│ (azure-rest-api-specs repository) │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ TypeSpec Emitter Chain │
│ │
│ @typespec/http-client-csharp │
│ → @azure-typespec/http-client-csharp │
│ → @azure-typespec/http-client-csharp-mgmt │
│ → @azure-typespec/http-client-csharp-provisioning │
│ │
│ Output: Code model (tspCodeModel.json) │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ProvisioningGenerator (C#) │
│ (extends ManagementClientGenerator) │
│ │
│ Pipeline: │
│ 1. Load code model from emitter │
│ 2. Override TypeFactory for provisioning types │
│ 3. Override OutputLibrary for provisioning output │
│ 4. Generate C# files: │
│ - ProvisionableResource subclasses │
│ - BicepValue<T> property wrappers │
│ - Model classes (ProvisionableConstruct subclasses) │
│ - Enum types │
│ - ResourceVersions nested classes │
│ - Built-in role enums │
│ │
│ Output: Azure.Provisioning.*/src/Generated/*.cs │
└─────────────────────────────────────────────────────────────┘
The provisioning generator uses TypeFactory extension points to intercept type creation at the framework level. This is the core architectural decision — instead of post-processing mgmt output, we replace type creation itself.
The ProvisioningTypeFactory extends ManagementTypeFactory and overrides four extension points: model creation, enum creation, C# type resolution, and property creation.
ProvisionableResource subclasses, regular models become ProvisionableConstruct subclasses, and system/framework types are skipped (they come from the Azure.Provisioning base package).enum types instead of the extensible readonly struct pattern used by mgmt libraries.BicepValue<T>, arrays in BicepList<T>, and dictionaries in BicepDictionary<T>. Model types resolve to our providers without wrapping.Etag → ETag), then creates a provisioning-style property with a linked backing field and BicepValue getter/setter.Resource models are identified from the ARM provider schema at the input level. The output library pre-creates all resources from the schema at construction time, populating a map from input model types to resource metadata used by the factory.
Important: We access the input-level resource schema and never the mgmt output library's resource providers, which would trigger model creation causing crashes since our factory returns provisioning types instead of the expected mgmt types.
InputModelType → Model creation
├─ Known framework type? → null (use Azure.Provisioning base)
├─ Inheritable system type? → null (e.g., ManagedServiceIdentity, SystemData)
├─ ARM resource model? → ProvisioningResourceProvider
├─ Discriminator variant of resource? → ProvisioningResourceProvider (derived)
└─ Regular model? → ProvisioningModelProvider
InputType → Type resolution
├─ Model type → provider's CSharpType (no wrapping)
├─ Array → BicepList<elementType>
├─ Dictionary → BicepDictionary<valueType>
├─ Enum (system) → BicepValue<frameworkEnumType>
├─ Enum (custom) → BicepValue<generatedEnumType>
└─ Scalar → BicepValue<T> (string, int, DateTimeOffset, ResourceIdentifier, etc.)
Element types for BicepList<T> and BicepDictionary<T> are resolved without BicepValue<T> wrapping (avoids BicepList<BicepValue<string>>).
Generates ProvisionableResource subclasses from input model types + resource metadata:
@flattenProperty decoratorname, location (required input), id, systemData (output-only), tags (input), type (skipped)Parent property for parent-child relationship(string bicepIdentifier, string? resourceVersion) with default API versionFromExisting(): static factory methodResourceVersions: nested class with GA API version constantsGenerates ProvisionableConstruct subclasses:
AssignOrReplace, BicepValue types use .Assign(), read-only properties are getter-onlyDefineProvisionableProperties() maps each property to its bicep pathGenerates simple C# enum types:
[DataMember(Name = "...")] attribute when the serialized value differs from the member nameDefineProperty at the resource/model levelThe output library bypasses the mgmt output pipeline (which would trigger mgmt-specific type initialization) and instead iterates input models and enums directly, routing each through our TypeFactory. Only resources are marked as "types to keep" — the post-processor automatically prunes unreferenced models and enums.
Azure.Provisioning.{ServiceName} (no .Models sub-namespace)model-namespace=false in the provisioning emitter, which prevents the base NamespaceVisitor from appending .ModelsResourceDataSuffixVisitor — the mgmt ResourceVisitor appends "Data" (e.g., ConfigurationStore → ConfigurationStoreData), and our visitor reverts it since provisioning libraries don't use the Data suffix conventionTypeFactory.CreatePropertyCore() pipeline, which runs mgmt visitors (specifically NameVisitor) to apply standard renames: Etag → ETag, CreationDate → CreatedOn, *Url → *Uri, and other datetime suffix normalizations. This ensures provisioning libraries follow the same naming conventions as mgmt libraries without duplicating rename rules.The new emitter-based generator coexists with the current reflection-based generator:
sdk/provisioning/Generator/ ← Current generator (reflection-based, for Swagger services)
eng/packages/http-client-csharp-provisioning/ ← New generator (emitter-based, for TypeSpec services)
Per-service migration:
Azure.ResourceManager.Storage gets tsp-location.yaml).tspconfig.yaml.tsp-location.yaml to the provisioning library pointing at the same TypeSpec project with the provisioning emitter.dotnet build /t:GenerateCode to generate provisioning code.sdk/provisioning/Generator/.Backward compatibility guarantee:
partial class customizations in non-generated code must continue to compile.