Skip to content

feat: add azure.provisioning extension (C# CDK importer)#7463

Draft
m-nash wants to merge 1 commit intoAzure:ext-importer-supportfrom
m-nash:feature/code-provider-extension
Draft

feat: add azure.provisioning extension (C# CDK importer)#7463
m-nash wants to merge 1 commit intoAzure:ext-importer-supportfrom
m-nash:feature/code-provider-extension

Conversation

@m-nash
Copy link
Copy Markdown
Member

@m-nash m-nash commented Apr 2, 2026

Summary

Adds an azd extension (azure.provisioning) that enables defining Azure infrastructure in C# using Azure.Provisioning instead of Bicep. Built on the new importer-provider capability from #7452.

The extension compiles C# to Bicep as an intermediate step — azd's built-in Bicep provider handles deployment, preview, state tracking, and destroy. Zero changes to azd core.

How it works

# azure.yaml
infra:
  importer:
    name: csharp
    options:
      path: infra

The user writes infra/infra.cs (or a .csproj project) using Azure.Provisioning, then runs azd up as normal. The extension:

  1. Detects the C# entry point (single .cs file or .csproj project)
  2. Runs dotnet run to compile C# to Bicep
  3. Returns the generated Bicep files to azd via the importer interface
  4. azd's Bicep provider deploys, previews, or destroys as usual

Testing

All scenarios verified E2E against live Azure:

Scenario Result
Single .cs file provision Pass
.csproj project provision Pass
azd provision --preview Pass
azd down Pass
azd init --template Pass
Options forwarding from azure.yaml Pass
Full todo app (7 resources + Playwright test) Pass

Sample repo: m-nash/todo-csharp-cosmos-sql-dotnet — full todo app with Cosmos DB, App Service, Key Vault, App Insights, all defined in a single infra.cs file.

Gaps to consider

1. Auto-detection — infra.importer config is required

Today, azd auto-detects .bicep → Bicep and .tf → Terraform without any infra: config in azure.yaml. The same should work for C# — a user should be able to drop an infra.cs into the infra/ folder and run azd up without any yaml changes.

Currently this requires the explicit infra.importer block because the auto-detection path does not consult extension importers. Ideally, when no provider is detected from built-in file checks, azd would ask each installed extension importer if it can handle the current infra directory, and let the extension decide based on whatever criteria makes sense (file types, markers, etc.). This keeps azd core language-agnostic while letting extensions opt in to auto-detection.

2. CLI argument passthrough

The extension forwards infra.importer.options from azure.yaml as --key value args to the C# program. This works for static configuration but there is no way to pass runtime arguments from the CLI (e.g. azd provision -- --region westus3).

We would strongly encourage azd to support CLI arg passthrough to importer extensions. Code-based CDK programs are written in full programming languages — C#, TypeScript, Java, Python — and developers will naturally use command-line arguments to parameterize their infrastructure. If a standalone dotnet run infra.cs -- --region westus3 (or npx ts-node infra.ts -- --region westus3) works but the same program requires manual wiring through azure.yaml options when run via azd, that friction becomes a real adoption blocker. Users should not have to restructure their code to work with azd.

Add an azd extension that enables defining Azure infrastructure in C#
using Azure.Provisioning. The extension implements the importer-provider
capability to compile C# to Bicep, which azd's Bicep provider then
deploys.

Supports:
- .NET 10 single-file apps (infra.cs with #:package directives)
- Traditional .csproj multi-file projects
- Options forwarding from azure.yaml to C# program args
- Preview, provision, and destroy via Bicep delegation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Member

@jongio jongio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issues to address:

  • importer_csharp.go:253 - readGeneratedFiles only reads top-level; nested Bicep modules get silently dropped
  • commands.go:14 - root command missing standard extension conventions (SilenceUsage, SilenceErrors, --debug, --no-prompt, metadata/version commands)
  • commands.go:23 - listen command should be Hidden: true
  • importer_csharp.go:156 - hasCSharpInfra is dead code, unused linter will flag
  • importer_csharp.go:210 - use slices.Sorted(maps.Keys(...)) per Go 1.26 conventions
  • importer_csharp.go:56,111 - ProjectInfrastructure and GenerateAllInfrastructure duplicate the resolve-compile-read pipeline

Missing from PR: go.mod, go.sum, build scripts, version.txt, CHANGELOG.md, .golangci.yaml.

}

// readGeneratedFiles reads all .bicep and .json files from a directory.
func readGeneratedFiles(dir string) ([]*azdext.GeneratedFile, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] This only reads top-level entries. If the C# program generates nested Bicep modules (e.g., modules/storage.bicep), they're silently dropped and deployment fails with missing module references.

Use filepath.WalkDir to recurse subdirectories and preserve relative paths with filepath.ToSlash.

"github.com/spf13/cobra"
)

func NewRootCommand() *cobra.Command {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Missing standard extension conventions: SilenceUsage: true, SilenceErrors: true, CompletionOptions, hidden help command, --debug/--no-prompt persistent flags, and metadata/version subcommands. See azure.appservice root.go for the pattern.

return root
}

func newListenCommand() *cobra.Command {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Should be Hidden: true - this is an internal protocol command, not user-facing. Every other extension hides it.

}

// hasCSharpInfra checks if a directory contains .cs or .csproj files.
func hasCSharpInfra(path string) bool {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Dead code - not called anywhere. The unused linter will flag this. Remove it, or if auto-detection is planned, wire it into CanImport when that's ready.


// optionsToArgs converts the importer options map to --key value CLI args,
// excluding the "path" key which is used for directory resolution.
func optionsToArgs(options map[string]string) []string {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Go 1.26 convention: use slices.Sorted(maps.Keys(options)) instead of manual key collection + sort.Strings. Drops the sort import entirely.

}

// ProjectInfrastructure compiles C# Azure.Provisioning code to Bicep for `azd provision`.
func (p *CSharpImporterProvider) ProjectInfrastructure(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] ProjectInfrastructure and GenerateAllInfrastructure duplicate the resolve-compile-read pipeline. Extract a shared helper like compileCSharpToBicep(ctx, projectPath, options) to avoid divergence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants