Skip to content

Commit d7a7faf

Browse files
Merge pull request #1085 from Fellmonkey/master
feat: migrate .NET templates to System.Text.Json + add .NET 9.0 tests
2 parents 7381d2d + 7e858d7 commit d7a7faf

File tree

13 files changed

+258
-97
lines changed

13 files changed

+258
-97
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jobs:
2424
Deno1303,
2525
DotNet60,
2626
DotNet80,
27+
DotNet90,
2728
FlutterStable,
2829
FlutterBeta,
2930
Go112,

src/SDK/Language/DotNet.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,11 @@ public function getFiles(): array
380380
'destination' => '{{ spec.title | caseUcfirst }}/Converters/ValueClassConverter.cs',
381381
'template' => 'dotnet/Package/Converters/ValueClassConverter.cs.twig',
382382
],
383+
[
384+
'scope' => 'default',
385+
'destination' => '{{ spec.title | caseUcfirst }}/Converters/ObjectToInferredTypesConverter.cs',
386+
'template' => 'dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig',
387+
],
383388
[
384389
'scope' => 'default',
385390
'destination' => '{{ spec.title | caseUcfirst }}/Extensions/Extensions.cs',

templates/dotnet/Package/Client.cs.twig

Lines changed: 102 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
using Newtonsoft.Json;
2-
using Newtonsoft.Json.Converters;
3-
using Newtonsoft.Json.Linq;
4-
using Newtonsoft.Json.Serialization;
51
using System;
62
using System.Collections.Generic;
73
using System.IO;
84
using System.Linq;
95
using System.Net.Http;
106
using System.Net.Http.Headers;
117
using System.Text;
8+
using System.Text.Json;
9+
using System.Text.Json.Serialization;
1210
using System.Threading.Tasks;
1311
using {{ spec.title | caseUcfirst }}.Converters;
1412
using {{ spec.title | caseUcfirst }}.Extensions;
@@ -29,26 +27,28 @@ namespace {{ spec.title | caseUcfirst }}
2927

3028
private static readonly int ChunkSize = 5 * 1024 * 1024;
3129

32-
public static JsonSerializerSettings DeserializerSettings { get; set; } = new JsonSerializerSettings
30+
public static JsonSerializerOptions DeserializerOptions { get; set; } = new JsonSerializerOptions
3331
{
34-
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
35-
NullValueHandling = NullValueHandling.Ignore,
36-
ContractResolver = new CamelCasePropertyNamesContractResolver(),
37-
Converters = new List<JsonConverter>
32+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
33+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
34+
PropertyNameCaseInsensitive = true,
35+
Converters =
3836
{
39-
new StringEnumConverter(new CamelCaseNamingStrategy()),
40-
new ValueClassConverter()
37+
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase),
38+
new ValueClassConverter(),
39+
new ObjectToInferredTypesConverter()
4140
}
4241
};
4342

44-
public static JsonSerializerSettings SerializerSettings { get; set; } = new JsonSerializerSettings
43+
public static JsonSerializerOptions SerializerOptions { get; set; } = new JsonSerializerOptions
4544
{
46-
NullValueHandling = NullValueHandling.Ignore,
47-
ContractResolver = new CamelCasePropertyNamesContractResolver(),
48-
Converters = new List<JsonConverter>
45+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
46+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
47+
Converters =
4948
{
50-
new StringEnumConverter(new CamelCaseNamingStrategy()),
51-
new ValueClassConverter()
49+
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase),
50+
new ValueClassConverter(),
51+
new ObjectToInferredTypesConverter()
5252
}
5353
};
5454

@@ -69,14 +69,14 @@ namespace {{ spec.title | caseUcfirst }}
6969
_headers = new Dictionary<string, string>()
7070
{
7171
{ "content-type", "application/json" },
72-
{ "user-agent" , "{{spec.title | caseUcfirst}}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} (${Environment.OSVersion.Platform}; ${Environment.OSVersion.VersionString})"},
72+
{ "user-agent" , $"{{spec.title | caseUcfirst}}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} ({Environment.OSVersion.Platform}; {Environment.OSVersion.VersionString})"},
7373
{ "x-sdk-name", "{{ sdk.name }}" },
7474
{ "x-sdk-platform", "{{ sdk.platform }}" },
7575
{ "x-sdk-language", "{{ language.name | caseLower }}" },
76-
{ "x-sdk-version", "{{ sdk.version }}"}{% if spec.global.defaultHeaders | length > 0 %},{% endif %}
76+
{ "x-sdk-version", "{{ sdk.version }}"}{% if spec.global.defaultHeaders | length > 0 %},
7777
{%~ for key,header in spec.global.defaultHeaders %}
7878
{ "{{key}}", "{{header}}" }{% if not loop.last %},{% endif %}
79-
{%~ endfor %}
79+
{%~ endfor %}{% endif %}
8080

8181
};
8282

@@ -86,8 +86,6 @@ namespace {{ spec.title | caseUcfirst }}
8686
{
8787
SetSelfSigned(true);
8888
}
89-
90-
JsonConvert.DefaultSettings = () => DeserializerSettings;
9189
}
9290

9391
public Client SetSelfSigned(bool selfSigned)
@@ -158,19 +156,23 @@ namespace {{ spec.title | caseUcfirst }}
158156
{
159157
if (parameter.Key == "file")
160158
{
161-
form.Add(((MultipartFormDataContent)parameters["file"]).First()!);
159+
var fileContent = parameters["file"] as MultipartFormDataContent;
160+
if (fileContent != null)
161+
{
162+
form.Add(fileContent.First()!);
163+
}
162164
}
163165
else if (parameter.Value is IEnumerable<object> enumerable)
164166
{
165167
var list = new List<object>(enumerable);
166168
for (int index = 0; index < list.Count; index++)
167169
{
168-
form.Add(new StringContent(list[index].ToString()!), $"{parameter.Key}[{index}]");
170+
form.Add(new StringContent(list[index]?.ToString() ?? string.Empty), $"{parameter.Key}[{index}]");
169171
}
170172
}
171173
else
172174
{
173-
form.Add(new StringContent(parameter.Value.ToString()!), parameter.Key);
175+
form.Add(new StringContent(parameter.Value?.ToString() ?? string.Empty), parameter.Key);
174176
}
175177
}
176178
request.Content = form;
@@ -243,16 +245,27 @@ namespace {{ spec.title | caseUcfirst }}
243245
}
244246

245247
if (contentType.Contains("application/json")) {
246-
message = JObject.Parse(text)["message"]!.ToString();
247-
type = JObject.Parse(text)["type"]?.ToString() ?? string.Empty;
248+
try
249+
{
250+
using var errorDoc = JsonDocument.Parse(text);
251+
message = errorDoc.RootElement.GetProperty("message").GetString() ?? "";
252+
if (errorDoc.RootElement.TryGetProperty("type", out var typeElement))
253+
{
254+
type = typeElement.GetString() ?? "";
255+
}
256+
}
257+
catch
258+
{
259+
message = text;
260+
}
248261
} else {
249262
message = text;
250263
}
251264

252265
throw new {{spec.title | caseUcfirst}}Exception(message, code, type, text);
253266
}
254267

255-
return response.Headers.Location.OriginalString;
268+
return response.Headers.Location?.OriginalString ?? string.Empty;
256269
}
257270

258271
public Task<Dictionary<string, object?>> Call(
@@ -298,8 +311,19 @@ namespace {{ spec.title | caseUcfirst }}
298311
var type = "";
299312

300313
if (isJson) {
301-
message = JObject.Parse(text)["message"]!.ToString();
302-
type = JObject.Parse(text)["type"]?.ToString() ?? string.Empty;
314+
try
315+
{
316+
using var errorDoc = JsonDocument.Parse(text);
317+
message = errorDoc.RootElement.GetProperty("message").GetString() ?? "";
318+
if (errorDoc.RootElement.TryGetProperty("type", out var typeElement))
319+
{
320+
type = typeElement.GetString() ?? "";
321+
}
322+
}
323+
catch
324+
{
325+
message = text;
326+
}
303327
} else {
304328
message = text;
305329
}
@@ -311,13 +335,13 @@ namespace {{ spec.title | caseUcfirst }}
311335
{
312336
var responseString = await response.Content.ReadAsStringAsync();
313337

314-
var dict = JsonConvert.DeserializeObject<Dictionary<string, object>>(
338+
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(
315339
responseString,
316-
DeserializerSettings);
340+
DeserializerOptions);
317341

318-
if (convert != null)
342+
if (convert != null && dict != null)
319343
{
320-
return convert(dict!);
344+
return convert(dict);
321345
}
322346

323347
return (dict as T)!;
@@ -337,7 +361,16 @@ namespace {{ spec.title | caseUcfirst }}
337361
string? idParamName = null,
338362
Action<UploadProgress>? onProgress = null) where T : class
339363
{
364+
if (string.IsNullOrEmpty(paramName))
365+
throw new ArgumentException("Parameter name cannot be null or empty", nameof(paramName));
366+
367+
if (!parameters.ContainsKey(paramName))
368+
throw new ArgumentException($"Parameter {paramName} not found", nameof(paramName));
369+
340370
var input = parameters[paramName] as InputFile;
371+
if (input == null)
372+
throw new ArgumentException($"Parameter {paramName} must be an InputFile", nameof(paramName));
373+
341374
var size = 0L;
342375
switch(input.SourceType)
343376
{
@@ -347,10 +380,16 @@ namespace {{ spec.title | caseUcfirst }}
347380
size = info.Length;
348381
break;
349382
case "stream":
350-
size = (input.Data as Stream).Length;
383+
var stream = input.Data as Stream;
384+
if (stream == null)
385+
throw new InvalidOperationException("Stream data is null");
386+
size = stream.Length;
351387
break;
352388
case "bytes":
353-
size = ((byte[])input.Data).Length;
389+
var bytes = input.Data as byte[];
390+
if (bytes == null)
391+
throw new InvalidOperationException("Byte array data is null");
392+
size = bytes.Length;
354393
break;
355394
};
356395

@@ -364,10 +403,16 @@ namespace {{ spec.title | caseUcfirst }}
364403
{
365404
case "path":
366405
case "stream":
367-
await (input.Data as Stream).ReadAsync(buffer, 0, (int)size);
406+
var dataStream = input.Data as Stream;
407+
if (dataStream == null)
408+
throw new InvalidOperationException("Stream data is null");
409+
await dataStream.ReadAsync(buffer, 0, (int)size);
368410
break;
369411
case "bytes":
370-
buffer = (byte[])input.Data;
412+
var dataBytes = input.Data as byte[];
413+
if (dataBytes == null)
414+
throw new InvalidOperationException("Byte array data is null");
415+
buffer = dataBytes;
371416
break;
372417
}
373418

@@ -393,14 +438,16 @@ namespace {{ spec.title | caseUcfirst }}
393438
// Make a request to check if a file already exists
394439
var current = await Call<Dictionary<string, object?>>(
395440
method: "GET",
396-
path: $"{path}/{parameters[idParamName]}",
441+
path: $"{path}/{parameters[idParamName!]}",
397442
new Dictionary<string, string> { { "content-type", "application/json" } },
398443
parameters: new Dictionary<string, object?>()
399444
);
400-
var chunksUploaded = (long)current["chunksUploaded"];
401-
offset = chunksUploaded * ChunkSize;
445+
if (current.TryGetValue("chunksUploaded", out var chunksUploadedValue) && chunksUploadedValue != null)
446+
{
447+
offset = Convert.ToInt64(chunksUploadedValue) * ChunkSize;
448+
}
402449
}
403-
catch (Exception ex)
450+
catch
404451
{
405452
// ignored as it mostly means file not found
406453
}
@@ -413,6 +460,8 @@ namespace {{ spec.title | caseUcfirst }}
413460
case "path":
414461
case "stream":
415462
var stream = input.Data as Stream;
463+
if (stream == null)
464+
throw new InvalidOperationException("Stream data is null");
416465
stream.Seek(offset, SeekOrigin.Begin);
417466
await stream.ReadAsync(buffer, 0, ChunkSize);
418467
break;
@@ -445,12 +494,12 @@ namespace {{ spec.title | caseUcfirst }}
445494
var id = result.ContainsKey("$id")
446495
? result["$id"]?.ToString() ?? string.Empty
447496
: string.Empty;
448-
var chunksTotal = result.ContainsKey("chunksTotal")
449-
? (long)result["chunksTotal"]
450-
: 0;
451-
var chunksUploaded = result.ContainsKey("chunksUploaded")
452-
? (long)result["chunksUploaded"]
453-
: 0;
497+
var chunksTotal = result.TryGetValue("chunksTotal", out var chunksTotalValue) && chunksTotalValue != null
498+
? Convert.ToInt64(chunksTotalValue)
499+
: 0L;
500+
var chunksUploaded = result.TryGetValue("chunksUploaded", out var chunksUploadedValue) && chunksUploadedValue != null
501+
? Convert.ToInt64(chunksUploadedValue)
502+
: 0L;
454503

455504
headers["x-appwrite-id"] = id;
456505

@@ -463,7 +512,11 @@ namespace {{ spec.title | caseUcfirst }}
463512
chunksUploaded: chunksUploaded));
464513
}
465514

466-
return converter(result);
515+
// Convert to non-nullable dictionary for converter
516+
var nonNullableResult = result.Where(kvp => kvp.Value != null)
517+
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value!);
518+
519+
return converter(nonNullableResult);
467520
}
468521
}
469522
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text.Json;
4+
using System.Text.Json.Serialization;
5+
6+
namespace {{ spec.title | caseUcfirst }}.Converters
7+
{
8+
public class ObjectToInferredTypesConverter : JsonConverter<object>
9+
{
10+
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
11+
{
12+
switch (reader.TokenType)
13+
{
14+
case JsonTokenType.True:
15+
return true;
16+
case JsonTokenType.False:
17+
return false;
18+
case JsonTokenType.Number:
19+
if (reader.TryGetInt64(out long l))
20+
{
21+
return l;
22+
}
23+
return reader.GetDouble();
24+
case JsonTokenType.String:
25+
if (reader.TryGetDateTime(out DateTime datetime))
26+
{
27+
return datetime;
28+
}
29+
return reader.GetString()!;
30+
case JsonTokenType.StartObject:
31+
return JsonSerializer.Deserialize<Dictionary<string, object>>(ref reader, options)!;
32+
case JsonTokenType.StartArray:
33+
return JsonSerializer.Deserialize<object[]>(ref reader, options)!;
34+
default:
35+
return JsonDocument.ParseValue(ref reader).RootElement.Clone();
36+
}
37+
}
38+
39+
public override void Write(Utf8JsonWriter writer, object objectToWrite, JsonSerializerOptions options)
40+
{
41+
JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options);
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)