Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 53 additions & 8 deletions src/Refitter.Core/ParameterExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public static IEnumerable<string> 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");
Expand Down Expand Up @@ -116,7 +116,8 @@ private static string ReplaceUnsafeCharacters(

private static List<string> ReOrderNullableParameters(
List<string> parameters,
RefitGeneratorSettings settings)
RefitGeneratorSettings settings,
ICollection<CSharpParameterModel> parameterModels)
{
if (!settings.OptionalParameters || settings.ApizrSettings?.WithRequestOptions == true)
return parameters;
Expand All @@ -125,12 +126,54 @@ private static List<string> 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<CSharpParameterModel> parameterModels)
{
var parts = parameterString.Split([' '], StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0)
return "default";

var variableName = parts[parts.Length - 1].TrimEnd(';', ',');

var parameterModel = parameterModels.FirstOrDefault(p => p.VariableName == variableName);
return parameterModel?.Schema?.Default != null

Copilot AI Nov 7, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code doesn't handle the case where parameterModel.Type could be null or empty. While this is unlikely given that it comes from NSwag's code generation, it would be safer to add a null check to prevent potential NullReferenceException.

Suggestion:

var parameterModel = parameterModels.FirstOrDefault(p => p.VariableName == variableName);
return parameterModel?.Schema?.Default != null && !string.IsNullOrEmpty(parameterModel.Type)
    ? FormatDefaultValue(parameterModel.Schema.Default, parameterModel.Type)
    : "default";
Suggested change
return parameterModel?.Schema?.Default != null
return parameterModel?.Schema?.Default != null && !string.IsNullOrEmpty(parameterModel?.Type)

Copilot uses AI. Check for mistakes.
? FormatDefaultValue(parameterModel.Schema.Default, parameterModel.Type)
: "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}\"",

Copilot AI Nov 7, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String default values are not properly escaped. If a default value contains special characters like quotes ("), backslashes (\), or newlines (\n), the generated code will be invalid.

Consider using a proper string escaping mechanism:

"string" => $"\"{defaultValue.ToString()?.Replace("\\", "\\\\").Replace("\"", "\\\"")}\"",

Or better yet, use a built-in method like System.Text.Json.JsonSerializer.Serialize() or similar to handle all edge cases correctly.

Copilot uses AI. Check for mistakes.
_ when IsNumericType(type) => defaultValue.ToString() ?? "default",
_ => "default"
};
}
Comment on lines +153 to +167

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix string escaping to prevent invalid generated code.

Line 163 wraps string default values in quotes but doesn't escape existing quotes or special characters. If the OpenAPI schema specifies a default like my"test or path\to\file, the generated code will be syntactically invalid:

string? filter = "my"test"      // Invalid C#
string? path = "path\to\file"   // Invalid escape sequence

Apply this diff to add proper string escaping:

-            "string" => $"\"{defaultValue}\"",
+            "string" => $"\"{defaultValue.ToString()?.Replace("\\", "\\\\").Replace("\"", "\\\"")}\"",

Alternatively, use C#'s verbatim string literals or @$"..." syntax with appropriate escaping, or leverage a library method for C# string literal generation.

🤖 Prompt for AI Agents
In src/Refitter.Core/ParameterExtractor.cs around lines 153 to 167, the string
default-value branch wraps values in quotes without escaping, producing invalid
C# when the default contains quotes or backslashes; fix by emitting a safe C#
string literal: generate a verbatim string literal (prefix with @ and wrap in
quotes) and escape any existing double-quote characters by doubling them
(replace " with "") so backslashes are preserved, or if you prefer non-verbatim
literals, escape backslashes, double-quotes and control characters properly;
update the "string" case to produce that escaped literal.


Comment on lines +164 to +168

Copilot AI Nov 7, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Floating-point numeric literals need type suffixes to avoid compilation errors. Without the proper suffix:

  • float values need an f or F suffix (e.g., 1.5f)
  • decimal values need an m or M suffix (e.g., 1.5m)

Without these suffixes, the compiler treats literals as double by default, which can cause type mismatch errors.

Suggestion:

_ when IsNumericType(type) => FormatNumericValue(defaultValue, type),

And add a helper method:

private static string FormatNumericValue(object value, string type)
{
    var numericString = value is IFormattable formattable 
        ? formattable.ToString(null, CultureInfo.InvariantCulture) 
        : (value.ToString() ?? "default");
    
    return type switch
    {
        "float" or "Single" => $"{numericString}f",
        "decimal" or "Decimal" => $"{numericString}m",
        _ => numericString
    };
}
Suggested change
_ when IsNumericType(type) => defaultValue.ToString() ?? "default",
_ => "default"
};
}
_ when IsNumericType(type) => FormatNumericValue(defaultValue, type),
_ => "default"
};
}
private static string FormatNumericValue(object defaultValue, string type)
{
var numericString = defaultValue is IFormattable formattable
? formattable.ToString(null, CultureInfo.InvariantCulture)
: (defaultValue.ToString() ?? "default");
return type switch
{
"float" or "Single" => $"{numericString}f",
"decimal" or "Decimal" => $"{numericString}m",
_ => numericString
};
}

Copilot uses AI. Check for mistakes.
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
Expand All @@ -156,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) + "] ";
Expand Down Expand Up @@ -316,12 +361,12 @@ private static List<string> GetQueryParameters(CSharpOperationModel operationMod
private static void AppendXmlDocComment(string description, StringBuilder codeBuilder)
{
codeBuilder.Append(
$$"""
"""
/// <summary>
""");

var lines = description.Split(
new[] { "\r\n", "\r", "\n" },
["\r\n", "\r", "\n"],
StringSplitOptions.None);

foreach (var line in lines)
Expand All @@ -335,7 +380,7 @@ private static void AppendXmlDocComment(string description, StringBuilder codeBu

codeBuilder.AppendLine();
codeBuilder.Append(
$$"""
"""
/// </summary>
""");
codeBuilder.AppendLine();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
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<string> 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;
}
}
Loading