From 993f0ebd31745c63af58c9feef1b9cd6f12050b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:10:14 +0000 Subject: [PATCH 1/5] Initial plan From 47ad15af0033edbdcf0efe981a6a516d87d4c3cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:20:38 +0000 Subject: [PATCH 2/5] fix: wrap Content-Type header and content assignment in null check for optional body When the request body is optional, both the Content-Type header setting and the request.Content assignment are now wrapped in an if (content != null) block. This prevents setting Content-Type when no body is provided. For optional body without Content-Type header, the content assignment alone is wrapped in a null check. Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/RestClientProvider.cs | 34 ++++++++++++++-- .../RestClientProviderTests.cs | 40 +++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 17c09257962..ea5e3f67b42 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -264,7 +264,7 @@ private MethodBodyStatements BuildMessage( { var contentParam = signature.Parameters.FirstOrDefault(p => p.Name == "content" && p.Location == ParameterLocation.Body); statements.AddRange(AppendHeaderParameters(request, operation, paramMap, contentParam: contentParam)); - statements.AddRange(GetSetContent(request, signature.Parameters)); + statements.AddRange(GetSetContent(request, signature.Parameters, operation)); } // Apply request options and return message @@ -327,11 +327,33 @@ private Dictionary GetReinjectedParametersMap( return reinjectedParamsMap; } - private IReadOnlyList GetSetContent(HttpRequestApi request, IReadOnlyList parameters) + private IReadOnlyList GetSetContent(HttpRequestApi request, IReadOnlyList parameters, InputOperation operation) { var contentParam = parameters.FirstOrDefault( p => p.Location == ParameterLocation.Body); - return contentParam is null ? [] : [request.Content().Assign(contentParam).Terminate()]; + if (contentParam is null) + { + return []; + } + + // If body is optional, check if content assignment is already handled + var hasOptionalBody = operation.Parameters.Any(p => + p is InputBodyParameter bodyParam && !bodyParam.IsRequired); + if (hasOptionalBody) + { + // If there's a Content-Type header, the content assignment is already inside + // the if (content != null) block in AppendHeaderParameters + var hasContentTypeHeader = operation.Parameters.Any(p => + p is InputHeaderParameter h && h.IsContentType); + if (hasContentTypeHeader) + { + return []; + } + // No Content-Type header but optional body - wrap content assignment in null check + return [new IfStatement(contentParam.NotEqual(Null)) { request.Content().Assign(contentParam).Terminate() }]; + } + + return [request.Content().Assign(contentParam).Terminate()]; } private Dictionary, PropertyProvider> BuildPipelineMessage20xClassifiers() @@ -445,7 +467,11 @@ private IEnumerable AppendHeaderParameters(HttpRequestApi r if (hasOptionalBody) { - statement = new IfStatement(contentParam.NotEqual(Null)) { statement }; + statement = new IfStatement(contentParam.NotEqual(Null)) + { + statement, + request.Content().Assign(contentParam).Terminate() + }; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs index 0111de0dc0e..f22195ace3a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs @@ -1690,6 +1690,7 @@ public void ContentTypeHeaderWrappedInNullCheckWhenContentIsOptional() var expectedStatement = @"if ((content != null)) { request.Headers.Set(""Content-Type"", ""application/json""); + request.Content = content; } "; var statementsString = string.Join("\n", statements!.Select(s => s.ToDisplayString())); @@ -1744,6 +1745,45 @@ public void ContentTypeHeaderNotWrappedInNullCheckWhenContentIsRequired() $"Content-Type should NOT be wrapped in an if statement for required content, but found:\n{statementsString}"); } + [Test] + public void ContentAssignmentWrappedInNullCheckWhenBodyIsOptionalWithoutContentType() + { + // Test that when there's an optional body parameter without a Content-Type header, + // the content assignment is still wrapped in a null check + var bodyParam = InputFactory.BodyParameter( + "body", + InputPrimitiveType.String, + isRequired: false); + var operation = InputFactory.Operation( + "TestOperation", + requestMediaTypes: ["application/json"], + parameters: [bodyParam]); + var inputServiceMethod = InputFactory.BasicServiceMethod("Test", operation); + var inputClient = InputFactory.Client("TestClient", methods: [inputServiceMethod]); + MockHelpers.LoadMockGenerator(clients: () => [inputClient]); + + var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient); + Assert.IsNotNull(client); + + var restClient = client!.RestClient; + Assert.IsNotNull(restClient); + + var createMethod = restClient.Methods.FirstOrDefault(m => m.Signature.Name == "CreateTestOperationRequest"); + Assert.IsNotNull(createMethod, "CreateTestOperationRequest method not found"); + + var statements = createMethod!.BodyStatements as MethodBodyStatements; + Assert.IsNotNull(statements); + + var expectedStatement = @"if ((content != null)) +{ + request.Content = content; +} +"; + var statementsString = string.Join("\n", statements!.Select(s => s.ToDisplayString())); + Assert.IsTrue(statements!.Any(s => s.ToDisplayString() == expectedStatement), + $"Expected to find statement:\n{expectedStatement}\nBut got statements:\n{statementsString}"); + } + [Test] public async Task PageSizeParameterCasingPreservedFromLastContractView() { From b959b7021135a416a1a28fb4893837b38c824fee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:41:05 +0000 Subject: [PATCH 3/5] revert: undo source and test changes to address reviewer feedback Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/RestClientProvider.cs | 34 ++-------------- .../RestClientProviderTests.cs | 40 ------------------- 2 files changed, 4 insertions(+), 70 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index ea5e3f67b42..17c09257962 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -264,7 +264,7 @@ private MethodBodyStatements BuildMessage( { var contentParam = signature.Parameters.FirstOrDefault(p => p.Name == "content" && p.Location == ParameterLocation.Body); statements.AddRange(AppendHeaderParameters(request, operation, paramMap, contentParam: contentParam)); - statements.AddRange(GetSetContent(request, signature.Parameters, operation)); + statements.AddRange(GetSetContent(request, signature.Parameters)); } // Apply request options and return message @@ -327,33 +327,11 @@ private Dictionary GetReinjectedParametersMap( return reinjectedParamsMap; } - private IReadOnlyList GetSetContent(HttpRequestApi request, IReadOnlyList parameters, InputOperation operation) + private IReadOnlyList GetSetContent(HttpRequestApi request, IReadOnlyList parameters) { var contentParam = parameters.FirstOrDefault( p => p.Location == ParameterLocation.Body); - if (contentParam is null) - { - return []; - } - - // If body is optional, check if content assignment is already handled - var hasOptionalBody = operation.Parameters.Any(p => - p is InputBodyParameter bodyParam && !bodyParam.IsRequired); - if (hasOptionalBody) - { - // If there's a Content-Type header, the content assignment is already inside - // the if (content != null) block in AppendHeaderParameters - var hasContentTypeHeader = operation.Parameters.Any(p => - p is InputHeaderParameter h && h.IsContentType); - if (hasContentTypeHeader) - { - return []; - } - // No Content-Type header but optional body - wrap content assignment in null check - return [new IfStatement(contentParam.NotEqual(Null)) { request.Content().Assign(contentParam).Terminate() }]; - } - - return [request.Content().Assign(contentParam).Terminate()]; + return contentParam is null ? [] : [request.Content().Assign(contentParam).Terminate()]; } private Dictionary, PropertyProvider> BuildPipelineMessage20xClassifiers() @@ -467,11 +445,7 @@ private IEnumerable AppendHeaderParameters(HttpRequestApi r if (hasOptionalBody) { - statement = new IfStatement(contentParam.NotEqual(Null)) - { - statement, - request.Content().Assign(contentParam).Terminate() - }; + statement = new IfStatement(contentParam.NotEqual(Null)) { statement }; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs index f22195ace3a..0111de0dc0e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs @@ -1690,7 +1690,6 @@ public void ContentTypeHeaderWrappedInNullCheckWhenContentIsOptional() var expectedStatement = @"if ((content != null)) { request.Headers.Set(""Content-Type"", ""application/json""); - request.Content = content; } "; var statementsString = string.Join("\n", statements!.Select(s => s.ToDisplayString())); @@ -1745,45 +1744,6 @@ public void ContentTypeHeaderNotWrappedInNullCheckWhenContentIsRequired() $"Content-Type should NOT be wrapped in an if statement for required content, but found:\n{statementsString}"); } - [Test] - public void ContentAssignmentWrappedInNullCheckWhenBodyIsOptionalWithoutContentType() - { - // Test that when there's an optional body parameter without a Content-Type header, - // the content assignment is still wrapped in a null check - var bodyParam = InputFactory.BodyParameter( - "body", - InputPrimitiveType.String, - isRequired: false); - var operation = InputFactory.Operation( - "TestOperation", - requestMediaTypes: ["application/json"], - parameters: [bodyParam]); - var inputServiceMethod = InputFactory.BasicServiceMethod("Test", operation); - var inputClient = InputFactory.Client("TestClient", methods: [inputServiceMethod]); - MockHelpers.LoadMockGenerator(clients: () => [inputClient]); - - var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient); - Assert.IsNotNull(client); - - var restClient = client!.RestClient; - Assert.IsNotNull(restClient); - - var createMethod = restClient.Methods.FirstOrDefault(m => m.Signature.Name == "CreateTestOperationRequest"); - Assert.IsNotNull(createMethod, "CreateTestOperationRequest method not found"); - - var statements = createMethod!.BodyStatements as MethodBodyStatements; - Assert.IsNotNull(statements); - - var expectedStatement = @"if ((content != null)) -{ - request.Content = content; -} -"; - var statementsString = string.Join("\n", statements!.Select(s => s.ToDisplayString())); - Assert.IsTrue(statements!.Any(s => s.ToDisplayString() == expectedStatement), - $"Expected to find statement:\n{expectedStatement}\nBut got statements:\n{statementsString}"); - } - [Test] public async Task PageSizeParameterCasingPreservedFromLastContractView() { From 067ed6f4395a75d050c06de8ebfdd35f23043dbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:48:15 +0000 Subject: [PATCH 4/5] refactor: rewrite Content-Type tests to use TestData pattern per review feedback Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../RestClientProviderTests.cs | 66 +++++-------------- ...WrappedInNullCheckWhenContentIsRequired.cs | 24 +++++++ ...WrappedInNullCheckWhenContentIsOptional.cs | 27 ++++++++ 3 files changed, 67 insertions(+), 50 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeHeaderNotWrappedInNullCheckWhenContentIsRequired.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeHeaderWrappedInNullCheckWhenContentIsOptional.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs index 0111de0dc0e..200ffed2962 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs @@ -1655,8 +1655,6 @@ public void TestApiVersionParameterReinjectedInCreateNextRequestMethod() [Test] public void ContentTypeHeaderWrappedInNullCheckWhenContentIsOptional() { - // Test that when there's an optional body parameter with a Content-Type header, - // the Content-Type header setting is wrapped in a null check for the content parameter var contentTypeParam = InputFactory.HeaderParameter( "Content-Type", InputFactory.Literal.String("application/json"), @@ -1671,37 +1669,21 @@ public void ContentTypeHeaderWrappedInNullCheckWhenContentIsOptional() "TestOperation", requestMediaTypes: ["application/json"], parameters: [contentTypeParam, bodyParam]); - var inputServiceMethod = InputFactory.BasicServiceMethod("Test", operation); - var inputClient = InputFactory.Client("TestClient", methods: [inputServiceMethod]); - MockHelpers.LoadMockGenerator(clients: () => [inputClient]); - - var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient); - Assert.IsNotNull(client); - - var restClient = client!.RestClient; - Assert.IsNotNull(restClient); - - var createMethod = restClient.Methods.FirstOrDefault(m => m.Signature.Name == "CreateTestOperationRequest"); - Assert.IsNotNull(createMethod, "CreateTestOperationRequest method not found"); + var inputClient = InputFactory.Client( + "TestClient", + methods: [InputFactory.BasicServiceMethod("Test", operation)]); - var statements = createMethod!.BodyStatements as MethodBodyStatements; - Assert.IsNotNull(statements); + var clientProvider = new ClientProvider(inputClient); + var restClientProvider = new MockClientProvider(inputClient, clientProvider); - var expectedStatement = @"if ((content != null)) -{ - request.Headers.Set(""Content-Type"", ""application/json""); -} -"; - var statementsString = string.Join("\n", statements!.Select(s => s.ToDisplayString())); - Assert.IsTrue(statements!.Any(s => s.ToDisplayString() == expectedStatement), - $"Expected to find statement:\n{expectedStatement}\nBut got statements:\n{statementsString}"); + var writer = new TypeProviderWriter(restClientProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } [Test] public void ContentTypeHeaderNotWrappedInNullCheckWhenContentIsRequired() { - // Test that when there's a required body parameter with a Content-Type header, - // the Content-Type header setting is NOT wrapped in a null check var contentTypeParam = InputFactory.HeaderParameter( "Content-Type", InputFactory.Literal.String("application/json"), @@ -1716,32 +1698,16 @@ public void ContentTypeHeaderNotWrappedInNullCheckWhenContentIsRequired() "TestOperation", requestMediaTypes: ["application/json"], parameters: [contentTypeParam, bodyParam]); - var inputServiceMethod = InputFactory.BasicServiceMethod("Test", operation); - var inputClient = InputFactory.Client("TestClient", methods: [inputServiceMethod]); - MockHelpers.LoadMockGenerator(clients: () => [inputClient]); - - var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient); - Assert.IsNotNull(client); - - var restClient = client!.RestClient; - Assert.IsNotNull(restClient); - - var createMethod = restClient.Methods.FirstOrDefault(m => m.Signature.Name == "CreateTestOperationRequest"); - Assert.IsNotNull(createMethod, "CreateTestOperationRequest method not found"); + var inputClient = InputFactory.Client( + "TestClient", + methods: [InputFactory.BasicServiceMethod("Test", operation)]); - var statements = createMethod!.BodyStatements as MethodBodyStatements; - Assert.IsNotNull(statements); + var clientProvider = new ClientProvider(inputClient); + var restClientProvider = new MockClientProvider(inputClient, clientProvider); - // Verify there's no if statement wrapping the Content-Type header - var wrappedStatement = @"if ((content != null)) -{ - request.Headers.Set(""Content-Type"", ""application/json""); -} -"; - var statementsString = string.Join("\n", statements!.Select(s => s.ToDisplayString())); - var hasIfWrappedContentType = statements!.Any(s => s.ToDisplayString().Contains(wrappedStatement)); - Assert.IsFalse(hasIfWrappedContentType, - $"Content-Type should NOT be wrapped in an if statement for required content, but found:\n{statementsString}"); + var writer = new TypeProviderWriter(restClientProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } [Test] diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeHeaderNotWrappedInNullCheckWhenContentIsRequired.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeHeaderNotWrappedInNullCheckWhenContentIsRequired.cs new file mode 100644 index 00000000000..7444c570591 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeHeaderNotWrappedInNullCheckWhenContentIsRequired.cs @@ -0,0 +1,24 @@ +// + +#nullable disable + +using System.ClientModel; +using System.ClientModel.Primitives; + +namespace Sample +{ + public partial class TestClient + { + internal global::System.ClientModel.Primitives.PipelineMessage CreateTestOperationRequest(global::System.ClientModel.BinaryContent content, global::System.ClientModel.Primitives.RequestOptions options) + { + global::Sample.ClientUriBuilder uri = new global::Sample.ClientUriBuilder(); + uri.Reset(_endpoint); + global::System.ClientModel.Primitives.PipelineMessage message = Pipeline.CreateMessage(uri.ToUri(), "GET", PipelineMessageClassifier200); + global::System.ClientModel.Primitives.PipelineRequest request = message.Request; + request.Headers.Set("Content-Type", "application/json"); + request.Content = content; + message.Apply(options); + return message; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeHeaderWrappedInNullCheckWhenContentIsOptional.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeHeaderWrappedInNullCheckWhenContentIsOptional.cs new file mode 100644 index 00000000000..08028ebda4d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeHeaderWrappedInNullCheckWhenContentIsOptional.cs @@ -0,0 +1,27 @@ +// + +#nullable disable + +using System.ClientModel; +using System.ClientModel.Primitives; + +namespace Sample +{ + public partial class TestClient + { + internal global::System.ClientModel.Primitives.PipelineMessage CreateTestOperationRequest(global::System.ClientModel.BinaryContent content, global::System.ClientModel.Primitives.RequestOptions options) + { + global::Sample.ClientUriBuilder uri = new global::Sample.ClientUriBuilder(); + uri.Reset(_endpoint); + global::System.ClientModel.Primitives.PipelineMessage message = Pipeline.CreateMessage(uri.ToUri(), "GET", PipelineMessageClassifier200); + global::System.ClientModel.Primitives.PipelineRequest request = message.Request; + if ((content != null)) + { + request.Headers.Set("Content-Type", "application/json"); + } + request.Content = content; + message.Apply(options); + return message; + } + } +} From 0a5a7da9705a6e6f36eb1d5a11c05d2daec930de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:05:59 +0000 Subject: [PATCH 5/5] fix: handle edge case where optional Content-Type header bypasses content null check Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/RestClientProvider.cs | 20 +++++-------- .../RestClientProviderTests.cs | 29 +++++++++++++++++++ ...pedInNullCheckWhenContentTypeIsOptional.cs | 27 +++++++++++++++++ 3 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeHeaderWrappedInNullCheckWhenContentTypeIsOptional.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 17c09257962..f24c7c279f5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -431,22 +431,16 @@ private IEnumerable AppendHeaderParameters(HttpRequestApi r statement = request.SetHeaders([Literal(inputHeaderParameter.SerializedName), toStringExpression.As()]); } - if (!TryGetSpecialHeaderParam(inputHeaderParameter, out _) && (!inputHeaderParameter.IsRequired || type?.IsNullable == true || - (type is { IsValueType: false, IsFrameworkType: true } && type.FrameworkType != typeof(string)))) + // If this is a Content-Type header and there's an optional content parameter, wrap in content null check + if (inputHeaderParameter.IsContentType && contentParam != null && + operation.Parameters.Any(p => p is InputBodyParameter bodyParam && !bodyParam.IsRequired)) { - statement = BuildQueryOrHeaderOrPathParameterNullCheck(type, valueExpression, statement); + statement = new IfStatement(contentParam.NotEqual(Null)) { statement }; } - // If this is a Content-Type header and there's an optional content parameter, wrap in content null check - else if (inputHeaderParameter.IsContentType && contentParam != null) + else if (!TryGetSpecialHeaderParam(inputHeaderParameter, out _) && (!inputHeaderParameter.IsRequired || type?.IsNullable == true || + (type is { IsValueType: false, IsFrameworkType: true } && type.FrameworkType != typeof(string)))) { - // Check if any body parameter in the operation is optional - var hasOptionalBody = operation.Parameters.Any(p => - p is InputBodyParameter bodyParam && !bodyParam.IsRequired); - - if (hasOptionalBody) - { - statement = new IfStatement(contentParam.NotEqual(Null)) { statement }; - } + statement = BuildQueryOrHeaderOrPathParameterNullCheck(type, valueExpression, statement); } statements.Add(statement); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs index 200ffed2962..2a9638dab4b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs @@ -1710,6 +1710,35 @@ public void ContentTypeHeaderNotWrappedInNullCheckWhenContentIsRequired() Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } + [Test] + public void ContentTypeHeaderWrappedInNullCheckWhenContentTypeIsOptional() + { + var contentTypeParam = InputFactory.HeaderParameter( + "Content-Type", + InputFactory.Literal.String("application/xml"), + isRequired: false, + isContentType: true, + scope: InputParameterScope.Constant); + var bodyParam = InputFactory.BodyParameter( + "body", + InputPrimitiveType.String, + isRequired: false); + var operation = InputFactory.Operation( + "TestOperation", + requestMediaTypes: ["application/xml"], + parameters: [contentTypeParam, bodyParam]); + var inputClient = InputFactory.Client( + "TestClient", + methods: [InputFactory.BasicServiceMethod("Test", operation)]); + + var clientProvider = new ClientProvider(inputClient); + var restClientProvider = new MockClientProvider(inputClient, clientProvider); + + var writer = new TypeProviderWriter(restClientProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + [Test] public async Task PageSizeParameterCasingPreservedFromLastContractView() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeHeaderWrappedInNullCheckWhenContentTypeIsOptional.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeHeaderWrappedInNullCheckWhenContentTypeIsOptional.cs new file mode 100644 index 00000000000..47ab29490c4 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeHeaderWrappedInNullCheckWhenContentTypeIsOptional.cs @@ -0,0 +1,27 @@ +// + +#nullable disable + +using System.ClientModel; +using System.ClientModel.Primitives; + +namespace Sample +{ + public partial class TestClient + { + internal global::System.ClientModel.Primitives.PipelineMessage CreateTestOperationRequest(global::System.ClientModel.BinaryContent content, global::System.ClientModel.Primitives.RequestOptions options) + { + global::Sample.ClientUriBuilder uri = new global::Sample.ClientUriBuilder(); + uri.Reset(_endpoint); + global::System.ClientModel.Primitives.PipelineMessage message = Pipeline.CreateMessage(uri.ToUri(), "GET", PipelineMessageClassifier200); + global::System.ClientModel.Primitives.PipelineRequest request = message.Request; + if ((content != null)) + { + request.Headers.Set("Content-Type", "application/xml"); + } + request.Content = content; + message.Apply(options); + return message; + } + } +}