Skip to content

Commit c93797f

Browse files
authored
Fix compile error in generated C# client when media type contains quotes (#5345)
1 parent c5fcfd4 commit c93797f

File tree

5 files changed

+363
-10
lines changed

5 files changed

+363
-10
lines changed

src/NSwag.CodeGeneration.CSharp.Tests/CodeGenerationTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,58 @@ public async Task When_path_starts_with_numeric_can_generate_client()
354354
CSharpCompiler.AssertCompile(code);
355355
}
356356

357+
[Fact]
358+
public async Task When_media_type_contains_quotes_can_generate_client()
359+
{
360+
// Arrange
361+
const string mediaType = "application/vnd.api+json; ext=\"https://jsonapi.org/ext/atomic https://www.jsonapi.net/ext/openapi\"";
362+
string mediaTypeJsonEscaped = mediaType.Replace("\"", "\\\"");
363+
364+
string json =
365+
$$"""
366+
{
367+
"openapi": "3.0.1",
368+
"paths": {
369+
"/Test": {
370+
"post": {
371+
"requestBody": {
372+
"content": {
373+
"{{mediaTypeJsonEscaped}}": {
374+
"schema": {
375+
"type": "object"
376+
}
377+
}
378+
}
379+
},
380+
"responses": {
381+
"200": {
382+
"description": "OK",
383+
"content": {
384+
"{{mediaTypeJsonEscaped}}": {
385+
"schema": {
386+
"type": "object"
387+
}
388+
}
389+
}
390+
}
391+
}
392+
}
393+
}
394+
}
395+
}
396+
""";
397+
398+
var document = await OpenApiDocument.FromJsonAsync(json);
399+
var codeGenerator = new CSharpClientGenerator(document, new CSharpClientGeneratorSettings());
400+
401+
// Act
402+
var code = codeGenerator.GenerateFile();
403+
404+
// Assert
405+
await VerifyHelper.Verify(code);
406+
CSharpCompiler.AssertCompile(code);
407+
}
408+
357409
private static OpenApiDocument CreateDocument()
358410
{
359411
var document = new OpenApiDocument();

src/NSwag.CodeGeneration.CSharp.Tests/ControllerGenerationFormatTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,10 +427,10 @@ public async Task When_controllertarget_aspnet_and_multiple_controllers_then_onl
427427
var code = codeGen.GenerateFile();
428428

429429
// Assert
430-
var fromHeaderCustomAttributeCount = Regex.Matches(code, "public class FromHeaderAttribute :").Count;
430+
var fromHeaderCustomAttributeCount = Regex.Count(code, "public class FromHeaderAttribute :");
431431
Assert.Equal(1, fromHeaderCustomAttributeCount);
432432

433-
var fromHeaderCustomBindingCount = Regex.Matches(code, "public class FromHeaderBinding :").Count;
433+
var fromHeaderCustomBindingCount = Regex.Count(code, "public class FromHeaderBinding :");
434434
Assert.Equal(1, fromHeaderCustomBindingCount);
435435

436436
await VerifyHelper.Verify(code);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+

2+
3+
namespace MyNamespace
4+
{
5+
using System = global::System;
6+
7+
public partial class Client
8+
{
9+
#pragma warning disable 8618
10+
private string _baseUrl;
11+
#pragma warning restore 8618
12+
13+
private System.Net.Http.HttpClient _httpClient;
14+
private static System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(CreateSerializerSettings, true);
15+
private Newtonsoft.Json.JsonSerializerSettings _instanceSettings;
16+
17+
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
18+
public Client(string baseUrl, System.Net.Http.HttpClient httpClient)
19+
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
20+
{
21+
BaseUrl = baseUrl;
22+
_httpClient = httpClient;
23+
Initialize();
24+
}
25+
26+
private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings()
27+
{
28+
var settings = new Newtonsoft.Json.JsonSerializerSettings();
29+
UpdateJsonSerializerSettings(settings);
30+
return settings;
31+
}
32+
33+
public string BaseUrl
34+
{
35+
get { return _baseUrl; }
36+
set
37+
{
38+
_baseUrl = value;
39+
if (!string.IsNullOrEmpty(_baseUrl) && !_baseUrl.EndsWith("/"))
40+
_baseUrl += '/';
41+
}
42+
}
43+
44+
protected Newtonsoft.Json.JsonSerializerSettings JsonSerializerSettings { get { return _instanceSettings ?? _settings.Value; } }
45+
46+
static partial void UpdateJsonSerializerSettings(Newtonsoft.Json.JsonSerializerSettings settings);
47+
48+
partial void Initialize();
49+
50+
partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url);
51+
partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder);
52+
partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response);
53+
54+
public virtual System.Threading.Tasks.Task<object> TestAsync(object body)
55+
{
56+
return TestAsync(body, System.Threading.CancellationToken.None);
57+
}
58+
59+
public virtual async System.Threading.Tasks.Task<object> TestAsync(object body, System.Threading.CancellationToken cancellationToken)
60+
{
61+
var client_ = _httpClient;
62+
var disposeClient_ = false;
63+
try
64+
{
65+
using (var request_ = new System.Net.Http.HttpRequestMessage())
66+
{
67+
var json_ = Newtonsoft.Json.JsonConvert.SerializeObject(body, JsonSerializerSettings);
68+
var content_ = new System.Net.Http.StringContent(json_);
69+
content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/vnd.api+json; ext=\"https://jsonapi.org/ext/atomic https://www.jsonapi.net/ext/openapi\"");
70+
request_.Content = content_;
71+
request_.Method = new System.Net.Http.HttpMethod("POST");
72+
request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/vnd.api+json; ext=\"https://jsonapi.org/ext/atomic https://www.jsonapi.net/ext/openapi\""));
73+
74+
var urlBuilder_ = new System.Text.StringBuilder();
75+
if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl);
76+
// Operation Path: "Test"
77+
urlBuilder_.Append("Test");
78+
79+
PrepareRequest(client_, request_, urlBuilder_);
80+
81+
var url_ = urlBuilder_.ToString();
82+
request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
83+
84+
PrepareRequest(client_, request_, url_);
85+
86+
var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
87+
var disposeResponse_ = true;
88+
try
89+
{
90+
var headers_ = new System.Collections.Generic.Dictionary<string, System.Collections.Generic.IEnumerable<string>>();
91+
foreach (var item_ in response_.Headers)
92+
headers_[item_.Key] = item_.Value;
93+
if (response_.Content != null && response_.Content.Headers != null)
94+
{
95+
foreach (var item_ in response_.Content.Headers)
96+
headers_[item_.Key] = item_.Value;
97+
}
98+
99+
ProcessResponse(client_, response_);
100+
101+
var status_ = (int)response_.StatusCode;
102+
if (status_ == 200)
103+
{
104+
var objectResponse_ = await ReadObjectResponseAsync<object>(response_, headers_, cancellationToken).ConfigureAwait(false);
105+
if (objectResponse_.Object == null)
106+
{
107+
throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
108+
}
109+
return objectResponse_.Object;
110+
}
111+
else
112+
{
113+
var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false);
114+
throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
115+
}
116+
}
117+
finally
118+
{
119+
if (disposeResponse_)
120+
response_.Dispose();
121+
}
122+
}
123+
}
124+
finally
125+
{
126+
if (disposeClient_)
127+
client_.Dispose();
128+
}
129+
}
130+
131+
protected struct ObjectResponseResult<T>
132+
{
133+
public ObjectResponseResult(T responseObject, string responseText)
134+
{
135+
this.Object = responseObject;
136+
this.Text = responseText;
137+
}
138+
139+
public T Object { get; }
140+
141+
public string Text { get; }
142+
}
143+
144+
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
145+
private static System.Threading.Tasks.Task<string> ReadAsStringAsync(System.Net.Http.HttpContent content, System.Threading.CancellationToken cancellationToken)
146+
{
147+
#if NET5_0_OR_GREATER
148+
return content.ReadAsStringAsync(cancellationToken);
149+
#else
150+
return content.ReadAsStringAsync();
151+
#endif
152+
}
153+
154+
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
155+
private static System.Threading.Tasks.Task<System.IO.Stream> ReadAsStreamAsync(System.Net.Http.HttpContent content, System.Threading.CancellationToken cancellationToken)
156+
{
157+
#if NET5_0_OR_GREATER
158+
return content.ReadAsStreamAsync(cancellationToken);
159+
#else
160+
return content.ReadAsStreamAsync();
161+
#endif
162+
}
163+
164+
public bool ReadResponseAsString { get; set; }
165+
166+
protected virtual async System.Threading.Tasks.Task<ObjectResponseResult<T>> ReadObjectResponseAsync<T>(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.Threading.CancellationToken cancellationToken)
167+
{
168+
if (response == null || response.Content == null)
169+
{
170+
return new ObjectResponseResult<T>(default(T), string.Empty);
171+
}
172+
173+
if (ReadResponseAsString)
174+
{
175+
var responseText = await ReadAsStringAsync(response.Content, cancellationToken).ConfigureAwait(false);
176+
try
177+
{
178+
var typedBody = Newtonsoft.Json.JsonConvert.DeserializeObject<T>(responseText, JsonSerializerSettings);
179+
return new ObjectResponseResult<T>(typedBody, responseText);
180+
}
181+
catch (Newtonsoft.Json.JsonException exception)
182+
{
183+
var message = "Could not deserialize the response body string as " + typeof(T).FullName + ".";
184+
throw new ApiException(message, (int)response.StatusCode, responseText, headers, exception);
185+
}
186+
}
187+
else
188+
{
189+
try
190+
{
191+
using (var responseStream = await ReadAsStreamAsync(response.Content, cancellationToken).ConfigureAwait(false))
192+
using (var streamReader = new System.IO.StreamReader(responseStream))
193+
using (var jsonTextReader = new Newtonsoft.Json.JsonTextReader(streamReader))
194+
{
195+
var serializer = Newtonsoft.Json.JsonSerializer.Create(JsonSerializerSettings);
196+
var typedBody = serializer.Deserialize<T>(jsonTextReader);
197+
return new ObjectResponseResult<T>(typedBody, string.Empty);
198+
}
199+
}
200+
catch (Newtonsoft.Json.JsonException exception)
201+
{
202+
var message = "Could not deserialize the response body stream as " + typeof(T).FullName + ".";
203+
throw new ApiException(message, (int)response.StatusCode, string.Empty, headers, exception);
204+
}
205+
}
206+
}
207+
208+
private string ConvertToString(object value, System.Globalization.CultureInfo cultureInfo)
209+
{
210+
if (value == null)
211+
{
212+
return "";
213+
}
214+
215+
if (value is System.Enum)
216+
{
217+
var name = System.Enum.GetName(value.GetType(), value);
218+
if (name != null)
219+
{
220+
var field_ = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name);
221+
if (field_ != null)
222+
{
223+
var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field_, typeof(System.Runtime.Serialization.EnumMemberAttribute))
224+
as System.Runtime.Serialization.EnumMemberAttribute;
225+
if (attribute != null)
226+
{
227+
return attribute.Value != null ? attribute.Value : name;
228+
}
229+
}
230+
231+
var converted = System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo));
232+
return converted == null ? string.Empty : converted;
233+
}
234+
}
235+
else if (value is bool)
236+
{
237+
return System.Convert.ToString((bool)value, cultureInfo).ToLowerInvariant();
238+
}
239+
else if (value is byte[])
240+
{
241+
return System.Convert.ToBase64String((byte[]) value);
242+
}
243+
else if (value is string[])
244+
{
245+
return string.Join(",", (string[])value);
246+
}
247+
else if (value.GetType().IsArray)
248+
{
249+
var valueArray = (System.Array)value;
250+
var valueTextArray = new string[valueArray.Length];
251+
for (var i = 0; i < valueArray.Length; i++)
252+
{
253+
valueTextArray[i] = ConvertToString(valueArray.GetValue(i), cultureInfo);
254+
}
255+
return string.Join(",", valueTextArray);
256+
}
257+
258+
var result = System.Convert.ToString(value, cultureInfo);
259+
return result == null ? "" : result;
260+
}
261+
}
262+
263+
264+
265+
266+
267+
public partial class ApiException : System.Exception
268+
{
269+
public int StatusCode { get; private set; }
270+
271+
public string Response { get; private set; }
272+
273+
public System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> Headers { get; private set; }
274+
275+
public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.Exception innerException)
276+
: base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + ((response == null) ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length)), innerException)
277+
{
278+
StatusCode = statusCode;
279+
Response = response;
280+
Headers = headers;
281+
}
282+
283+
public override string ToString()
284+
{
285+
return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString());
286+
}
287+
}
288+
289+
public partial class ApiException<TResult> : ApiException
290+
{
291+
public TResult Result { get; private set; }
292+
293+
public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, TResult result, System.Exception innerException)
294+
: base(message, statusCode, response, headers, innerException)
295+
{
296+
Result = result;
297+
}
298+
}
299+
300+
}

0 commit comments

Comments
 (0)