Skip to content

Commit 1ffa330

Browse files
authored
Improve OpenApiOperation ActualResponses and Responses performance (#5148)
* use internal helpers and avoid allocating dictionaries * optimize GetSuccessResponse
1 parent 6905a31 commit 1ffa330

File tree

10 files changed

+109
-50
lines changed

10 files changed

+109
-50
lines changed

src/NSwag.CodeGeneration.CSharp/Models/CSharpClientTemplateModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ public string JsonConvertersArrayCode
195195
private bool RequiresJsonExceptionConverter =>
196196
_settings.CSharpGeneratorSettings.JsonLibrary == CSharpJsonLibrary.NewtonsoftJson &&
197197
_settings.CSharpGeneratorSettings.ExcludedTypeNames?.Contains("JsonExceptionConverter") != true &&
198-
_document.Operations.Any(o => o.Operation.ActualResponses.Any(r => r.Value.Schema?.InheritsSchema(_exceptionSchema) == true));
198+
_document.Operations.Any(o => o.Operation.HasActualResponse((_, response) => response.Schema?.InheritsSchema(_exceptionSchema) == true));
199199

200200
private static readonly string[] jsonExceptionConverterArray = ["JsonExceptionConverter"];
201201
}

src/NSwag.CodeGeneration.CSharp/Models/CSharpFileTemplateModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public CSharpFileTemplateModel(
8383
public string ExceptionModelClass => JsonExceptionTypes.FirstOrDefault(t => t != "Exception") ?? "Exception";
8484

8585
private IEnumerable<string> JsonExceptionTypes => _document.Operations
86-
.SelectMany(o => o.Operation.ActualResponses.Where(r => r.Value.Schema?.InheritsSchema(_resolver.ExceptionSchema) == true).Select(r => new { o.Operation, Response = r.Value }))
86+
.SelectMany(o => o.Operation.GetActualResponses((_, response) => response.Schema?.InheritsSchema(_resolver.ExceptionSchema) == true).Select(r => new { o.Operation, Response = r.Value }))
8787
.Select(t => _generator.GetTypeName(t.Response.Schema, t.Response.IsNullable(_settings.CSharpGeneratorSettings.SchemaType), "Response"));
8888

8989
/// <summary>Gets a value indicating whether the generated code requires the FileParameter type.</summary>
@@ -99,7 +99,7 @@ public CSharpFileTemplateModel(
9999
/// <summary>Gets a value indicating whether [generate file response class].</summary>
100100
public bool GenerateFileResponseClass =>
101101
_settings.CSharpGeneratorSettings.ExcludedTypeNames?.Contains("FileResponse") != true &&
102-
_document.Operations.Any(o => o.Operation.ActualResponses.Any(r => r.Value.IsBinary(o.Operation)));
102+
_document.Operations.Any(o => o.Operation.HasActualResponse((_, response) => response.IsBinary(o.Operation)));
103103

104104
/// <summary>Gets or sets a value indicating whether to generate exception classes (default: true).</summary>
105105
public bool GenerateExceptionClasses => _settings is CSharpClientGeneratorSettings { GenerateExceptionClasses: true };

src/NSwag.CodeGeneration.CSharp/Models/CSharpOperationModel.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,14 @@ public override string ExceptionType
143143
{
144144
get
145145
{
146-
if (_operation.ActualResponses.Count(r => !HttpUtilities.IsSuccessStatusCode(r.Key)) != 1)
146+
var response = _operation.GetActualResponse((code, _) => !HttpUtilities.IsSuccessStatusCode(code));
147+
if (response == null)
147148
{
148149
return "System.Exception";
149150
}
150151

151-
var response = _operation.ActualResponses.Single(r => !HttpUtilities.IsSuccessStatusCode(r.Key));
152-
var isNullable = response.Value.IsNullable(_settings.CodeGeneratorSettings.SchemaType);
153-
return _generator.GetTypeName(response.Value.Schema, isNullable, "Exception");
152+
var isNullable = response.IsNullable(_settings.CodeGeneratorSettings.SchemaType);
153+
return _generator.GetTypeName(response.Schema, isNullable, "Exception");
154154
}
155155
}
156156

src/NSwag.CodeGeneration.CSharp/NSwag.CodeGeneration.CSharp.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
<ItemGroup>
88
<EmbeddedResource Include="Templates\*.liquid" />
9-
<Compile Include="..\NSwag.Core\Polyfills.cs" Link="Polyfills.cs" />
109
</ItemGroup>
1110

1211
<ItemGroup>

src/NSwag.CodeGeneration.TypeScript/Models/TypeScriptFileTemplateModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public IEnumerable<string> ResponseClassNames
140140
public bool RequiresFileResponseInterface =>
141141
!Framework.IsJQuery &&
142142
!_settings.TypeScriptGeneratorSettings.ExcludedTypeNames.Contains("FileResponse") &&
143-
_document.Operations.Any(o => o.Operation.ActualResponses.Any(r => r.Value.IsBinary(o.Operation)));
143+
_document.Operations.Any(o => o.Operation.HasActualResponse((_, response) => response.IsBinary(o.Operation)));
144144

145145
/// <summary>Gets a value indicating whether the client functions are required.</summary>
146146
public bool RequiresClientFunctions =>

src/NSwag.CodeGeneration.TypeScript/Models/TypeScriptOperationModel.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,13 @@ public override string ExceptionType
123123
{
124124
get
125125
{
126-
if (_operation.ActualResponses.All(r => HttpUtilities.IsSuccessStatusCode(r.Key)))
126+
var actualResponses = _operation.ActualResponses;
127+
if (actualResponses.All(static r => HttpUtilities.IsSuccessStatusCode(r.Key)))
127128
{
128129
return "string";
129130
}
130131

131-
return string.Join(" | ", _operation.ActualResponses
132+
return string.Join(" | ", actualResponses
132133
.Where(r => !HttpUtilities.IsSuccessStatusCode(r.Key) && r.Value.Schema != null)
133134
.Select(r => _generator.GetTypeName(r.Value.Schema, r.Value.IsNullable(_settings.CodeGeneratorSettings.SchemaType), "Exception"))
134135
.Concat(["string"]));

src/NSwag.CodeGeneration/Models/OperationModelBase.cs

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ protected OperationModelBase(JsonSchema exceptionSchema, OpenApiOperation operat
3434
_generator = generator;
3535
_settings = settings;
3636

37-
var responses = _operation.ActualResponses
37+
var responses = _operation.GetActualResponses(static (_, _) => true)
3838
.Select(response => CreateResponseModel(operation, response.Key, response.Value, exceptionSchema, generator, resolver, settings))
3939
.ToList();
4040

@@ -295,21 +295,7 @@ public string Produces
295295
}
296296

297297
/// <summary>Gets a value indicating whether a file response is expected from one of the responses.</summary>
298-
public bool IsFile
299-
{
300-
get
301-
{
302-
foreach (var r in _operation.ActualResponses)
303-
{
304-
if (r.Value.IsBinary(_operation))
305-
{
306-
return true;
307-
}
308-
}
309-
310-
return false;
311-
}
312-
}
298+
public bool IsFile => _operation.HasActualResponse((_, response) => response.IsBinary(_operation));
313299

314300
/// <summary>Gets a value indicating whether to wrap the response of this operation.</summary>
315301
public bool WrapResponse => _settings.WrapResponses && (
@@ -324,18 +310,7 @@ public bool IsFile
324310
/// <returns>The response.</returns>
325311
protected KeyValuePair<string, OpenApiResponse> GetSuccessResponse()
326312
{
327-
if (_operation.ActualResponses.TryGetValue("200", out var response200))
328-
{
329-
return new KeyValuePair<string, OpenApiResponse>("200", response200);
330-
}
331-
332-
var response = _operation.ActualResponses.FirstOrDefault(static r => HttpUtilities.IsSuccessStatusCode(r.Key));
333-
if (response.Value != null)
334-
{
335-
return new KeyValuePair<string, OpenApiResponse>(response.Key, response.Value);
336-
}
337-
338-
return new KeyValuePair<string, OpenApiResponse>("default", _operation.ActualResponses.FirstOrDefault(r => r.Key == "default").Value);
313+
return _operation.GetSuccessResponse();
339314
}
340315

341316
/// <summary>Gets the name of the parameter variable.</summary>

src/NSwag.Core/NSwag.Core.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
<Project Sdk="Microsoft.NET.Sdk">
2+
23
<PropertyGroup>
34
<TargetFrameworks>net462;netstandard2.0</TargetFrameworks>
45
<RootNamespace>NSwag</RootNamespace>
56
<GenerateDocumentationFile>true</GenerateDocumentationFile>
67
</PropertyGroup>
8+
79
<ItemGroup>
810
<PackageReference Include="NJsonSchema" Version="11.2.0" />
911
</ItemGroup>
12+
13+
<ItemGroup>
14+
<InternalsVisibleTo Include="NSwag.CodeGeneration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100eba55a5211cd3198b5ba4b96c5cb70484b376ce083664d47dcb7439a97c3368a26ea54de3ec6d90d2899d39c4e3b65cb4ac7ef8ba5c9ded7c8aa6538757b31291624e96f374c23fdbeeaa85dfe841ab6afffbd3593d2a40c96a0f0888f25d7bd9361611db9450041b57776d33e3acb90794254c428251ddd63aa329d86ec809f" />
15+
<InternalsVisibleTo Include="NSwag.CodeGeneration.CSharp, PublicKey=0024000004800000940000000602000000240000525341310004000001000100eba55a5211cd3198b5ba4b96c5cb70484b376ce083664d47dcb7439a97c3368a26ea54de3ec6d90d2899d39c4e3b65cb4ac7ef8ba5c9ded7c8aa6538757b31291624e96f374c23fdbeeaa85dfe841ab6afffbd3593d2a40c96a0f0888f25d7bd9361611db9450041b57776d33e3acb90794254c428251ddd63aa329d86ec809f" />
16+
<InternalsVisibleTo Include="NSwag.CodeGeneration.TypeScript, PublicKey=0024000004800000940000000602000000240000525341310004000001000100eba55a5211cd3198b5ba4b96c5cb70484b376ce083664d47dcb7439a97c3368a26ea54de3ec6d90d2899d39c4e3b65cb4ac7ef8ba5c9ded7c8aa6538757b31291624e96f374c23fdbeeaa85dfe841ab6afffbd3593d2a40c96a0f0888f25d7bd9361611db9450041b57776d33e3acb90794254c428251ddd63aa329d86ec809f" />
17+
</ItemGroup>
18+
1019
</Project>

src/NSwag.Core/OpenApiDocument.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,9 @@ public void GenerateOperationIds()
268268
{
269269
if (group.Count() > 1)
270270
{
271-
var collections = group.Where(o => o.Operation.ActualResponses.Any(r =>
272-
HttpUtilities.IsSuccessStatusCode(r.Key) &&
273-
r.Value.Schema?.ActualSchema.Type == JsonObjectType.Array));
271+
var collections = group.Where(o => o.Operation.HasActualResponse(static (code, response) =>
272+
HttpUtilities.IsSuccessStatusCode(code) &&
273+
response.Schema?.ActualSchema.Type == JsonObjectType.Array));
274274
// if we have just collections, adding All will not help in discrimination
275275
if (collections.Count() == group.Count())
276276
{
@@ -279,9 +279,9 @@ public void GenerateOperationIds()
279279

280280
foreach (var o in group)
281281
{
282-
var isCollection = o.Operation.ActualResponses.Any(r =>
283-
HttpUtilities.IsSuccessStatusCode(r.Key) &&
284-
r.Value.Schema?.ActualSchema.Type == JsonObjectType.Array);
282+
var isCollection = o.Operation.HasActualResponse(static (code, response) =>
283+
HttpUtilities.IsSuccessStatusCode(code) &&
284+
response.Schema?.ActualSchema.Type == JsonObjectType.Array);
285285

286286
if (isCollection)
287287
{

src/NSwag.Core/OpenApiOperation.cs

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
using System.Collections.ObjectModel;
1010
using System.Collections.Specialized;
11+
using System.Runtime.CompilerServices;
1112
using Newtonsoft.Json;
1213
using Newtonsoft.Json.Converters;
1314
using NJsonSchema;
@@ -24,6 +25,8 @@ public class OpenApiOperation : JsonExtensionObject
2425
private bool _disableRequestBodyUpdate;
2526
private bool _disableBodyParameterUpdate;
2627

28+
private readonly ObservableDictionary<string, OpenApiResponse> _responses;
29+
2730
/// <summary>Initializes a new instance of the <see cref="OpenApiPathItem"/> class.</summary>
2831
public OpenApiOperation()
2932
{
@@ -42,14 +45,14 @@ public OpenApiOperation()
4245
Parameters = parameters;
4346

4447
var responses = new ObservableDictionary<string, OpenApiResponse>();
48+
_responses = responses;
4549
responses.CollectionChanged += (sender, args) =>
4650
{
47-
foreach (var response in Responses.Values)
51+
foreach (var pair in _responses)
4852
{
49-
response.Parent = this;
53+
pair.Value.Parent = this;
5054
}
5155
};
52-
Responses = responses;
5356
}
5457

5558
/// <summary>Gets the parent operations list.</summary>
@@ -132,7 +135,7 @@ public IReadOnlyList<OpenApiParameter> ActualParameters
132135

133136
/// <summary>Gets or sets the HTTP Status Code/Response pairs.</summary>
134137
[JsonProperty(PropertyName = "responses", Order = 10, Required = Required.Always, DefaultValueHandling = DefaultValueHandling.Ignore)]
135-
public IDictionary<string, OpenApiResponse> Responses { get; }
138+
public IDictionary<string, OpenApiResponse> Responses => _responses;
136139

137140
/// <summary>Gets or sets the schemes.</summary>
138141
[JsonProperty(PropertyName = "schemes", Order = 11, DefaultValueHandling = DefaultValueHandling.Ignore, ItemConverterType = typeof(StringEnumConverter))]
@@ -172,7 +175,79 @@ public IReadOnlyList<OpenApiParameter> ActualParameters
172175

173176
/// <summary>Gets the responses from the operation and from the <see cref="OpenApiDocument"/> and dereferences them if necessary.</summary>
174177
[JsonIgnore]
175-
public IReadOnlyDictionary<string, OpenApiResponse> ActualResponses => Responses.ToDictionary(t => t.Key, t => t.Value.ActualResponse);
178+
public IReadOnlyDictionary<string, OpenApiResponse> ActualResponses
179+
{
180+
get
181+
{
182+
var dictionary = new Dictionary<string, OpenApiResponse>(_responses.Count);
183+
foreach (var response in _responses)
184+
{
185+
dictionary.Add(response.Key, response.Value.ActualResponse);
186+
}
187+
return dictionary;
188+
}
189+
}
190+
191+
// helpers to avoid extra allocations
192+
internal IEnumerable<KeyValuePair<string, OpenApiResponse>> GetActualResponses(Func<string, OpenApiResponse, bool> predicate)
193+
{
194+
foreach (var pair in _responses)
195+
{
196+
if (predicate(pair.Key, pair.Value.ActualResponse))
197+
{
198+
yield return new KeyValuePair<string, OpenApiResponse>(pair.Key, pair.Value.ActualResponse);
199+
}
200+
}
201+
}
202+
203+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
204+
internal bool HasActualResponse(Func<string, OpenApiResponse, bool> predicate) => GetActualResponse(predicate) != null;
205+
206+
internal OpenApiResponse GetActualResponse(Func<string, OpenApiResponse, bool> predicate)
207+
{
208+
foreach (var pair in _responses)
209+
{
210+
if (predicate(pair.Key, pair.Value.ActualResponse))
211+
{
212+
return pair.Value.ActualResponse;
213+
}
214+
}
215+
216+
return null;
217+
}
218+
219+
internal KeyValuePair<string,OpenApiResponse> GetSuccessResponse()
220+
{
221+
KeyValuePair<string, OpenApiResponse> firstOtherSuccessResponse = new(null, null);
222+
OpenApiResponse defaultResponse = null;
223+
foreach (var pair in _responses)
224+
{
225+
var code = pair.Key;
226+
var actualResponse = pair.Value.ActualResponse;
227+
228+
if (code == "200")
229+
{
230+
// 200 is the default response
231+
return new KeyValuePair<string, OpenApiResponse>(code, actualResponse);
232+
}
233+
234+
if (firstOtherSuccessResponse.Key == null && HttpUtilities.IsSuccessStatusCode(code))
235+
{
236+
firstOtherSuccessResponse = new KeyValuePair<string, OpenApiResponse>(code, actualResponse);
237+
}
238+
else if (code == "default")
239+
{
240+
defaultResponse = actualResponse;
241+
}
242+
}
243+
244+
if (firstOtherSuccessResponse.Key != null)
245+
{
246+
return firstOtherSuccessResponse;
247+
}
248+
249+
return new KeyValuePair<string, OpenApiResponse>("default", defaultResponse);
250+
}
176251

177252
/// <summary>Gets the actual security description, either from the operation or from the <see cref="OpenApiDocument"/>.</summary>
178253
[JsonIgnore]

0 commit comments

Comments
 (0)