Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for sending and recieving large json arrays. #4729

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
82 changes: 82 additions & 0 deletions src/NSwag.CodeGeneration.CSharp.Tests/CSharpClientSettingsTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NJsonSchema.Generation;
Expand All @@ -17,6 +18,14 @@ public object GetPerson(bool @override = false)
}
}

public class BarController : Controller
{
public IEnumerable<object> GetPeople(IEnumerable<object> names)
{
return null;
}
}

[Fact]
public async Task When_ConfigurationClass_is_set_then_correct_ctor_is_generated()
{
Expand Down Expand Up @@ -129,6 +138,79 @@ public async Task When_custom_http_client_type_is_specified_then_an_instance_of_
Assert.Contains("var client_ = new CustomNamespace.CustomHttpClient();", code);
}

[Fact]
public async Task When_LargeJsonArrayResponseMethods_is_set_then_IAsyncEnumerator_response_is_generated()
{
// Arrange
var swaggerGenerator = new WebApiOpenApiDocumentGenerator(new WebApiOpenApiDocumentGeneratorSettings());
var document = await swaggerGenerator.GenerateForControllerAsync<BarController>();

var generator = new CSharpClientGenerator(document, new CSharpClientGeneratorSettings
{
LargeJsonArrayResponseMethods = ["BarClient.GetPeople"],
GenerateClientInterfaces = true,
});
generator.Settings.CSharpGeneratorSettings.JsonLibrary = NJsonSchema.CodeGeneration.CSharp.CSharpJsonLibrary.SystemTextJson;

// Act
var code = generator.GenerateFile();

// Assert
Assert.Contains("System.Threading.Tasks.Task<System.Collections.Generic.IAsyncEnumerator<object>> GetPeopleAsync(System.Collections.Generic.IEnumerable<object> names, System.Threading.CancellationToken cancellationToken);", code);
Assert.Contains("public virtual async System.Threading.Tasks.Task<System.Collections.Generic.IAsyncEnumerator<object>> GetPeopleAsync(System.Collections.Generic.IEnumerable<object> names, System.Threading.CancellationToken cancellationToken)", code);
Assert.Contains("throw new ApiException(\"Response content was null which was not expected.\", status_, null, headers_, null);", code);
Assert.Contains("var result_ = ConvertToIAsyncEnumerator<object>(response_, cancellationToken);", code);
}

[Fact]
public async Task When_LargeJsonArrayResponseMethods_and_WrapResponse_is_set_then_disposable_wrapped_IAsyncEnumerator_response_is_generated()
{
// Arrange
var swaggerGenerator = new WebApiOpenApiDocumentGenerator(new WebApiOpenApiDocumentGeneratorSettings());
var document = await swaggerGenerator.GenerateForControllerAsync<BarController>();

var generator = new CSharpClientGenerator(document, new CSharpClientGeneratorSettings
{
LargeJsonArrayResponseMethods = ["BarClient.GetPeople"],
WrapResponses = true,
GenerateClientInterfaces = true,
});
generator.Settings.CSharpGeneratorSettings.JsonLibrary = NJsonSchema.CodeGeneration.CSharp.CSharpJsonLibrary.SystemTextJson;

// Act
var code = generator.GenerateFile();

// Assert
Assert.Contains("System.Threading.Tasks.Task<SwaggerResponseDisposable<System.Collections.Generic.IAsyncEnumerator<object>>> GetPeopleAsync(System.Collections.Generic.IEnumerable<object> names, System.Threading.CancellationToken cancellationToken);", code);
Assert.Contains("public virtual async System.Threading.Tasks.Task<SwaggerResponseDisposable<System.Collections.Generic.IAsyncEnumerator<object>>> GetPeopleAsync(System.Collections.Generic.IEnumerable<object> names, System.Threading.CancellationToken cancellationToken)", code);
Assert.Contains("return new SwaggerResponseDisposable<System.Collections.Generic.IAsyncEnumerator<object>>(status_, headers_, result_);", code);
Assert.Contains("public partial class SwaggerResponseDisposable<TResult> : SwaggerResponse<TResult>, System.IAsyncDisposable", code);
}

[Fact]
public async Task When_LargeJsonArrayRequestMethods_is_set_then_IAsyncEnumerable_parameter_is_generated()
{
// Arrange
var swaggerGenerator = new WebApiOpenApiDocumentGenerator(new WebApiOpenApiDocumentGeneratorSettings());
var document = await swaggerGenerator.GenerateForControllerAsync<BarController>();

var generator = new CSharpClientGenerator(document, new CSharpClientGeneratorSettings
{
LargeJsonArrayRequestMethods = ["BarClient.GetPeople"],
GenerateClientInterfaces = true,
});
generator.Settings.CSharpGeneratorSettings.JsonLibrary = NJsonSchema.CodeGeneration.CSharp.CSharpJsonLibrary.SystemTextJson;

// Act
var code = generator.GenerateFile();

// Assert //
Assert.Contains("System.Threading.Tasks.Task<System.Collections.Generic.ICollection<object>> GetPeopleAsync(System.Collections.Generic.IAsyncEnumerable<object> names, System.Threading.CancellationToken cancellationToken);", code);
Assert.Contains("public virtual async System.Threading.Tasks.Task<System.Collections.Generic.ICollection<object>> GetPeopleAsync(System.Collections.Generic.IAsyncEnumerable<object> names, System.Threading.CancellationToken cancellationToken)", code);
Assert.Contains("var content_ = new StreamHttpContent<System.Collections.Generic.IAsyncEnumerable<object>>(names, _settings.Value, cancellationToken);", code);
Assert.Contains("public class StreamHttpContent<T> : System.Net.Http.HttpContent", code);
}

[Fact]
public async Task When_client_base_interface_is_not_specified_then_client_interface_should_have_no_base_interface()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,11 @@ public CSharpClientGeneratorSettings()

/// <summary>Gets or sets a value indicating whether to expose the JsonSerializerSettings property (default: false).</summary>
public bool ExposeJsonSerializerSettings { get; set; }

/// <summary> Gets or sets the list of operation ids, that should use IAsyncEnumerator to handle a large json array response. Requires System.Text.Json.</summary>
public string[] LargeJsonArrayResponseMethods { get; set; }

/// <summary> Gets or sets the list of operation ids, that should use IAsyncEnumerables to handle a large json array request. Requires System.Text.Json.</summary>
public string[] LargeJsonArrayRequestMethods { get; set; }
}
}
11 changes: 11 additions & 0 deletions src/NSwag.CodeGeneration.CSharp/Models/CSharpFileTemplateModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ public class CSharpFileTemplateModel
/// <summary>Gets or sets a value indicating whether to generate the response class (only applied when WrapResponses == true, default: true).</summary>
public bool GenerateResponseClasses => _settings.GenerateResponseClasses;

/// <summary>Gets or sets a value indicating whether to generate the disposable response class (only applied when WrapResponses == true and using large json array responses).</summary>
public bool GenerateDisposableResponseClasses =>
GenerateResponseClasses &&
_settings.CSharpGeneratorSettings.JsonLibrary == CSharpJsonLibrary.SystemTextJson &&
(_settings as CSharpClientGeneratorSettings)?.LargeJsonArrayResponseMethods?.Length > 0;

/// <summary>Gets a value indicating whether to generate StreamHttpContent.</summary>
public bool GenerateStreamHttpContent =>
_settings.CSharpGeneratorSettings.JsonLibrary == CSharpJsonLibrary.SystemTextJson &&
(_settings as CSharpClientGeneratorSettings)?.LargeJsonArrayRequestMethods?.Length > 0;

/// <summary>Gets the response class names.</summary>
public IEnumerable<string> ResponseClassNames
{
Expand Down
53 changes: 52 additions & 1 deletion src/NSwag.CodeGeneration.CSharp/Models/CSharpOperationModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using NJsonSchema;
using NJsonSchema.CodeGeneration;
using NJsonSchema.CodeGeneration.CSharp;
Expand Down Expand Up @@ -122,15 +123,31 @@ public string SyncResultType
{
if (_settings != null && WrapResponse && UnwrappedResultType != "FileResponse")
{
var disposableSuffix = GenerateResultAsIAsyncEnumerator ? "Disposable" : "";
return UnwrappedResultType == "void"
? _settings.ResponseClass.Replace("{controller}", ControllerName)
: _settings.ResponseClass.Replace("{controller}", ControllerName) + "<" + UnwrappedResultType + ">";
: _settings.ResponseClass.Replace("{controller}", ControllerName) + disposableSuffix + "<" + UnwrappedResultType + ">";
}

return UnwrappedResultType;
}
}

/// <summary>Gets the type of the unwrapped result type (without Task).</summary>
public override string UnwrappedResultType
{
get
{
if (GenerateResultAsIAsyncEnumerator)
{
var regex = new Regex(Regex.Escape(_settings.ResponseArrayType + "<"));
return regex.Replace(base.UnwrappedResultType, "System.Collections.Generic.IAsyncEnumerator<", count: 1);
}

return base.UnwrappedResultType;
}
}

/// <summary>Gets or sets the type of the result.</summary>
public override string ResultType
{
Expand Down Expand Up @@ -158,6 +175,16 @@ public override string ExceptionType
}
}

/// <summary>Gets the type of the inner array result type (without IAsyncEnumerator).</summary>
public string InnerIAsyncEnumeratorType
{
get
{
var removedBeginningType = UnwrappedResultType.Replace("System.Collections.Generic.IAsyncEnumerator<", string.Empty);
return removedBeginningType.Substring(0, removedBeginningType.Length - 1);
}
}

/// <summary>Gets or sets the exception descriptions.</summary>
public IEnumerable<CSharpExceptionDescriptionModel> ExceptionDescriptions
{
Expand Down Expand Up @@ -214,6 +241,28 @@ public string RouteName
}
}

/// <summary>Gets a value indicating whether the request result should be generated as an IAsyncEnumerator instead of the default ArrayType.</summary>
public bool GenerateResultAsIAsyncEnumerator =>
_settings.CSharpGeneratorSettings.JsonLibrary == CSharpJsonLibrary.SystemTextJson &&
(_settings as CSharpClientGeneratorSettings)?.LargeJsonArrayResponseMethods?.Contains(_settings.GenerateControllerName(ControllerName) + "." + ActualOperationName) == true &&
base.UnwrappedResultType.StartsWith(_settings.ResponseArrayType + "<");

/// <summary>Gets a value indicating whether the request content parameter is generated as an IAsyncEnumerable instead of the default ArrayType.</summary>
public bool ContentParameterIsIAsyncEnumerable =>
_settings.CSharpGeneratorSettings.JsonLibrary == CSharpJsonLibrary.SystemTextJson &&
(_settings as CSharpClientGeneratorSettings)?.LargeJsonArrayRequestMethods?.Contains(_settings.GenerateControllerName(ControllerName) + "." + ActualOperationName) == true &&
HasContent && ContentParameter.Type.StartsWith(_settings.ParameterArrayType + "<");

/// <summary>Gets the content parameter type with the replaced IAsyncEnumerable.</summary>
public string ContentParameterIAsyncEnumerableType
{
get
{
var regex = new Regex(Regex.Escape(_settings.ParameterArrayType + "<"));
return regex.Replace(ContentParameter.Type, "System.Collections.Generic.IAsyncEnumerable<", count: 1);
}
}

/// <summary>True if the operation has any security schemes</summary>
public bool RequiresAuthentication => (_operation.ActualSecurity?.Count() ?? 0) != 0;

Expand Down Expand Up @@ -269,6 +318,8 @@ protected override string ResolveParameterType(OpenApiParameter parameter)
}
}

if (parameter.Kind == OpenApiParameterKind.Body)

if (schema.Type == JsonObjectType.Array && (schema.Item?.IsBinary ?? false))
{
return "System.Collections.Generic.IEnumerable<FileParameter>";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ return result_;
{% else -%}
throw new {{ ExceptionClass }}<{{ response.Type }}>("{{ response.ExceptionDescription }}", status_, responseData_, headers_, result_, null);
{% endif -%}
{% elsif operation.GenerateResultAsIAsyncEnumerator and response.IsSuccess -%}
if (response_.Content == null)
{
throw new {{ ExceptionClass }}("Response content was null which was not expected.", status_, null, headers_, null);
}
var result_ = ConvertToIAsyncEnumerator<{{ operation.InnerIAsyncEnumeratorType }}>(response_, cancellationToken);
disposeResponse_ = false;
{% if operation.WrapResponse -%}
return new {{ ResponseClass }}Disposable<{{ operation.UnwrappedResultType }}>(status_, headers_, result_);
{% else -%}
return result_;
{% endif -%}
{% else -%}
var objectResponse_ = await ReadObjectResponseAsync<{{ response.Type }}>(response_, headers_, cancellationToken).ConfigureAwait(false);
{% if response.IsNullable == false -%}
Expand Down
23 changes: 20 additions & 3 deletions src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
{% if GenerateOptionalParameters == false -%}
{% template Client.Method.Documentation %}
{% template Client.Method.Annotations %}
{{ operation.MethodAccessModifier }} virtual {{ operation.ResultType }} {{ operation.ActualOperationName }}Async({% for parameter in operation.Parameters %}{{ parameter.Type }} {{ parameter.VariableName }}{% if GenerateOptionalParameters and parameter.IsOptional %} = null{% endif %}{% if parameter.IsLast == false %}, {% endif %}{% endfor %})
{{ operation.MethodAccessModifier }} virtual {{ operation.ResultType }} {{ operation.ActualOperationName }}Async({% for parameter in operation.Parameters %}{% if parameter.IsBody and operation.ContentParameterIsIAsyncEnumerable %}{{ operation.ContentParameterIAsyncEnumerableType }}{% else %}{{ parameter.Type }}{% endif %} {{ parameter.VariableName }}{% if GenerateOptionalParameters and parameter.IsOptional %} = null{% endif %}{% if parameter.IsLast == false %}, {% endif %}{% endfor %})
{
return {{ operation.ActualOperationName }}Async({% for parameter in operation.Parameters %}{{ parameter.VariableName }}, {% endfor %}System.Threading.CancellationToken.None);
}
Expand All @@ -132,7 +132,7 @@
{% if GenerateSyncMethods -%}
{% template Client.Method.Documentation %}
{% template Client.Method.Annotations %}
{{ operation.MethodAccessModifier }} virtual {{ operation.SyncResultType }} {{ operation.ActualOperationName }}({% for parameter in operation.Parameters %}{{ parameter.Type }} {{ parameter.VariableName }}{% if GenerateOptionalParameters and parameter.IsOptional %} = null{% endif %}{% if parameter.IsLast == false %}, {% endif %}{% endfor %})
{{ operation.MethodAccessModifier }} virtual {{ operation.SyncResultType }} {{ operation.ActualOperationName }}({% for parameter in operation.Parameters %}{% if parameter.IsBody and operation.ContentParameterIsIAsyncEnumerable %}{{ operation.ContentParameterIAsyncEnumerableType }}{% else %}{{ parameter.Type }}{% endif %} {{ parameter.VariableName }}{% if GenerateOptionalParameters and parameter.IsOptional %} = null{% endif %}{% if parameter.IsLast == false %}, {% endif %}{% endfor %})
{
{% if operation.HasResult or operation.WrapResponse %}return {% endif %}System.Threading.Tasks.Task.Run(async () => await {{ operation.ActualOperationName }}Async({% for parameter in operation.Parameters %}{{ parameter.VariableName }}, {% endfor %}System.Threading.CancellationToken.None)).GetAwaiter().GetResult();
}
Expand All @@ -141,7 +141,7 @@
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
{% template Client.Method.Documentation %}
{% template Client.Method.Annotations %}
{{ operation.MethodAccessModifier }} virtual async {{ operation.ResultType }} {{ operation.ActualOperationName }}Async({% for parameter in operation.Parameters %}{{ parameter.Type }} {{ parameter.VariableName }}{% if GenerateOptionalParameters and parameter.IsOptional %} = null{% endif %}, {% endfor %}System.Threading.CancellationToken cancellationToken{% if GenerateOptionalParameters %} = default(System.Threading.CancellationToken){% endif %})
{{ operation.MethodAccessModifier }} virtual async {{ operation.ResultType }} {{ operation.ActualOperationName }}Async({% for parameter in operation.Parameters %}{% if parameter.IsBody and operation.ContentParameterIsIAsyncEnumerable %}{{ operation.ContentParameterIAsyncEnumerableType }}{% else %}{{ parameter.Type }}{% endif %} {{ parameter.VariableName }}{% if GenerateOptionalParameters and parameter.IsOptional %} = null{% endif %}, {% endfor %}System.Threading.CancellationToken cancellationToken{% if GenerateOptionalParameters %} = default(System.Threading.CancellationToken){% endif %})
{
{% for parameter in operation.PathParameters -%}
{% if parameter.IsNullable == false and parameter.IsRequired -%}
Expand Down Expand Up @@ -212,6 +212,9 @@
{% elsif operation.HasPlainTextBodyParameter -%}
var content_ = new System.Net.Http.StringContent({{ operation.ContentParameter.VariableName }});
content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("{{ operation.Consumes }}");
{% elsif operation.ContentParameterIsIAsyncEnumerable -%}
var content_ = new StreamHttpContent<{{ operation.ContentParameterIAsyncEnumerableType }}>({{ operation.ContentParameter.VariableName }}, {% if UseRequestAndResponseSerializationSettings %}_requestSettings{% else %}_settings{% endif %}.Value, cancellationToken);
content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("{{ operation.Consumes }}");
{% else -%}
var json_ = {% if UseSystemTextJson %}System.Text.Json.JsonSerializer.SerializeToUtf8Bytes{% else %}Newtonsoft.Json.JsonConvert.SerializeObject{% endif %}({{ operation.ContentParameter.VariableName }}, {% if SerializeTypeInformation %}typeof({{ operation.ContentParameter.Type }}), {% endif %}{% if UseRequestAndResponseSerializationSettings %}_requestSettings{% else %}_settings{% endif %}.Value);
var content_ = new System.Net.Http.{% if UseSystemTextJson %}ByteArrayContent{% else %}StringContent{% endif %}(json_);
Expand Down Expand Up @@ -466,6 +469,20 @@

public string Text { get; }
}
{% if UseSystemTextJson == true %}

private async System.Collections.Generic.IAsyncEnumerator<T> ConvertToIAsyncEnumerator<T>(System.Net.Http.HttpResponseMessage response, System.Threading.CancellationToken cancellationToken)
{
using (response)
{
using var stream_ = await response.Content.ReadAsStreamAsync(cancellationToken);
await foreach (var item in System.Text.Json.JsonSerializer.DeserializeAsyncEnumerable<T>(stream_, {% if UseRequestAndResponseSerializationSettings %}Response{% endif %}JsonSerializerSettings, cancellationToken))
{
yield return item;
}
}
}
{% endif %}

{% template Client.Class.ReadObjectResponse %}

Expand Down