diff --git a/generators/csharp/base/src/context/CsharpTypeMapper.ts b/generators/csharp/base/src/context/CsharpTypeMapper.ts index 93234701815c..446dc01b6ed4 100644 --- a/generators/csharp/base/src/context/CsharpTypeMapper.ts +++ b/generators/csharp/base/src/context/CsharpTypeMapper.ts @@ -59,7 +59,8 @@ export class CsharpTypeMapper extends WithGeneration { return property.isOptional ? this.Types.FileParameter.asOptional() : this.Types.FileParameter; } case "fileArray": { - return property.isOptional ? this.Types.FileParameter.asOptional() : this.Types.FileParameter; + const listType = this.Collection.list(this.Types.FileParameter); + return property.isOptional ? listType.asOptional() : listType; } default: assertNever(property); diff --git a/generators/csharp/dynamic-snippets/src/context/DynamicLiteralMapper.ts b/generators/csharp/dynamic-snippets/src/context/DynamicLiteralMapper.ts index 4d943073ac9c..7d866f6d0991 100644 --- a/generators/csharp/dynamic-snippets/src/context/DynamicLiteralMapper.ts +++ b/generators/csharp/dynamic-snippets/src/context/DynamicLiteralMapper.ts @@ -140,6 +140,11 @@ export class DynamicLiteralMapper extends WithGeneration { value: unknown; fallbackToDefault?: string; }): ast.Literal { + // When generateLiterals is enabled, inline literal properties use `= new()` + // default initializers in the C# model and must not be set in the snippet. + if (this.settings.generateLiterals) { + return this.csharp.Literal.nop(); + } switch (literal.type) { case "boolean": { const bool = this.context.getValueAsBoolean({ value }); @@ -240,6 +245,21 @@ export class DynamicLiteralMapper extends WithGeneration { }): ast.Literal { switch (named.type) { case "alias": + // When generateLiterals is enabled and the alias resolves to a literal, + // the C# model emits a readonly struct (e.g. `FormatMp3`) instead of a + // raw string/bool. Instantiate it with `new TypeName()` rather than + // emitting a plain literal value. + if (this.settings.generateLiterals && named.typeReference.type === "literal") { + return this.csharp.Literal.reference( + this.csharp.instantiateClass({ + classReference: this.csharp.classReference({ + origin: named.declaration, + namespace: this.context.getNamespace(named.declaration.fernFilepath) + }), + arguments_: [] + }) + ); + } return this.convert({ typeReference: named.typeReference, value, as, fallbackToDefault }); case "discriminatedUnion": if (this.settings.shouldGeneratedDiscriminatedUnions) { diff --git a/generators/csharp/dynamic-snippets/src/context/DynamicTypeMapper.ts b/generators/csharp/dynamic-snippets/src/context/DynamicTypeMapper.ts index 80295c5a3b4f..9a8d98c2c1eb 100644 --- a/generators/csharp/dynamic-snippets/src/context/DynamicTypeMapper.ts +++ b/generators/csharp/dynamic-snippets/src/context/DynamicTypeMapper.ts @@ -68,6 +68,12 @@ export class DynamicTypeMapper extends WithGeneration { private convertNamed({ named }: { named: FernIr.dynamic.NamedType }): ast.Type { switch (named.type) { case "alias": + if (this.settings.generateLiterals && named.typeReference.type === "literal") { + return this.csharp.classReference({ + origin: named.declaration, + namespace: this.context.getNamespace(named.declaration.fernFilepath) + }); + } return this.convert({ typeReference: named.typeReference }); case "enum": case "object": diff --git a/generators/csharp/sdk/src/generateSdkTests.ts b/generators/csharp/sdk/src/generateSdkTests.ts index b9f2adafaeae..df104269ad16 100644 --- a/generators/csharp/sdk/src/generateSdkTests.ts +++ b/generators/csharp/sdk/src/generateSdkTests.ts @@ -30,7 +30,17 @@ function generateMockServerTests({ context }: { context: SdkGeneratorContext }): // TODO: support other response body types const useableExamples = allExamples.filter((example): example is FernIr.ExampleEndpointCall => { const response = example?.response; - return response?.type === "ok" && response.value.type === "body"; + if (response?.type !== "ok" || response.value.type !== "body") { + return false; + } + // Skip examples with empty string path parameters. An empty path parameter + // causes a URL mismatch: the mock server registers a collapsed path (e.g. + // /v0/tools/version/1) while the SDK client sends a double-slash path (e.g. + // /v0/tools//version/1), resulting in a 404 from WireMock. + if (example != null && hasEmptyPathParameter(example)) { + return false; + } + return true; }); if (useableExamples.length === 0) { continue; @@ -49,6 +59,15 @@ function generateMockServerTests({ context }: { context: SdkGeneratorContext }): return files; } +function hasEmptyPathParameter(example: FernIr.ExampleEndpointCall): boolean { + const allPathParams = [ + ...example.rootPathParameters, + ...example.servicePathParameters, + ...example.endpointPathParameters + ]; + return allPathParams.some((param) => param.value.jsonExample === ""); +} + function shouldSkipMockServerTestForEndpoint({ endpoint }: { endpoint: FernIr.HttpEndpoint }): boolean { const responseBodyType = endpoint.response?.body?.type; if (responseBodyType === "fileDownload" || responseBodyType === "streamParameter") { diff --git a/generators/csharp/sdk/versions.yml b/generators/csharp/sdk/versions.yml index 876485a5dd9c..3e6773b9c777 100644 --- a/generators/csharp/sdk/versions.yml +++ b/generators/csharp/sdk/versions.yml @@ -1,4 +1,26 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 2.59.5 + changelogEntry: + - summary: | + Skip mock server test examples with empty string path parameters. An + empty path parameter causes the mock server to register a collapsed URL + (e.g. `/v0/tools/version/1`) while the SDK client sends a double-slash + URL (e.g. `/v0/tools//version/1`), resulting in a 404 from WireMock. + type: fix + - summary: | + Fix dynamic snippet generation for literal types when `generate-literals` + (or `experimental-readonly-constants`) is enabled. Inline literal + properties now correctly omit the value (relying on the `= new()` + default initializer), and named literal alias types emit `new TypeName()` + instead of a raw string, preventing CS0029 compilation errors. + type: fix + - summary: | + Fix file upload request properties declared as `list` generating + a single `FileParameter` field instead of `List`. + type: fix + createdAt: "2026-04-10" + irVersion: 66 + - version: 2.59.4 changelogEntry: - summary: | diff --git a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Requests/MyOtherRequest.cs b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Requests/MyOtherRequest.cs index d6b7121f7a4e..bf70fc1aea00 100644 --- a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Requests/MyOtherRequest.cs +++ b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Requests/MyOtherRequest.cs @@ -11,11 +11,11 @@ public record MyOtherRequest public required FileParameter File { get; set; } - public required FileParameter FileList { get; set; } + public IEnumerable FileList { get; set; } = new List(); public FileParameter? MaybeFile { get; set; } - public FileParameter? MaybeFileList { get; set; } + public IEnumerable? MaybeFileList { get; set; } public int? MaybeInteger { get; set; } diff --git a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Requests/MyRequest.cs b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Requests/MyRequest.cs index 43b3a09e3564..2d5b9b596800 100644 --- a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Requests/MyRequest.cs +++ b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Requests/MyRequest.cs @@ -11,11 +11,11 @@ public record MyRequest public required FileParameter File { get; set; } - public required FileParameter FileList { get; set; } + public IEnumerable FileList { get; set; } = new List(); public FileParameter? MaybeFile { get; set; } - public FileParameter? MaybeFileList { get; set; } + public IEnumerable? MaybeFileList { get; set; } public int? MaybeInteger { get; set; } diff --git a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/ServiceClient.cs b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/ServiceClient.cs index 8ed6c8968220..7fc4212bf0a0 100644 --- a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/ServiceClient.cs +++ b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/ServiceClient.cs @@ -300,9 +300,9 @@ public async Task PostAsync( multipartFormRequest_.AddStringPart("maybe_string", request.MaybeString); multipartFormRequest_.AddStringPart("integer", request.Integer); multipartFormRequest_.AddFileParameterPart("file", request.File); - multipartFormRequest_.AddFileParameterPart("file_list", request.FileList); + multipartFormRequest_.AddFileParameterParts("file_list", request.FileList); multipartFormRequest_.AddFileParameterPart("maybe_file", request.MaybeFile); - multipartFormRequest_.AddFileParameterPart("maybe_file_list", request.MaybeFileList); + multipartFormRequest_.AddFileParameterParts("maybe_file_list", request.MaybeFileList); multipartFormRequest_.AddStringPart("maybe_integer", request.MaybeInteger); multipartFormRequest_.AddStringParts( "optional_list_of_strings", @@ -582,9 +582,9 @@ public async Task WithFormEncodedContainersAsync( multipartFormRequest_.AddFormEncodedPart("maybe_string", request.MaybeString); multipartFormRequest_.AddFormEncodedPart("integer", request.Integer); multipartFormRequest_.AddFileParameterPart("file", request.File); - multipartFormRequest_.AddFileParameterPart("file_list", request.FileList); + multipartFormRequest_.AddFileParameterParts("file_list", request.FileList); multipartFormRequest_.AddFileParameterPart("maybe_file", request.MaybeFile); - multipartFormRequest_.AddFileParameterPart("maybe_file_list", request.MaybeFileList); + multipartFormRequest_.AddFileParameterParts("maybe_file_list", request.MaybeFileList); multipartFormRequest_.AddFormEncodedPart("maybe_integer", request.MaybeInteger); multipartFormRequest_.AddFormEncodedParts( "optional_list_of_strings", diff --git a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example0.cs b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example0.cs index 32ca711fea3d..7319feb9c885 100644 --- a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example0.cs +++ b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example0.cs @@ -13,8 +13,6 @@ public async Task Do() { await client.Headers.SendAsync( new SendLiteralsInHeadersRequest { - EndpointVersion = "02-12-2024", - Async = true, Query = "What is the weather today" } ); diff --git a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example1.cs b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example1.cs index 267779a87a65..f843954448d1 100644 --- a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example1.cs +++ b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example1.cs @@ -13,8 +13,6 @@ public async Task Do() { await client.Headers.SendAsync( new SendLiteralsInHeadersRequest { - EndpointVersion = "02-12-2024", - Async = true, Query = "query" } ); diff --git a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example2.cs b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example2.cs index 1ecae4a063ef..ba630fe045e7 100644 --- a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example2.cs +++ b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example2.cs @@ -14,16 +14,11 @@ public async Task Do() { await client.Inlined.SendAsync( new SendLiteralsInlinedRequest { Temperature = 10.1, - Prompt = "You are a helpful assistant", - Context = "You're super wise", - AliasedContext = "You're super wise", - MaybeContext = "You're super wise", + AliasedContext = new SomeAliasedLiteral(), + MaybeContext = new SomeAliasedLiteral(), ObjectWithLiteral = new ATopLevelLiteral { - NestedLiteral = new ANestedLiteral { - MyLiteral = "How super cool" - } + NestedLiteral = new ANestedLiteral() }, - Stream = false, Query = "What is the weather today" } ); diff --git a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example3.cs b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example3.cs index 8233afcf91a1..0f6c70cd1244 100644 --- a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example3.cs +++ b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example3.cs @@ -13,17 +13,12 @@ public async Task Do() { await client.Inlined.SendAsync( new SendLiteralsInlinedRequest { - Prompt = "You are a helpful assistant", - Context = "You're super wise", Query = "query", Temperature = 1.1, - Stream = false, - AliasedContext = "You're super wise", - MaybeContext = "You're super wise", + AliasedContext = new SomeAliasedLiteral(), + MaybeContext = new SomeAliasedLiteral(), ObjectWithLiteral = new ATopLevelLiteral { - NestedLiteral = new ANestedLiteral { - MyLiteral = "How super cool" - } + NestedLiteral = new ANestedLiteral() } } ); diff --git a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example4.cs b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example4.cs index 7cf2fc180d7b..2dd01ef35bea 100644 --- a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example4.cs +++ b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example4.cs @@ -12,7 +12,6 @@ public async Task Do() { ); await client.Path.SendAsync( - "123" ); } diff --git a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example5.cs b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example5.cs index 58f01da22d45..d0913e36b5f3 100644 --- a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example5.cs +++ b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example5.cs @@ -12,7 +12,6 @@ public async Task Do() { ); await client.Path.SendAsync( - "123" ); } diff --git a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example6.cs b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example6.cs index 175fa7d8b737..cd97f9175ea4 100644 --- a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example6.cs +++ b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example6.cs @@ -13,14 +13,10 @@ public async Task Do() { await client.Query.SendAsync( new SendLiteralsInQueryRequest { - Prompt = "You are a helpful assistant", - OptionalPrompt = "You are a helpful assistant", - AliasPrompt = "You are a helpful assistant", - AliasOptionalPrompt = "You are a helpful assistant", - Stream = false, - OptionalStream = false, - AliasStream = false, - AliasOptionalStream = false, + AliasPrompt = new AliasToPrompt(), + AliasOptionalPrompt = new AliasToPrompt(), + AliasStream = new AliasToStream(), + AliasOptionalStream = new AliasToStream(), Query = "What is the weather today" } ); diff --git a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example7.cs b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example7.cs index 85574f435075..0d89a2c34e39 100644 --- a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example7.cs +++ b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example7.cs @@ -13,15 +13,11 @@ public async Task Do() { await client.Query.SendAsync( new SendLiteralsInQueryRequest { - Prompt = "You are a helpful assistant", - OptionalPrompt = "You are a helpful assistant", - AliasPrompt = "You are a helpful assistant", - AliasOptionalPrompt = "You are a helpful assistant", + AliasPrompt = new AliasToPrompt(), + AliasOptionalPrompt = new AliasToPrompt(), Query = "query", - Stream = false, - OptionalStream = false, - AliasStream = false, - AliasOptionalStream = false + AliasStream = new AliasToStream(), + AliasOptionalStream = new AliasToStream() } ); } diff --git a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example8.cs b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example8.cs index e00a846308d6..f9dba1508443 100644 --- a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example8.cs +++ b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example8.cs @@ -13,21 +13,16 @@ public async Task Do() { await client.Reference.SendAsync( new SendRequest { - Prompt = "You are a helpful assistant", - Stream = false, - Context = "You're super wise", + Context = new SomeLiteral(), Query = "What is the weather today", ContainerObject = new ContainerObject { NestedObjects = new List(){ new NestedObjectWithLiterals { - Literal1 = "literal1", - Literal2 = "literal2", StrProp = "strProp" }, } - }, - Ending = "$ending" + } } ); } diff --git a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example9.cs b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example9.cs index ffd035b778d5..b2367951ef71 100644 --- a/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example9.cs +++ b/seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example9.cs @@ -13,22 +13,15 @@ public async Task Do() { await client.Reference.SendAsync( new SendRequest { - Prompt = "You are a helpful assistant", Query = "query", - Stream = false, - Ending = "$ending", - Context = "You're super wise", - MaybeContext = "You're super wise", + Context = new SomeLiteral(), + MaybeContext = new SomeLiteral(), ContainerObject = new ContainerObject { NestedObjects = new List(){ new NestedObjectWithLiterals { - Literal1 = "literal1", - Literal2 = "literal2", StrProp = "strProp" }, new NestedObjectWithLiterals { - Literal1 = "literal1", - Literal2 = "literal2", StrProp = "strProp" }, } diff --git a/seed/csharp-sdk/seed.yml b/seed/csharp-sdk/seed.yml index cf465b5f48b3..abe0aa620ac7 100644 --- a/seed/csharp-sdk/seed.yml +++ b/seed/csharp-sdk/seed.yml @@ -398,7 +398,6 @@ fixtures: redact-response-body-on-error: true outputFolder: redact-response-body-on-error allowedFailures: - - schemaless-request-body-examples - bytes-upload - enum:forward-compatible-enums - imdb:exported-client-class-name @@ -412,10 +411,7 @@ allowedFailures: - package-yml - pagination-uri-path - property-access - - trace - unions:no-custom-config - unions-with-local-date - server-sent-event-examples - - oauth-client-credentials-openapi - - server-sent-events-openapi - - streaming-parameter + - server-sent-events-openapi \ No newline at end of file