diff --git a/src/NSwag.CodeGeneration.CSharp.Tests/QueryParameterTests.cs b/src/NSwag.CodeGeneration.CSharp.Tests/QueryParameterTests.cs
index 844971b300..1e5332949e 100644
--- a/src/NSwag.CodeGeneration.CSharp.Tests/QueryParameterTests.cs
+++ b/src/NSwag.CodeGeneration.CSharp.Tests/QueryParameterTests.cs
@@ -1,4 +1,7 @@
-using Xunit;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using Xunit;
namespace NSwag.CodeGeneration.CSharp.Tests
{
@@ -78,5 +81,164 @@ public void When_query_parameter_is_set_to_explode_and_style_is_form_object_para
"urlBuilder_.Append(System.Uri.EscapeDataString(\"limit\") + \"=\").Append(System.Uri.EscapeDataString(ConvertToString(paging.Limit, System.Globalization.CultureInfo.InvariantCulture))).Append(\"&\");",
code);
}
+
+ [Theory]
+ [InlineData("form", ",")]
+ [InlineData("spaceDelimited", "%20")]
+ [InlineData("pipeDelimited", "|")]
+ public void When_query_parameter_is_set_to_not_explode_style_should_be_respected_and_array_parameters_delimited(string style, string delimiter)
+ {
+ var spec = $@"{{
+ ""openapi"": ""3.0.0"",
+ ""info"": {{
+ ""version"": ""1.0.0"",
+ ""title"": ""Query params tests""
+ }},
+ ""servers"": [
+ {{
+ ""url"": ""http://localhost:8080""
+ }}
+ ],
+ ""paths"": {{
+ ""/settings"": {{
+ ""get"": {{
+ ""summary"": ""List all settings"",
+ ""operationId"": ""listSettings"",
+ ""parameters"": [
+ {{
+ ""name"": ""paging"",
+ ""in"": ""query"",
+ ""description"": ""list setting filter"",
+ ""required"": false,
+ ""style"": ""{style}"",
+ ""explode"": false,
+ ""type"": ""array"",
+ ""items"": {{
+ ""type"": ""integer"",
+ ""format"": ""int32""
+ }}
+ }}
+ ],
+ ""responses"": {{
+ ""200"": {{
+ ""description"": ""A paged array of settings""
+ }}
+ }}
+ }}
+ }}
+ }}
+}}
+";
+
+
+
+ var document = OpenApiDocument.FromJsonAsync(spec).Result;
+
+ //// Act
+ var generator = new CSharpClientGenerator(document, new CSharpClientGeneratorSettings());
+ var code = generator.GenerateFile();
+
+ //// Assert
+
+ var expected =
+ $@"{{
+ bool first_ = true;
+ foreach (var item_ in paging)
+ {{
+ if (first_)
+ {{
+ urlBuilder_.Append(System.Uri.EscapeDataString(""paging"")).Append('=');
+ first_ = false;
+ }}
+ else
+ urlBuilder_.Append({(delimiter.Length == 1 ? $"'{delimiter}'" : $"\"{delimiter}\"")});
+
+ urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(item_, System.Globalization.CultureInfo.InvariantCulture)));
+ }}
+ if (!first_)
+ urlBuilder_.Append('&');
+}}";
+
+ AssertCodeContains(expected, code);
+ }
+
+ [Theory]
+ [InlineData("form")]
+ [InlineData("spaceDelimited")]
+ [InlineData("pipeDelimited")]
+ public void When_query_parameter_is_set_to_explode_style_is_ignored_and_array_parameters_are_exploded(string style)
+ {
+ var spec = $@"{{
+ ""openapi"": ""3.0.0"",
+ ""info"": {{
+ ""version"": ""1.0.0"",
+ ""title"": ""Query params tests""
+ }},
+ ""servers"": [
+ {{
+ ""url"": ""http://localhost:8080""
+ }}
+ ],
+ ""paths"": {{
+ ""/settings"": {{
+ ""get"": {{
+ ""summary"": ""List all settings"",
+ ""operationId"": ""listSettings"",
+ ""parameters"": [
+ {{
+ ""name"": ""paging"",
+ ""in"": ""query"",
+ ""description"": ""list setting filter"",
+ ""required"": false,
+ ""style"": ""{style}"",
+ ""explode"": true,
+ ""type"": ""array"",
+ ""items"": {{
+ ""type"": ""integer"",
+ ""format"": ""int32""
+ }}
+ }}
+ ],
+ ""responses"": {{
+ ""200"": {{
+ ""description"": ""A paged array of settings""
+ }}
+ }}
+ }}
+ }}
+ }}
+}}
+";
+
+
+
+ var document = OpenApiDocument.FromJsonAsync(spec).Result;
+
+ //// Act
+ var generator = new CSharpClientGenerator(document, new CSharpClientGeneratorSettings());
+ var code = generator.GenerateFile();
+
+ //// Assert
+
+ var expected = "foreach (var item_ in paging) { urlBuilder_.Append(System.Uri.EscapeDataString(\"paging\") + \"=\").Append((item_ == null) ? \"\" : System.Uri.EscapeDataString(ConvertToString(item_, System.Globalization.CultureInfo.InvariantCulture))).Append(\"&\"); }";
+
+ AssertCodeContains(expected, code);
+ }
+
+ private static void AssertCodeContains(string expected, string actual)
+ {
+ var reader = new StringReader(expected);
+
+ const string regexLinePrefix = @"^\s+";
+ const string regexLineSuffix = @"\r?\n";
+
+ StringBuilder regexPattern = new StringBuilder();
+ while (reader.ReadLine() is string line)
+ regexPattern.AppendLine(regexLinePrefix + Regex.Escape(line) + regexLineSuffix);
+
+ Regex regex = new Regex(regexPattern.ToString(), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace);
+
+ Assert.True(regex.IsMatch(actual), $"Unable to find:\r\n{expected}\r\n in:\r\n{actual}");
+ }
}
}
\ No newline at end of file
diff --git a/src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.QueryParameter.liquid b/src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.QueryParameter.liquid
index b58f0a265f..c295763927 100644
--- a/src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.QueryParameter.liquid
+++ b/src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.QueryParameter.liquid
@@ -7,7 +7,63 @@ urlBuilder_.Append(System.Uri.EscapeDataString("{{ parameter.Name }}") + "=").Ap
{% elseif parameter.IsDate -%}
urlBuilder_.Append(System.Uri.EscapeDataString("{{ parameter.Name }}") + "=").Append(System.Uri.EscapeDataString({% if parameter.IsNullable and parameter.IsRequired %}{{ parameter.VariableName }} != null ? {% endif %}{{ parameter.VariableName }}{% if parameter.IsSystemNullable %}.Value{% endif %}.ToString("{{ ParameterDateFormat }}", System.Globalization.CultureInfo.InvariantCulture){% if parameter.IsNullable and parameter.IsRequired %} : "{{ QueryNullValue }}"{% endif %})).Append("&");
{% elseif parameter.IsArray -%}
+{% if parameter.Explode -%}
foreach (var item_ in {{ parameter.VariableName }}) { urlBuilder_.Append(System.Uri.EscapeDataString("{{ parameter.Name }}") + "=").Append(System.Uri.EscapeDataString(ConvertToString(item_, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); }
+{% elseif parameter.IsForm -%}
+{
+ bool first_ = true;
+ foreach (var item_ in {{ parameter.VariableName }})
+ {
+ if (first_)
+ {
+ urlBuilder_.Append(System.Uri.EscapeDataString("{{ parameter.Name }}")).Append('=');
+ first_ = false;
+ }
+ else
+ urlBuilder_.Append(',');
+
+ urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(item_, System.Globalization.CultureInfo.InvariantCulture)));
+ }
+ if (!first_)
+ urlBuilder_.Append('&');
+}
+{% elseif parameter.IsSpaceDelimited -%}
+{
+ bool first_ = true;
+ foreach (var item_ in {{ parameter.VariableName }})
+ {
+ if (first_)
+ {
+ urlBuilder_.Append(System.Uri.EscapeDataString("{{ parameter.Name }}")).Append('=');
+ first_ = false;
+ }
+ else
+ urlBuilder_.Append("%20");
+
+ urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(item_, System.Globalization.CultureInfo.InvariantCulture)));
+ }
+ if (!first_)
+ urlBuilder_.Append('&');
+}
+{% elseif parameter.IsPipeDelimited -%}
+{
+ bool first_ = true;
+ foreach (var item_ in {{ parameter.VariableName }})
+ {
+ if (first_)
+ {
+ urlBuilder_.Append(System.Uri.EscapeDataString("{{ parameter.Name }}")).Append('=');
+ first_ = false;
+ }
+ else
+ urlBuilder_.Append('|');
+
+ urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(item_, System.Globalization.CultureInfo.InvariantCulture)));
+ }
+ if (!first_)
+ urlBuilder_.Append('&');
+}
+{% endif -%}
{% elseif parameter.IsDeepObject -%}
{% for property in parameter.PropertyNames -%}
if ({{parameter.Name}}.{{property.Name}} != null)
diff --git a/src/NSwag.CodeGeneration/Models/ParameterModelBase.cs b/src/NSwag.CodeGeneration/Models/ParameterModelBase.cs
index 26a2635caa..126c9d6485 100644
--- a/src/NSwag.CodeGeneration/Models/ParameterModelBase.cs
+++ b/src/NSwag.CodeGeneration/Models/ParameterModelBase.cs
@@ -90,16 +90,26 @@ public string Default
public OpenApiParameterKind Kind => _parameter.Kind;
/// Gets the parameter style.
- public OpenApiParameterStyle Style => _parameter.Style;
+ public OpenApiParameterStyle Style => _parameter.ActualStyle;
/// Gets the the value indicating if the parameter values should be exploded when included in the query string.
- public bool Explode => _parameter.Explode;
+ public bool Explode => _parameter.ActualExplode;
/// Gets a value indicating whether the parameter is a deep object (OpenAPI 3).
- public bool IsDeepObject => _parameter.Style == OpenApiParameterStyle.DeepObject;
+ public bool IsDeepObject => _parameter.ActualStyle == OpenApiParameterStyle.DeepObject;
/// Gets a value indicating whether the parameter has form style.
- public bool IsForm => _parameter.Style == OpenApiParameterStyle.Form;
+ public bool IsForm => _parameter.ActualStyle == OpenApiParameterStyle.Form;
+
+ ///
+ /// Gets a value indicating whether this array parameter has space-delimited style (OpenAPI 3).
+ ///
+ public bool IsSpaceDelimited => this._parameter.ActualStyle == OpenApiParameterStyle.SpaceDelimeted;
+
+ ///
+ /// Gets a value indicating whether this array parameter has pipe-delimited style (OpenAPI 3).
+ ///
+ public bool IsPipeDelimited => this._parameter.ActualStyle == OpenApiParameterStyle.PipeDelimited;
/// Gets the contained value property names (OpenAPI 3).
public IEnumerable PropertyNames
diff --git a/src/NSwag.Core.Tests/Serialization/QueryParameterSerializationTests.cs b/src/NSwag.Core.Tests/Serialization/QueryParameterSerializationTests.cs
new file mode 100644
index 0000000000..699c65e14e
--- /dev/null
+++ b/src/NSwag.Core.Tests/Serialization/QueryParameterSerializationTests.cs
@@ -0,0 +1,85 @@
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using NJsonSchema;
+using Xunit;
+
+namespace NSwag.Core.Tests.Serialization
+{
+ public class QueryParameterSerializationTests
+ {
+ [Fact]
+ public async Task When_style_is_form_then_explode_should_serialize_as_true_by_default()
+ {
+ //// Arrange
+ var parameter = new OpenApiParameter();
+ parameter.Name = "Foo";
+ parameter.Kind = OpenApiParameterKind.Query;
+ parameter.Style = OpenApiParameterStyle.Form;
+ parameter.Schema = new JsonSchema
+ {
+ Type = JsonObjectType.Array,
+ Item = new JsonSchema
+ {
+ Type = JsonObjectType.String
+ }
+ };
+
+ //// Act
+ var json = parameter.ToJson(Formatting.Indented);
+
+ //// Assert
+ Assert.Equal(
+ @"{
+ ""$schema"": ""http://json-schema.org/draft-04/schema#"",
+ ""name"": ""Foo"",
+ ""in"": ""query"",
+ ""style"": ""form"",
+ ""explode"": true,
+ ""schema"": {
+ ""type"": ""array"",
+ ""items"": {
+ ""type"": ""string""
+ }
+ }
+}".Replace("\r", ""), json.Replace("\r", ""));
+ }
+
+ [Fact]
+ public async Task When_style_is_form_then_explode_should_serialize_as_false_when_set()
+ {
+ //// Arrange
+ var parameter = new OpenApiParameter();
+ parameter.Name = "Foo";
+ parameter.Kind = OpenApiParameterKind.Query;
+ parameter.Style = OpenApiParameterStyle.Form;
+ parameter.Explode = false;
+ parameter.Schema = new JsonSchema
+ {
+ Type = JsonObjectType.Array,
+ Item = new JsonSchema
+ {
+ Type = JsonObjectType.String
+ }
+ };
+
+ //// Act
+ var json = parameter.ToJson(Formatting.Indented);
+
+ //// Assert
+ Assert.Equal(
+ @"{
+ ""$schema"": ""http://json-schema.org/draft-04/schema#"",
+ ""name"": ""Foo"",
+ ""in"": ""query"",
+ ""style"": ""form"",
+ ""explode"": false,
+ ""schema"": {
+ ""type"": ""array"",
+ ""items"": {
+ ""type"": ""string""
+ }
+ }
+}".Replace("\r", ""), json.Replace("\r", ""));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NSwag.Core/OpenApiParameter.cs b/src/NSwag.Core/OpenApiParameter.cs
index 4f1ec64bf1..bc40d08851 100644
--- a/src/NSwag.Core/OpenApiParameter.cs
+++ b/src/NSwag.Core/OpenApiParameter.cs
@@ -24,7 +24,7 @@ public class OpenApiParameter : JsonSchema
private bool _isRequired = false;
private JsonSchema _schema;
private IDictionary _examples;
- private bool _explode;
+ private bool? _explode;
private int? _position;
private static readonly Regex AppJsonRegex = new Regex(@"application\/(\S+?)?\+?json;?(\S+)?");
@@ -56,6 +56,37 @@ public OpenApiParameterKind Kind
}
}
+ /// Gets the actual style of the parameter (OpenAPI only).
+ [JsonIgnore]
+ public OpenApiParameterStyle ActualStyle
+ {
+ get
+ {
+ OpenApiParameterStyle ret;
+ if (_style == OpenApiParameterStyle.Undefined)
+ {
+ switch (Kind)
+ {
+ case OpenApiParameterKind.Query:
+ case OpenApiParameterKind.Cookie:
+ ret = OpenApiParameterStyle.Form;
+ break;
+ case OpenApiParameterKind.Path:
+ case OpenApiParameterKind.Header:
+ ret = OpenApiParameterStyle.Simple;
+ break;
+ default:
+ ret = OpenApiParameterStyle.Undefined;
+ break;
+ }
+ }
+ else
+ ret = _style;
+
+ return ret;
+ }
+ }
+
/// Gets or sets the style of the parameter (OpenAPI only).
[JsonProperty(PropertyName = "style", DefaultValueHandling = DefaultValueHandling.Ignore)]
public OpenApiParameterStyle Style
@@ -68,11 +99,15 @@ public OpenApiParameterStyle Style
}
}
+ /// Gets the actual explode setting for the parameter (OpenAPI only).
+ [JsonIgnore]
+ public bool ActualExplode => _explode ?? ActualStyle == OpenApiParameterStyle.Form;
+
/// Gets or sets the explode setting for the parameter (OpenAPI only).
- [JsonProperty(PropertyName = "explode", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [JsonProperty(PropertyName = "explode", DefaultValueHandling = DefaultValueHandling.Include)]
public bool Explode
{
- get => _explode;
+ get => _explode ?? ActualExplode;
set
{
_explode = value;