Skip to content

Add C# SDK (Pulumi.Esc.Sdk)#113

Closed
dbeattie71 wants to merge 9 commits intopulumi:mainfrom
dbeattie71:add-csharp-sdk
Closed

Add C# SDK (Pulumi.Esc.Sdk)#113
dbeattie71 wants to merge 9 commits intopulumi:mainfrom
dbeattie71:add-csharp-sdk

Conversation

@dbeattie71
Copy link

Adds a C# SDK for the Pulumi ESC API, following the same generated + hand-written wrapper pattern as the Go, Python, and TypeScript SDKs.

What's included

Generated code (OpenAPI Generator 7.6.0, csharp / generichost library):

  • Full async API client targeting net6.0
  • All models with nullable reference types enabled
  • DI-based IEscApi with IServiceCollection extensions

Hand-written wrappers:

  • EscClient.cs — High-level convenience wrapper with ~30 methods, factory methods (Create, CreateDefault), IDisposable support
  • EscAuth.cs — Reads credentials from PULUMI_ACCESS_TOKEN env var or ~/.pulumi/credentials.json (Pulumi CLI + ESC CLI paths)
  • ValueMapper.cs — Recursive Value tree → plain .NET object unwrapping
  • EnvironmentDefinitionSerializer.cs — YamlDotNet-based YAML ↔ EnvironmentDefinition serialization

Template overrides:

  • partial_header.mustache — Pulumi copyright header on all generated files
  • netcore_project.mustache — Custom .csproj with Pulumi branding, YamlDotNet dependency

Tests:

  • 30 unit tests (EscAuth, ValueMapper, EnvironmentDefinitionSerializer)
  • Integration test (full CRUD lifecycle, requires PULUMI_ACCESS_TOKEN + PULUMI_ORG)

Makefile targets:

  • generate_csharp_client_sdk, build_csharp, test_csharp, test_csharp_integration

Method parity with Go SDK

Full parity with api_esc_extensions.go:

Category Methods
Environments CreateEnvironment, CloneEnvironment, DeleteEnvironment, ListEnvironments
Get/Update GetEnvironment, GetEnvironmentAtVersion, GetEnvironmentYaml, GetEnvironmentAtVersionYaml, UpdateEnvironment, UpdateEnvironmentYaml
Open/Read OpenEnvironment, OpenEnvironmentAtVersion, ReadOpenEnvironment, ReadOpenEnvironmentProperty, OpenAndReadEnvironment, OpenAndReadEnvironmentAtVersion
Decrypt DecryptEnvironment, DecryptEnvironmentYaml
Validate CheckEnvironment, CheckEnvironmentYaml
Revision tags ListEnvironmentRevisionTags, ListEnvironmentRevisionTagsPaginated, GetEnvironmentRevisionTag, CreateEnvironmentRevisionTag, UpdateEnvironmentRevisionTag, DeleteEnvironmentRevisionTag
Environment tags ListEnvironmentTags, ListEnvironmentTagsPaginated, GetEnvironmentTag, CreateEnvironmentTag, UpdateEnvironmentTag, DeleteEnvironmentTag
Revisions ListEnvironmentRevisions, ListEnvironmentRevisionsPaginated

Bugs found and fixed in generated code

Bug Root Cause Fix
ReadOpenEnvironmentProperty returns 401 .NET Uri normalizes /// in swagger path /open//{sessionID} Wrapper calls ReadOpenEnvironment + navigates Value tree
UpdateEnvironmentYaml sends garbled YAML JsonSerializer.Serialize(string) wraps YAML in JSON quotes Wrapper sends raw YAML via direct HttpClient call
CreateEnvironment creates under wrong project Generated constructor is (name, project) but intuitively called as (project, name) Wrapper passes args in correct order
CheckEnvironmentYaml fails on Value deserialization Swagger marks value as required on Value but API returns {\"unknown\":true} without it Removed required check in Value.cs, protected via .openapi-generator-ignore

Usage

using Pulumi.Esc.Sdk;

using var client = EscClient.CreateDefault();

// Create an environment
await client.CreateEnvironmentAsync(\"myOrg\", \"myProject\", \"dev\");

// Update with YAML
await client.UpdateEnvironmentYamlAsync(\"myOrg\", \"myProject\", \"dev\",
    \"values:\\n  greeting: hello world\\n\");

// Open, resolve secrets, and read values
var (env, values) = await client.OpenAndReadEnvironmentAsync(\"myOrg\", \"myProject\", \"dev\");
Console.WriteLine(values[\"greeting\"]); // \"hello world\"

Checklist

  • Builds clean on net6.0 (make build_csharp)
  • 30 unit tests passing (make test_csharp)
  • Full method parity with Go SDK
  • .openapi-generator-ignore protects hand-written files
  • README, CONTRIBUTING, CHANGELOG_PENDING updated
  • Integration tests pass (make test_csharp_integration — requires API credentials)
  • NuGet package publish workflow (future PR)

- Generate C# SDK using openapi-generator v7.6.0 (csharp/generichost)
- Target net6.0 to match Pulumi .NET ecosystem
- Package: Pulumi.Esc.Sdk at sdk/csharp/Pulumi.Esc.Sdk/
- Hand-written wrappers: EscClient.cs, EscAuth.cs, ValueMapper.cs
- Custom .csproj template at sdk/templates/csharp/
- Makefile targets: generate_csharp_client_sdk, build_csharp, test_csharp
- .openapi-generator-ignore blocks extra artifacts (api/, docs/, appveyor.yml)
- .gitignore updated for C# bin/obj directories
…onmentDefinitionSerializer

- Override partial_header.mustache template for Pulumi copyright on all generated files
- Add X-Pulumi-Source and User-Agent headers in EscClient.BuildClient()
- Add YamlDotNet 16.3.0 dependency via .csproj template
- Create EnvironmentDefinitionSerializer.cs for YAML <-> EnvironmentDefinition
- Add convenience methods: GetEnvironmentAsync, GetEnvironmentAtVersionAsync, DecryptEnvironmentAsync
- Fix .openapi-generator-ignore docs pattern (docs/* -> docs/**)
- Add xUnit test project (Pulumi.Esc.Sdk.Tests)
- EscAuthTests: 6 tests for env var, credential file reading (shared fixtures), ESC override, URL conversion
- ValueMapperTests: 13 tests for Value tree unwrapping, JsonElement handling
- EnvironmentDefinitionSerializerTests: 8 tests for YAML/JSON serialization round-trips
- EscApiTests: E2E integration test (full CRUD lifecycle, tagged Category=Integration)
- Fix EscCredentials JSON field: 'currentAccountName' -> 'name' (matches Python/TypeScript SDKs)
- Block generated Pulumi.Esc.Sdk.Test/ stubs in .openapi-generator-ignore
- README: Add C# install, quick example, API reference link
- CONTRIBUTING: Add build_csharp, test_csharp, test_csharp_integration targets, .NET 6.0 prereq
- CHANGELOG_PENDING: Note C# SDK addition
- Makefile: test_csharp filters out integration tests, add test_csharp_integration target
- Fix YAML body serialization: bypass generated JsonSerializer for
  UpdateEnvironmentYaml and CheckEnvironmentYaml (send raw YAML)
- Fix CreateEnvironment/CloneEnvironment constructor arg order
- Fix Value deserialization: remove required check on 'value' field
  (API omits it when unknown=true)
- Fix .sln project paths (src\ prefix removed)
- Add UpdateEnvironmentAsync and CheckEnvironmentAsync accepting
  EnvironmentDefinition (serializes to YAML, matching Go SDK parity)
- Add .gitignore for C# SDK (exclude .vs/, bin/, obj/)
- Protect Value.cs and .sln in .openapi-generator-ignore
…full Go test parity

- Fix SerializeToYaml to include AdditionalProperties from
  EnvironmentDefinitionValues (foo, my_secret, my_array, etc.)
- Fix Deserialize to capture non-typed YAML keys into
  AdditionalProperties as JsonElements
- Add helper methods: ObjectToJsonElement, JsonElementToObject,
  ConvertYamlValue for bridging YamlDotNet ↔ System.Text.Json
- Add 3 unit tests for AdditionalProperties round-tripping
- Rewrite integration test to match Go SDK test structure:
  - Separate test methods: FullLifecycle, CheckEnvironment_Valid,
    CheckEnvironmentYaml_Invalid
  - Verify resolved values: my_array, pulumiConfig, environmentVariables
  - Test UpdateEnvironmentAsync(EnvironmentDefinition) with versioned property
  - Test OpenAndReadEnvironmentAtVersion with revision tags
  - Verify envDef structure via AssertEnvDef helper
  - Verify tag values
  - Clean up clone project envs in RemoveAll
- Add test_csharp job to stage-test.yml (runs make test_csharp)
- Add publish-csharp-sdk job to stage-publish-sdk.yml (pack + nuget push)
- Lint check-sdk-generation-clean already covers C# via generate_sdks
@dbeattie71 dbeattie71 marked this pull request as ready for review February 11, 2026 14:59
@tehsis
Copy link
Contributor

tehsis commented Feb 11, 2026

Thank you very much for your contribution @dbeattie71 we trully appreciate it!

I'd be more than happy to merge your code once some issues are addressed.

  1. Copyright notice is missing from a few files.
sdk/csharp/Pulumi.Esc.Sdk/Client/ApiResponseEventArgs.cs
sdk/csharp/Pulumi.Esc.Sdk/Client/ApiFactory.cs
sdk/csharp/Pulumi.Esc.Sdk/Api/IApi.cs
sdk/csharp/Pulumi.Esc.Sdk/Client/TokenBase.cs
sdk/csharp/Pulumi.Esc.Sdk/Client/JsonSerializerOptionsProvider.cs
sdk/csharp/Pulumi.Esc.Sdk/Client/CookieContainer.cs
sdk/csharp/Pulumi.Esc.Sdk/Client/ExceptionEventArgs.cs
sdk/csharp/Pulumi.Esc.Sdk/Client/TokenContainer.cs

They need to show the following notice: Copyright 2026, Pulumi Corporation. All rights reserved.

You can use pulumictl tool to validate them.

  1. A few commited files don't match what the generator produces.
    We need to add Pulumi.Esc.Sdk/README.md. to .openapi-generator-ignore
    sdk/csharp/Pulumi.Esc.Sdk/Api/EscApi.cs seems to contain a blank line after the header comment

The latter should be fixed by running make generate_csharp_client_sdk and commit the result.

Let me know if you are able to address these issues.

Thanks!

- Add 9 custom Mustache template overrides in sdk/templates/csharp/libraries/generichost/
  to fix missing copyright notices in generated files (upstream OpenAPI Generator bug:
  templates used {{partial_header}} variable instead of {{>partial_header}} partial include)
- Update copyright year to 2026 in partial_header.mustache
- Add Pulumi.Esc.Sdk/README.md to .openapi-generator-ignore
- Regenerate all C# SDK files with correct copyright headers
- Verified with pulumictl copyright
@dbeattie71
Copy link
Author

@tehsis Those issues should be fixed and validated with pulimictl

.NET 10 disables reflection-based serialization by default. When the
net6.0-targeted SDK runs on a .NET 10 runtime, JsonSerializer.Deserialize
fails because JsonSerializerOptions has no TypeInfoResolver set.

Changes:
- Add JsonDefaults.cs with runtime reflection to detect and set
  DefaultJsonTypeInfoResolver when available (.NET 7+)
- HostConfiguration.cs: use JsonDefaults.EnsureTypeInfoResolver()
  when initializing JsonSerializerOptions
- Add HostConfiguration.mustache template override so the fix
  survives SDK regeneration
- EscAuth.cs: pass JsonDefaults.Options to bare Deserialize calls
  for credentials.json parsing
- EscClient.cs: change ResolvePropertyPath from static to instance
  method to use _jsonSerializerOptions with custom converters
- Add JsonDeserializationTests.cs with 8 unit tests covering the
  affected deserialization paths
@tehsis tehsis mentioned this pull request Feb 13, 2026
7 tasks
@tehsis
Copy link
Contributor

tehsis commented Feb 17, 2026

Thanks @dbeattie71! I have rebased your work and addressed some issues (unrelated to this PR) at #118

@tehsis tehsis closed this Feb 17, 2026
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