From 935cfb9c80a5ac43dd91baeebb7b49fcd6e02fc2 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Fri, 7 Nov 2025 00:40:03 +0100 Subject: [PATCH 1/5] Add support for OpenAPI default values in optional parameters --- src/Refitter.Core/ParameterExtractor.cs | 54 +++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/src/Refitter.Core/ParameterExtractor.cs b/src/Refitter.Core/ParameterExtractor.cs index 6dfd4f97f..1554b2d39 100644 --- a/src/Refitter.Core/ParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtractor.cs @@ -86,7 +86,7 @@ public static IEnumerable GetParameters( parameters.AddRange(formParameters); parameters.AddRange(binaryBodyParameters); - parameters = ReOrderNullableParameters(parameters, settings); + parameters = ReOrderNullableParameters(parameters, settings, operationModel.Parameters); if (settings.ApizrSettings?.WithRequestOptions == true) parameters.Add("[RequestOptions] IApizrRequestOptions options"); @@ -116,7 +116,8 @@ private static string ReplaceUnsafeCharacters( private static List ReOrderNullableParameters( List parameters, - RefitGeneratorSettings settings) + RefitGeneratorSettings settings, + ICollection parameterModels) { if (!settings.OptionalParameters || settings.ApizrSettings?.WithRequestOptions == true) return parameters; @@ -125,12 +126,59 @@ private static List ReOrderNullableParameters( for (int index = 0; index < parameters.Count; index++) { if (parameters[index].Contains("?")) - parameters[index] += " = default"; + { + var parameterString = parameters[index]; + var defaultValue = GetDefaultValueForParameter(parameterString, parameterModels); + parameters[index] = parameterString + " = " + defaultValue; + } } return parameters; } + private static string GetDefaultValueForParameter(string parameterString, ICollection parameterModels) + { + // Extract variable name from the parameter string + var parts = parameterString.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) + return "default"; + + var variableName = parts[parts.Length - 1].TrimEnd(';', ','); + + // Find the matching parameter model + var parameterModel = parameterModels.FirstOrDefault(p => p.VariableName == variableName); + if (parameterModel?.Schema?.Default != null) + { + return FormatDefaultValue(parameterModel.Schema.Default, parameterModel.Type); + } + + return "default"; + } + + private static string FormatDefaultValue(object defaultValue, string parameterType) + { + if (defaultValue == null) + return "default"; + + var type = parameterType.TrimEnd('?').Trim(); + + return type switch + { + "bool" => defaultValue.ToString()?.ToLowerInvariant() ?? "default", + "string" => $"\"{defaultValue}\"", + _ when IsNumericType(type) => defaultValue.ToString() ?? "default", + _ => "default" + }; + } + + private static bool IsNumericType(string type) + { + return type is "int" or "Int32" or "long" or "Int64" or "short" or "Int16" + or "byte" or "Byte" or "decimal" or "Decimal" or "float" or "Single" + or "double" or "Double" or "sbyte" or "SByte" or "uint" or "UInt32" + or "ulong" or "UInt64" or "ushort" or "UInt16"; + } + private static string GetQueryAttribute(CSharpParameterModel parameter, RefitGeneratorSettings settings) { return (parameter, settings) switch From d4435c49d48b9bfd5a323afaf6e3a1423f5eea55 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Fri, 7 Nov 2025 00:40:09 +0100 Subject: [PATCH 2/5] Add tests for optional parameters with default values --- ...ptionalParametersWithDefaultValuesTests.cs | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesTests.cs diff --git a/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesTests.cs b/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesTests.cs new file mode 100644 index 000000000..8fef8f1b4 --- /dev/null +++ b/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesTests.cs @@ -0,0 +1,137 @@ +using FluentAssertions; +using Refitter.Core; +using Refitter.Tests.Build; +using Refitter.Tests.TestUtilities; +using Xunit; + +namespace Refitter.Tests.Examples; + +public class OptionalParametersWithDefaultValuesTests +{ + private const string OpenApiSpec = @" +{ + ""openapi"": ""3.0.1"", + ""info"": { + ""title"": ""Test API"", + ""version"": ""v1"" + }, + ""paths"": { + ""/api/schedule/list"": { + ""get"": { + ""tags"": [""Schedule""], + ""operationId"": ""List"", + ""parameters"": [ + { + ""name"": ""start"", + ""in"": ""query"", + ""required"": true, + ""schema"": { + ""type"": ""string"", + ""format"": ""date"" + } + }, + { + ""name"": ""end"", + ""in"": ""query"", + ""required"": true, + ""schema"": { + ""type"": ""string"", + ""format"": ""date"" + } + }, + { + ""name"": ""includeCancelled"", + ""in"": ""query"", + ""required"": false, + ""schema"": { + ""type"": ""boolean"", + ""default"": true + } + }, + { + ""name"": ""pageSize"", + ""in"": ""query"", + ""required"": false, + ""schema"": { + ""type"": ""integer"", + ""format"": ""int32"", + ""default"": 10 + } + }, + { + ""name"": ""filter"", + ""in"": ""query"", + ""required"": false, + ""schema"": { + ""type"": ""string"", + ""default"": ""active"" + } + } + ], + ""responses"": { + ""200"": { + ""description"": ""Success"", + ""content"": { + ""application/json"": { + ""schema"": { + ""type"": ""array"", + ""items"": { + ""type"": ""object"" + } + } + } + } + } + } + } + } + } +}"; + + [Fact] + public async Task Can_Generate_Code() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Generated_Code_Should_Have_Optional_Parameters_With_Default_Values() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().Contain("bool? includeCancelled = true"); + generatedCode.Should().Contain("int? pageSize = 10"); + generatedCode.Should().Contain("string? filter = \"active\""); + } + + [Fact] + public async Task Generated_Code_Should_Have_Required_Parameters_Without_Defaults() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().Contain("[Query] System.DateTimeOffset start"); + generatedCode.Should().Contain("[Query] System.DateTimeOffset end"); + generatedCode.Should().NotContain("System.DateTimeOffset start ="); + generatedCode.Should().NotContain("System.DateTimeOffset end ="); + } + + [Fact] + public async Task Can_Build_Generated_Code() + { + string generatedCode = await GenerateCode(); + BuildHelper.BuildCSharp(generatedCode).Should().BeTrue(); + } + + private static async Task GenerateCode() + { + var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(OpenApiSpec); + var settings = new RefitGeneratorSettings + { + OpenApiPath = swaggerFile, + OptionalParameters = true + }; + + var sut = await RefitGenerator.CreateAsync(settings); + var code = sut.Generate(); + return code; + } +} From 181f74ed1650b69f5a3abc418ff3ff1f305d0160 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Fri, 7 Nov 2025 09:43:01 +0100 Subject: [PATCH 3/5] Use raw string for JSON example --- ...ptionalParametersWithDefaultValuesTests.cs | 146 +++++++++--------- 1 file changed, 74 insertions(+), 72 deletions(-) diff --git a/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesTests.cs b/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesTests.cs index 8fef8f1b4..4f34c2f0c 100644 --- a/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesTests.cs +++ b/src/Refitter.Tests/Examples/OptionalParametersWithDefaultValuesTests.cs @@ -8,85 +8,87 @@ namespace Refitter.Tests.Examples; public class OptionalParametersWithDefaultValuesTests { - private const string OpenApiSpec = @" -{ - ""openapi"": ""3.0.1"", - ""info"": { - ""title"": ""Test API"", - ""version"": ""v1"" - }, - ""paths"": { - ""/api/schedule/list"": { - ""get"": { - ""tags"": [""Schedule""], - ""operationId"": ""List"", - ""parameters"": [ - { - ""name"": ""start"", - ""in"": ""query"", - ""required"": true, - ""schema"": { - ""type"": ""string"", - ""format"": ""date"" - } - }, - { - ""name"": ""end"", - ""in"": ""query"", - ""required"": true, - ""schema"": { - ""type"": ""string"", - ""format"": ""date"" - } - }, - { - ""name"": ""includeCancelled"", - ""in"": ""query"", - ""required"": false, - ""schema"": { - ""type"": ""boolean"", - ""default"": true - } - }, - { - ""name"": ""pageSize"", - ""in"": ""query"", - ""required"": false, - ""schema"": { - ""type"": ""integer"", - ""format"": ""int32"", - ""default"": 10 - } + private const string OpenApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" }, - { - ""name"": ""filter"", - ""in"": ""query"", - ""required"": false, - ""schema"": { - ""type"": ""string"", - ""default"": ""active"" - } - } - ], - ""responses"": { - ""200"": { - ""description"": ""Success"", - ""content"": { - ""application/json"": { - ""schema"": { - ""type"": ""array"", - ""items"": { - ""type"": ""object"" + "paths": { + "/api/schedule/list": { + "get": { + "tags": ["Schedule"], + "operationId": "List", + "parameters": [ + { + "name": "start", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "end", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "includeCancelled", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + }, + { + "name": "filter", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "active" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } } } } } } } - } - } - } -}"; + """; [Fact] public async Task Can_Generate_Code() From 8091fe4146ddfeb6bfdeba153a2e7f9f9522659b Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Fri, 7 Nov 2025 09:43:33 +0100 Subject: [PATCH 4/5] Refactor `ParameterExtractor` methods --- src/Refitter.Core/ParameterExtractor.cs | 27 +++++++++++-------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/Refitter.Core/ParameterExtractor.cs b/src/Refitter.Core/ParameterExtractor.cs index 1554b2d39..026b7fbd1 100644 --- a/src/Refitter.Core/ParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtractor.cs @@ -138,24 +138,19 @@ private static List ReOrderNullableParameters( private static string GetDefaultValueForParameter(string parameterString, ICollection parameterModels) { - // Extract variable name from the parameter string - var parts = parameterString.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + var parts = parameterString.Split([' '], StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0) return "default"; var variableName = parts[parts.Length - 1].TrimEnd(';', ','); - // Find the matching parameter model var parameterModel = parameterModels.FirstOrDefault(p => p.VariableName == variableName); - if (parameterModel?.Schema?.Default != null) - { - return FormatDefaultValue(parameterModel.Schema.Default, parameterModel.Type); - } - - return "default"; + return parameterModel?.Schema?.Default != null + ? FormatDefaultValue(parameterModel.Schema.Default, parameterModel.Type) + : "default"; } - private static string FormatDefaultValue(object defaultValue, string parameterType) + private static string FormatDefaultValue(object? defaultValue, string parameterType) { if (defaultValue == null) return "default"; @@ -204,9 +199,11 @@ private static string GetAliasAsAttribute(CSharpParameterModel parameterModel) = private static string JoinAttributes(params string[] attributes) { - var filteredAttributes = attributes.Where(a => !string.IsNullOrWhiteSpace(a)); + var filteredAttributes = attributes + .Where(a => !string.IsNullOrWhiteSpace(a)) + .ToList(); - if (!filteredAttributes.Any()) + if (filteredAttributes.Count == 0) return string.Empty; return "[" + string.Join(", ", filteredAttributes) + "] "; @@ -364,12 +361,12 @@ private static List GetQueryParameters(CSharpOperationModel operationMod private static void AppendXmlDocComment(string description, StringBuilder codeBuilder) { codeBuilder.Append( -$$""" +""" /// """); var lines = description.Split( - new[] { "\r\n", "\r", "\n" }, + ["\r\n", "\r", "\n"], StringSplitOptions.None); foreach (var line in lines) @@ -383,7 +380,7 @@ private static void AppendXmlDocComment(string description, StringBuilder codeBu codeBuilder.AppendLine(); codeBuilder.Append( -$$""" +""" /// """); codeBuilder.AppendLine(); From 284bdc853570f543e204f27d38dfca44192ceef9 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Fri, 7 Nov 2025 13:20:50 +0100 Subject: [PATCH 5/5] Add default value support for optional parameters when using dynamic query string parameter classes --- src/Refitter.Core/ParameterExtractor.cs | 6 + ...yStringParametersWithDefaultValuesTests.cs | 140 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 src/Refitter.Tests/Examples/DynamicQueryStringParametersWithDefaultValuesTests.cs diff --git a/src/Refitter.Core/ParameterExtractor.cs b/src/Refitter.Core/ParameterExtractor.cs index 026b7fbd1..003d0ef9b 100644 --- a/src/Refitter.Core/ParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtractor.cs @@ -312,6 +312,12 @@ private static List GetQueryParameters(CSharpOperationModel operationMod {{attributes}} {{modifier}} {{propertyType}} {{propertyName}} { get; {{setterStyle}}; } """); + var defaultValue = operationParameter.Schema.Default; + if (defaultValue != null) + { + var formattedDefaultValue = FormatDefaultValue(defaultValue, propertyType); + propertiesCodeBuilder.Append($" = {formattedDefaultValue};"); + } propertiesCodeBuilder.AppendLine(); operationModel.Parameters.Remove(operationParameter); } diff --git a/src/Refitter.Tests/Examples/DynamicQueryStringParametersWithDefaultValuesTests.cs b/src/Refitter.Tests/Examples/DynamicQueryStringParametersWithDefaultValuesTests.cs new file mode 100644 index 000000000..ac838a9eb --- /dev/null +++ b/src/Refitter.Tests/Examples/DynamicQueryStringParametersWithDefaultValuesTests.cs @@ -0,0 +1,140 @@ +using FluentAssertions; +using Refitter.Core; +using Refitter.Tests.Build; +using Refitter.Tests.TestUtilities; +using Xunit; + +namespace Refitter.Tests.Examples; + +public class DynamicQueryStringParametersWithDefaultValuesTests +{ + private const string OpenApiSpec = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "v1" + }, + "paths": { + "/api/schedule/list": { + "get": { + "tags": ["Schedule"], + "operationId": "List", + "parameters": [ + { + "name": "start", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "end", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "includeCancelled", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + }, + { + "name": "filter", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "active" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + } + } + """; + + [Fact] + public async Task Can_Generate_Code() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Generated_Code_Should_Have_Optional_Parameters_With_Default_Values() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().Contain("bool? IncludeCancelled { get; set; } = true"); + generatedCode.Should().Contain("int? PageSize { get; set; } = 10"); + generatedCode.Should().Contain("string? Filter { get; set; } = \"active\""); + } + + [Fact] + public async Task Generated_Code_Should_Have_Required_Parameters_Without_Defaults() + { + string generatedCode = await GenerateCode(); + generatedCode.Should().Contain("System.DateTimeOffset Start { get; set; }"); + generatedCode.Should().Contain("System.DateTimeOffset End { get; set; }"); + generatedCode.Should().NotContain("System.DateTimeOffset Start { get; set; } ="); + generatedCode.Should().NotContain("System.DateTimeOffset End { get; set; } ="); + } + + [Fact] + public async Task Can_Build_Generated_Code() + { + string generatedCode = await GenerateCode(); + BuildHelper.BuildCSharp(generatedCode).Should().BeTrue(); + } + + private static async Task GenerateCode() + { + var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(OpenApiSpec); + var settings = new RefitGeneratorSettings + { + OpenApiPath = swaggerFile, + OptionalParameters = true, + UseDynamicQuerystringParameters = true, + }; + + var sut = await RefitGenerator.CreateAsync(settings); + var code = sut.Generate(); + return code; + } +}