feat!: ship the v3 generated SDK surface#230
Conversation
…th URL builders, PublicClient Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The quickstart referenced WorkOS.SetApiKey and WorkOS.WorkOSClient, which do not exist on the current public API. The correct entry point is the WorkOSConfiguration static class. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, SSO and User Management services silently fell back to an
empty string ("") when WorkOSOptions.ClientId was not configured, which
led to a confusing 422 from the API rather than a clear client-side
error. Add WorkOSClient.RequireClientId() that throws
InvalidOperationException with an actionable message, document ClientId
on both WorkOSOptions and WorkOSClient, and update generated services to
call RequireClientId() instead of the silent null-coalesce. README gains
a Client ID configuration section.
Tests: every generated service test constructor now also sets
ClientId = "client_test" so services that require it work out of the box.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ListAuditLogConfiguration and ListOrganizationAuditLogsRetention return single objects, not collections, so the List prefix is misleading. Rename them (and their tests) to GetAuditLogConfiguration and GetOrganizationAuditLogsRetention to keep List* reserved for list/array/paginated responses. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add XML docs to ApiError and each subclass describing which HTTP status they map to and when the SDK raises them. Expand the README with an Error handling section (status-to-exception table plus a try/catch example) and a Retry behavior section that makes explicit that the SDK does not auto-retry and that callers are expected to wrap calls in their own retry policy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Passwordless, Vault, Actions, and Session are fully supported services that don't map to a single REST endpoint and therefore aren't generated from the OpenAPI spec. Add a Services section to the README so users can discover them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The generated enum types previously carried the stock
StringEnumConverter from Newtonsoft.Json, which throws on any enum value
the SDK does not recognize. This means a new value added server-side
immediately breaks existing clients.
Add WorkOSNewtonsoftStringEnumConverter, a Newtonsoft converter that:
* respects EnumMember(Value = "...") for both read and write,
* falls back to the enum's zero member (by convention, Unknown) for
unrecognized strings,
* handles Nullable<T> enum fields correctly.
All generated enum files now use this converter instead of
StringEnumConverter. The STJ side already had parity behavior via
WorkOSStringEnumConverterFactory and is unchanged. Adds targeted tests
covering both JSON stacks: known round-trip, unknown-value forward
compat, and write-of-Unknown.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AnyOf<T1,T2> and AnyOf<T1,T2,T3> previously serialized to
{"Value": ...} because the JSON stacks only saw a public Value property.
That produced invalid payloads for endpoints that accept free-form
metadata, notably AuditLogEvent.Metadata and
AuditLogEventActor.Metadata (Dictionary<string, AnyOf<string, double,
bool>>), where the server expects scalar JSON values.
Introduce AnyOfJsonConverter (Newtonsoft) and AnyOfJsonConverterFactory
(System.Text.Json). Both:
* write the inner value directly, and
* read by trying each type argument in declaration order, constructing
the AnyOf via its implicit conversion operator.
Attach both converters to AnyOf<,> and AnyOf<,,>. Includes tests for
the common AuditLog metadata shape on both JSON stacks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BuildUri only appended query strings for GET, and CreateHttpRequestMessage always sent Options as an HTTP body for non-GET methods. That meant a DELETE endpoint using query params (e.g. cascade_delete=true on DeleteOrganizationResource) had its options serialized into a body and was silently dropped by the server, which reads them from the query string. Route DELETE through the same query-string path as GET, and skip body construction on DELETE. Also teach FlattenQueryParameters about bool so bool query params serialize as `true`/`false` instead of being dropped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FlattenQueryParameters previously silently dropped values of most types, only serializing string, IEnumerable, DateTime, Enum, long, and int. Options that used bool, double, decimal, short, or DateTimeOffset fields had those fields omitted from the request — there was no warning and no error, which made the missing data very difficult to diagnose. Add cases for DateTimeOffset, bool, short, double, float, decimal, and a final IConvertible fallback. Use CultureInfo.InvariantCulture everywhere so hosts with non-US locales don't emit "3,14" instead of "3.14". CreateHttpContent's form-url-encoded path also previously deserialized directly into IDictionary<string, string>, which threw JsonException on any non-string field. Route it through the object-based flattener so it shares behavior (and stays in sync) with the query-string path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ConstructEvent handles signature verification and is the single entry point customers use to turn an HTTP payload into a typed Webhook event. It previously had no direct tests — only the primitives (signature compute, timestamp parse) were exercised. Add tests for the happy path plus the failure modes that matter for security: * valid signature -> Webhook deserializes correctly * tampered payload -> throws * wrong secret -> throws * expired timestamp (outside tolerance) -> throws * malformed signature header -> throws Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WorkOSEntity<T> was an empty generic marker: it carried no fields, methods, or behavior, and the original reasons for keeping it (RawResponse, UnmappedFields) are not on the roadmap. It only added visual noise and a generic type parameter to every generated entity. Delete the base class and strip `: WorkOSEntity<Foo>` from every generated entity file. Generated DTOs are now plain POCOs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
System.Text.Json (STJ) is a supported serializer alongside Newtonsoft, but until now nothing exercised it directly — the suite only used Newtonsoft via the runtime client. That meant any drift in the [STJS.JsonPropertyName] / [STJS.JsonConverter] coverage would have silently slipped through. Add a SystemTextJsonSupportTest that locks in: * snake_case property mapping on Organization * nested list and dictionary deserialization * round-trip preserving the wire-format keys * enum mapping via WorkOSStringEnumConverterFactory on Connection * forward-compat fallback to ConnectionState.Unknown on unknown values * basic Webhook envelope deserialization Also: Webhook.cs (a hand-maintained envelope) was missing all [STJS.JsonPropertyName] attributes, which broke STJ deserialization for the very payload customers most often pass through ConstructEvent. Add the missing annotations. Document the dual-serializer support in the README. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ListOptions, ListMetadata, WorkOSList<T>, and PaginationOrder are part of every paginated response in the SDK but lacked [STJS.JsonPropertyName] / [STJS.JsonConverter] attributes. Under STJ, that meant `data` and `list_metadata` could not be mapped onto their properties and `order` was serialized as the C# enum name instead of the wire value. Add the missing STJ annotations so the runtime pagination shape behaves identically on both serializers. Lock the behavior in with two tests: a list-page round trip on WorkOSList<Organization> and a PaginationOrder enum round trip via ListOptions. T05 (enums) and T06 (AnyOf) already added STJ parity for the other advanced shapes called out by the deep review. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
POST /user_management/password_reset *creates* a reset request (sends the email / mints the token). POST /user_management/password_reset/confirm is the call that actually resets the password using a valid token. The generated method names had these inverted: ResetPassword targeted the create endpoint and ConfirmPasswordReset targeted the reset endpoint. Rename: ResetPassword -> CreatePasswordReset ConfirmPasswordReset -> ResetPassword Options classes follow the same swap. Update tests accordingly. workos-dotnet is greenfield, so no Obsolete shims for the old names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tics" This reverts commit 953be11.
POST /user_management/authenticate is a oneOf union over eight grant
types. The emitter previously emitted both the typed AuthenticateWith*
wrappers (one per grant) AND a raw CreateAuthenticate that took an empty
UserManagementCreateAuthenticateOptions. There is no input you can
construct that makes the raw call succeed — every invocation 422s — so
the only thing the public method was good for was confusing callers.
Delete the raw method, its empty options class, and the auto-generated
test that verified the trap. Future calls go through the typed
AuthenticateWith{Password,Code,RefreshToken,MagicAuth,EmailVerification,
Totp,OrganizationSelection,DeviceCode} wrappers, which already carry
the correct grant_type and required fields.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… shape
The eight AuthenticateWith*Options classes had two recurring problems:
* required fields (Code, RefreshToken, Email, Password,
PendingAuthenticationToken, OrganizationId, DeviceCode, etc.) were
declared as nullable string?, so the C# type system gave callers no
signal that the field was required and the SDK happily sent
half-formed payloads to the server.
* the optional fraud/audit fields the spec accepts on every variant
(ip_address, device_id, user_agent) were dropped entirely.
Update each options class so that:
* spec-required exposed params are non-nullable `string ... = default!`,
* ip_address / device_id / user_agent are present as nullable strings,
* hidden grant_type / client_id / client_secret remain internal.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three SDK methods named GetAuthorizationUrl / GetLogoutUrl returned Task/void and issued real HTTP requests to the WorkOS authorize/logout endpoints. The names imply local URL construction, the endpoints expect the *user's browser* (not the SDK) to redirect there, and most callers ended up triggering an unwanted server-side state change. Add WorkOSClient.BuildRequestUri(WorkOSRequest) that constructs the same URI the SDK would send, without performing any I/O. Rewrite the four URL-builder methods (UserManagement.GetAuthorizationUrl, UserManagement.GetLogoutUrl, SSO.GetAuthorizationUrl, SSO.GetLogoutUrl) to return the URL as a string. Update their tests to assert URL shape instead of mocking HTTP. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…Async
The eight AuthenticateWith* methods each duplicated the same 8 lines of
WorkOSRequest construction and MakeAPIRequest call. That made adding a
new grant type a copy-paste exercise and made it easy to drift on
shared invariants (path, method, response type).
Extract a private SendAuthenticateAsync helper that owns the request
build and POST. Each wrapper shrinks to: set grant_type, set client_id
via RequireClientId(), conditionally set client_secret, delegate.
Make ClientSecret inclusion explicit and tested:
* seven wrappers set ClientSecret = client.ApiKey
* AuthenticateWithDeviceCode does NOT (public-client device flow)
* the comment on that wrapper explains why
* a new AuthenticateWrappersTest asserts each wrapper's wire body
contains/omits client_secret as appropriate
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two systemic naming misses surfaced from the deep review:
CreateEndpoints -> CreateEndpoint (POST /webhooks/endpoints)
CreateApplications dropped entirely (POST /connect/applications is a
union split with two typed
wrappers — the raw method took
an empty options class and was
dead weight)
Rename WebhooksCreateEndpointsOptions to WebhooksCreateEndpointOptions
to match. Delete ConnectCreateApplicationsOptions and the corresponding
TestCreateApplications case, since the raw CreateApplications method is
gone (CreateOAuthApplication / CreateM2MApplication remain).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add EntityRoundTripTest that loads a fixture, deserializes, re-serializes, and structurally compares the result for representative entities under both Newtonsoft and System.Text.Json. Catches drift in property naming, enum mapping, date handling, and nested-object shape that key-only checks would miss. The structural compare is permissive about extra keys in the output (the SDK may include explicit nulls the fixture omits) and treats date strings as instants so "...000Z" and "...+00:00" are considered equal, which avoids false positives from serializer-specific date formatting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
HttpMock previously only let tests check that a request was *made* on a
given method+path. The body and query string went uninspected, so a
serializer regression that produced a totally wrong payload would have
passed the suite as long as the URL was right.
Add two HttpMock helpers:
* AssertRequestBodyJsonContainsAsync(expectedJson) — structural JSON
subset assertion against the captured request body, with a clear
JSON-path on failure.
* AssertQueryParam(key, value) — URL-decoded query-param assertion.
Add RequestPayloadShapeTest demonstrating both helpers against
Organizations.Create and Organizations.List. Used as the pattern
generated service tests should adopt next.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lpers on Service
Every generated service method previously had its own ~8-line WorkOSRequest
build + MakeAPIRequest call: the same boilerplate repeated across ~156
operations. Add the missing transport methods to the Service base so an
emitter can collapse a typical operation to a one-liner like:
return this.PostAsync<Foo>($"/foo/{id}", options, requestOptions, cancellationToken);
ListAutoPagingAsync is also exposed at the base so paginated wrappers can
delegate without rebuilding the request.
Existing generated methods still work unchanged — switching the emitter
to delegate through the helpers is a follow-up that requires a regen
pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ListAutoPagingAsync mutated the caller's own ListOptions instance in place to advance the `after` cursor, and it never cleared `before` when doing so. Two concrete bugs: 1. Caller-visible state drift — after iterating, the caller's options object now carried a stale `after` cursor from the last page. 2. Page 2 could send both `before` and `after` simultaneously, which the API rejects. Clone the options via a new `BaseOptions.Clone()` virtual (MemberwiseClone on the concrete subclass) and build a fresh WorkOSRequest so pagination mutates only our copy. Clear `before` whenever we set `after`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous flattener serialized options with Newtonsoft, deserialized back into IDictionary<string, object>, then walked the dict. That round-trip erased int/short/decimal/float type info (all numbers came back as long or double), let nested objects fall through to ToString() soup, and reintroduced bool casing ambiguity every time the spec evolved. Walk the options object graph directly via reflection instead. Respect [JsonProperty] for wire names and DefaultValueHandling.Ignore, respect [JsonIgnore] for full exclusion, and flatten nested objects with bracket notation. Dates still serialize in ISO 8601 UTC, enums still render as their member name, booleans are lowercase, floats use the round-trip format. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extend HttpMock with two opt-in primitives: - AssertQueryParams(IDictionary<string,string>): assert every expected key/value pair appears on the last captured request's query string with an exact value match. Extra keys are tolerated. - MockResponseExact(method, path, query, bodyPredicate, status, body): strict variant of MockResponse that only matches when both the query params and (optionally) the request body match. Paired with MockBehavior.Strict, a mismatched query or body now fails the test on SendAsync instead of returning silently. Keeps the existing MockResponse untouched so generated tests can migrate in place without a mass rewrite. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cumulative regen from the emitter-side work (P0.1, P0.3, P0.4, P1.8–10,
P2.11–14, P3.15–16) plus the IR-level enum synthesis (P0.2). Notable
user-visible changes:
- Required enum fields skip serializing the Unknown sentinel so unset
required enums produce a clean missing-field API error instead of
`"unknown"` on the wire.
- Service methods collapse to one-line GetAsync/PostAsync/… calls on
the Service base; hundreds of inlined WorkOSRequest blocks removed.
- Duplicate enum classes collapse to canonical names; parent classes
that only differed by aliased children dedupe further.
- Literal / single-value enum fields ("object": "event", …) emit as
string with an internal setter and const initializer.
- Dictionary<string, object> fields get typed Get<Field>Attribute<T>
accessors that work under Newtonsoft and STJ.
- oneOf-of-string-consts (e.g. UserIdentitiesGetItem.provider) now
materialize as a real enum (UserIdentitiesGetItemProvider) instead
of collapsing to `object`.
- Generated tests seed required string body/query fields and assert
against the exact fixture values, catching snake_case regressions.
- URL-builder ops (GetAuthorizationUrl, GetLogoutUrl) get sync URL-
structure tests instead of awaited HttpMock calls.
- XML doc summaries preserve acronyms (SSO, API, …) and pick the
right indefinite article.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make WorkOSClient a partial class and move generated service accessor properties (backing fields + lazy-init properties) from the hand- maintained file to WorkOSClient.Generated.cs. The generated partial class adds XML <summary> docs to each service accessor property. Hand-maintained services (Passwordless, Vault, Actions, Session) remain in the hand-maintained file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ect> Regenerated output from emitter changes that generate synthetic models and enums for inline definitions in oneOf branches: - ConnectApplication.RedirectUris: List<object> -> List<ConnectApplicationRedirectUri> with typed Uri (string) and Default (bool) properties - DataIntegrationAccessTokenResponse.AccessToken: object? -> DataIntegrationAccessTokenResponseAccessToken with typed fields (AccessToken, ExpiresAt, Scopes, MissingScopes) - DataIntegrationAccessTokenResponse.Error: string? -> DataIntegrationAccessTokenResponseError enum (NeedsReauthorization, NotInstalled) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Alias classes now emit the model's actual schema description in their XML <summary> tag instead of "structurally identical to X". The structural note is preserved in <remarks> with a <see cref> link. Also adds synthetic models for EventSchema inline objects (EventSchemaContext, EventSchemaContextActor, EventSchemaData). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Regenerated options classes now emit [System.Obsolete] attributes for deprecated body fields, matching the existing behavior for deprecated query parameters. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements the sdk-runtime-contract §2 requirements: - Retry 429 and 5xx responses with exponential backoff + full jitter - Honor Retry-After header as a minimum delay floor - Auto-generate Idempotency-Key for POST requests when not provided - Configurable via WorkOSOptions.MaxRetries (default 2) and per-request override via RequestOptions.MaxRetries Adds test coverage for contract §2, §3, §4, §6: - Error-path tests for 401, 404, 422, 429, 500, 400 - Pagination tests for empty page and multi-page auto-pagination - Retry behavior tests (backoff, Retry-After, per-request override) - Auto-idempotency key generation for POST requests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| } | ||
| } | ||
|
|
||
| // Forward-compat: unknown API enum values deserialize to the zero |
There was a problem hiding this comment.
Why I believe the unknown enum values need to be 0
There was a problem hiding this comment.
I think this is correct. WorkOSNewtonsoftStringEnumConverter (and the STJ counterpart) fall back to Enum.ToObject(underlying, 0) for unrecognized values — which lands on Unknown because it's the zero member. This gives us forward-compatibility: if the API adds a new enum value the SDK doesn't know about yet, it deserializes cleanly instead of throwing.
| [STJS.JsonConverter(typeof(WorkOSStringEnumConverterFactory))] | ||
| public enum ApplicationsOrder | ||
| { | ||
| [EnumMember(Value = "unknown")] |
There was a problem hiding this comment.
Does this need to be EnumMember(Value = 0)]
There was a problem hiding this comment.
This is the JSON string that gets sent to/from the API, not the numeric index — so Value = 0 wouldn't compile. The numeric 0 is already implicit (it's the first member). Value = "unknown" just ensures that if this member is ever serialized to JSON, it writes lowercase "unknown" rather than PascalCase "Unknown".
| { | ||
|
|
||
| /// <summary>Unique identifier for the event.</summary> | ||
| [JsonProperty("id")] |
There was a problem hiding this comment.
This sucks that we need to have both attributes on all of these.
Could we go with a convention based approach? Would we ever actually use a different JSON key then the camelcase version of the property name?
// System.Text.Json
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
// Newtonsoft
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
| @@ -0,0 +1,8 @@ | |||
| // This file is auto-generated by oagen. Do not edit. | |||
|
|
|||
| namespace WorkOS | |||
| @@ -0,0 +1,52 @@ | |||
| // @oagen-ignore-file | |||
| namespace WorkOS | |||
There was a problem hiding this comment.
Should we consider:
OneOf library
The OneOf NuGet package is the community standard for discriminated unions in C#:
csharppublic Dictionary<string, OneOf<string, double, bool>>? Metadata { get; set; }
| /// status code (e.g. 400, 403, 409) surfaces as an <see cref="ApiError"/> directly. | ||
| /// The <see cref="Exception.Message"/> contains the raw response body. | ||
| /// </summary> | ||
| public class ApiError : Exception |
There was a problem hiding this comment.
We should probably call this ApiException instead of ApiError
| } | ||
|
|
||
| /// <summary>Thrown when the API rejects the provided credentials (HTTP 401).</summary> | ||
| public class AuthenticationError : ApiError |
There was a problem hiding this comment.
Same here... in C# the terminology is Exception
…ed JSON, OneOf - Rename *Error → *Exception (ApiError→ApiException, AuthenticationError→ AuthenticationException, etc.) to follow C# naming conventions - Remove per-property [JsonProperty]/[JsonPropertyName] attributes in favor of global SnakeCaseNamingStrategy on both Newtonsoft and STJ serializers - Replace custom AnyOf<T1,T2> with OneOf NuGet package (community standard for discriminated unions, supports up to 9 type params) - Regenerate all entities, options, and tests from updated emitter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Model references are now rewritten to the canonical name via mapTypeRef,
so the empty `class Foo : Bar { }` alias files are no longer needed.
Removes 93 dead entity files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…re catch - Fix README retry docs (was incorrectly saying SDK doesn't retry) - Add Async suffix to VaultService and SessionService methods with compat wrappers - Refactor VaultService to use Service base class helpers (GetAsync/PostAsync/etc.) - Have SessionService extend Service base class - Add XML docs to SessionAuthResult and SessionRefreshResult properties - Fix bare catch in RequestUtilities.FlattenObject → catch (TargetInvocationException) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…refs - Add Code, ErrorMessage, Errors, and RawBody properties to ApiException, parsed best-effort from the JSON response body - Remove internal spec references (H04-H07, H13) from SessionService doc - Add tests for structured error parsing (JSON, non-JSON, errors array) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
List/Create/Get/Update/DeleteWorkOStoWorkOSConfiguration, targets.NET 8, and centralizes auth-relatedclient_idconfiguration onWorkOSOptions/WorkOSClientRequestOptions, adds automatic retries with exponential backoff plus POST auto-idempotency headers, and exposes per-request retry / API key overridesApiErrorsubclasses instead of relying on best-effort response deserializationoneOf/AnyOf, enums, query and form encoding, and regenerated DTO coverage, while preserving hand-maintained helpers like Passwordless, Vault, Actions, Session, PKCE, auth URL builders, and the public clientdocs/V3_MIGRATION_GUIDE.mdto document the breaking changes and upgrade path from v2-style integrations