From 0a48f7aeea8db3454cb3e60bc72c33b84c47fc81 Mon Sep 17 00:00:00 2001 From: Fellmonkey <90258055+Fellmonkey@users.noreply.github.com> Date: Sat, 2 Aug 2025 15:04:00 +0300 Subject: [PATCH 1/4] Refactor object inference in JSON converter Replaces switch on JsonTokenType with a recursive method using JsonElement.ValueKind for more robust and accurate type inference. This improves handling of nested objects and arrays, and unifies the logic for converting JSON values to .NET types. --- .../ObjectToInferredTypesConverter.cs.twig | 64 +++++++++++++------ 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig b/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig index 563f92992..ce772c93d 100644 --- a/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig +++ b/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig @@ -7,32 +7,60 @@ namespace {{ spec.title | caseUcfirst }}.Converters { public class ObjectToInferredTypesConverter : JsonConverter { - public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - switch (reader.TokenType) + using (JsonDocument document = JsonDocument.ParseValue(ref reader)) { - case JsonTokenType.True: - return true; - case JsonTokenType.False: - return false; - case JsonTokenType.Number: - if (reader.TryGetInt64(out long l)) + return ConvertElement(document.RootElement); + } + } + + private object? ConvertElement(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + var dictionary = new Dictionary(); + foreach (var property in element.EnumerateObject()) { - return l; + dictionary[property.Name] = ConvertElement(property.Value); + } + return dictionary; + + case JsonValueKind.Array: + var list = new List(); + foreach (var item in element.EnumerateArray()) + { + list.Add(ConvertElement(item)); } - return reader.GetDouble(); - case JsonTokenType.String: - if (reader.TryGetDateTime(out DateTime datetime)) + return list; + + case JsonValueKind.String: + if (element.TryGetDateTime(out DateTime datetime)) { return datetime; } - return reader.GetString()!; - case JsonTokenType.StartObject: - return JsonSerializer.Deserialize>(ref reader, options)!; - case JsonTokenType.StartArray: - return JsonSerializer.Deserialize(ref reader, options)!; + return element.GetString(); + + case JsonValueKind.Number: + if (element.TryGetInt64(out long l)) + { + return l; + } + return element.GetDouble(); + + case JsonValueKind.True: + return true; + + case JsonValueKind.False: + return false; + + case JsonValueKind.Null: + case JsonValueKind.Undefined: + return null; + default: - return JsonDocument.ParseValue(ref reader).RootElement.Clone(); + throw new JsonException($"Unsupported JsonValueKind: {element.ValueKind}"); } } From b44f0b12a8f0847ffd30166f9180e0e7770ab2dd Mon Sep 17 00:00:00 2001 From: Fellmonkey <90258055+Fellmonkey@users.noreply.github.com> Date: Sat, 2 Aug 2025 15:04:19 +0300 Subject: [PATCH 2/4] Refactor model deserialization logic in C# template Simplifies and standardizes the deserialization of model properties from dictionaries, removing special handling for JsonElement and streamlining array and primitive type conversions. This improves code readability and maintainability in generated model classes. --- templates/dotnet/Package/Models/Model.cs.twig | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/templates/dotnet/Package/Models/Model.cs.twig b/templates/dotnet/Package/Models/Model.cs.twig index ff46ff18e..85468fac0 100644 --- a/templates/dotnet/Package/Models/Model.cs.twig +++ b/templates/dotnet/Package/Models/Model.cs.twig @@ -1,6 +1,5 @@ {% macro sub_schema(property) %}{% if property.sub_schema %}{% if property.type == 'array' %}List<{{property.sub_schema | caseUcfirst | overrideIdentifier}}>{% else %}{{property.sub_schema | caseUcfirst | overrideIdentifier}}{% endif %}{% else %}{{property | typeName}}{% endif %}{% if not property.required %}?{% endif %}{% endmacro %} {% macro property_name(definition, property) %}{{ property.name | caseUcfirst | removeDollarSign | escapeKeyword }}{% endmacro %} - using System; using System.Linq; using System.Collections.Generic; @@ -42,25 +41,21 @@ namespace {{ spec.title | caseUcfirst }}.Models {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}:{{' '}} {%- if property.sub_schema %} {%- if property.type == 'array' -%} - map["{{ property.name }}"] is JsonElement jsonArray{{ loop.index }} ? jsonArray{{ loop.index }}.Deserialize>>()!.Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: it)).ToList() : ((IEnumerable>)map["{{ property.name }}"]).Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: it)).ToList() + ((IEnumerable)map["{{ property.name }}"]).Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary)it)).ToList() {%- else -%} - {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: map["{{ property.name }}"] is JsonElement jsonObj{{ loop.index }} ? jsonObj{{ loop.index }}.Deserialize>()! : (Dictionary)map["{{ property.name }}"]) + {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary)map["{{ property.name }}"]) {%- endif %} {%- else %} {%- if property.type == 'array' -%} - map["{{ property.name }}"] is JsonElement jsonArrayProp{{ loop.index }} ? jsonArrayProp{{ loop.index }}.Deserialize<{{ property | typeName }}>()! : ({{ property | typeName }})map["{{ property.name }}"] + ((IEnumerable)map["{{ property.name }}"]).Select(x => {% if property.items.type == "string" %}x?.ToString(){% elseif property.items.type == "integer" %}{% if not property.required %}x == null ? (long?)null : {% endif %}Convert.ToInt64(x){% elseif property.items.type == "number" %}{% if not property.required %}x == null ? (double?)null : {% endif %}Convert.ToDouble(x){% elseif property.items.type == "boolean" %}{% if not property.required %}x == null ? (bool?)null : {% endif %}(bool)x{% else %}x{% endif %}).{% if property.items.type == "string" and property.required %}Where(x => x != null).{% endif %}ToList()! {%- else %} {%- if property.type == "integer" or property.type == "number" %} - {%- if not property.required -%}map["{{ property.name }}"] == null ? null :{% endif %}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}(map["{{ property.name }}"]) + {%- if not property.required -%}map["{{ property.name }}"] == null ? null : {% endif %}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}(map["{{ property.name }}"]) {%- else %} {%- if property.type == "boolean" -%} ({{ property | typeName }}{% if not property.required %}?{% endif %})map["{{ property.name }}"] - {%- else %} - {%- if not property.required -%} - map.TryGetValue("{{ property.name }}", out var {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}) ? {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}?.ToString() : null - {%- else -%} - map["{{ property.name }}"].ToString() - {%- endif %} + {%- else -%} + map["{{ property.name }}"]{% if not property.required %}?{% endif %}.ToString() {%- endif %} {%~ endif %} {%~ endif %} From 4c6b9f7c0bb3003ff8602d311ad47088e19e6a72 Mon Sep 17 00:00:00 2001 From: Fellmonkey <90258055+Fellmonkey@users.noreply.github.com> Date: Sat, 9 Aug 2025 22:25:23 +0300 Subject: [PATCH 3/4] Handle optional properties in model From method Updated the From method in the model template to check for the existence of optional properties in the input map before assigning values. This prevents errors when optional properties are missing from the input dictionary. (for examle in model: User, :-/ ) --- templates/dotnet/Package/Models/Model.cs.twig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/dotnet/Package/Models/Model.cs.twig b/templates/dotnet/Package/Models/Model.cs.twig index 85468fac0..f4eabaa7d 100644 --- a/templates/dotnet/Package/Models/Model.cs.twig +++ b/templates/dotnet/Package/Models/Model.cs.twig @@ -39,6 +39,7 @@ namespace {{ spec.title | caseUcfirst }}.Models public static {{ definition.name | caseUcfirst | overrideIdentifier }} From(Dictionary map) => new {{ definition.name | caseUcfirst | overrideIdentifier }}( {%~ for property in definition.properties %} {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}:{{' '}} + {%- if not property.required -%}map.ContainsKey("{{ property.name }}") ? {% endif %} {%- if property.sub_schema %} {%- if property.type == 'array' -%} ((IEnumerable)map["{{ property.name }}"]).Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary)it)).ToList() @@ -60,6 +61,7 @@ namespace {{ spec.title | caseUcfirst }}.Models {%~ endif %} {%~ endif %} {%~ endif %} + {%- if not property.required %} : null{% endif %} {%- if not loop.last or (loop.last and definition.additionalProperties) %}, {%~ endif %} {%~ endfor %} @@ -96,4 +98,4 @@ namespace {{ spec.title | caseUcfirst }}.Models {%~ endif %} {%~ endfor %} } -} +} \ No newline at end of file From cf7eb57b22644c4974cb0ba40df827e107983ac8 Mon Sep 17 00:00:00 2001 From: Fellmonkey <90258055+Fellmonkey@users.noreply.github.com> Date: Thu, 14 Aug 2025 09:04:31 +0300 Subject: [PATCH 4/4] synchronization with the Unity template --- .../Package/Converters/ObjectToInferredTypesConverter.cs.twig | 2 +- templates/dotnet/Package/Enums/Enum.cs.twig | 2 +- templates/dotnet/Package/Exception.cs.twig | 2 +- templates/dotnet/Package/Models/InputFile.cs.twig | 2 +- templates/dotnet/Package/Role.cs.twig | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig b/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig index ce772c93d..fd9512f9b 100644 --- a/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig +++ b/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig @@ -69,4 +69,4 @@ namespace {{ spec.title | caseUcfirst }}.Converters JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options); } } -} +} \ No newline at end of file diff --git a/templates/dotnet/Package/Enums/Enum.cs.twig b/templates/dotnet/Package/Enums/Enum.cs.twig index 6720ce59b..d3c768a4e 100644 --- a/templates/dotnet/Package/Enums/Enum.cs.twig +++ b/templates/dotnet/Package/Enums/Enum.cs.twig @@ -16,4 +16,4 @@ namespace {{ spec.title | caseUcfirst }}.Enums public static {{ enum.name | caseUcfirst | overrideIdentifier }} {{ key | caseEnumKey }} => new {{ enum.name | caseUcfirst | overrideIdentifier }}("{{ value }}"); {%~ endfor %} } -} +} \ No newline at end of file diff --git a/templates/dotnet/Package/Exception.cs.twig b/templates/dotnet/Package/Exception.cs.twig index e78d78c2c..31d9c70ad 100644 --- a/templates/dotnet/Package/Exception.cs.twig +++ b/templates/dotnet/Package/Exception.cs.twig @@ -18,10 +18,10 @@ namespace {{spec.title | caseUcfirst}} this.Type = type; this.Response = response; } + public {{spec.title | caseUcfirst}}Exception(string message, Exception inner) : base(message, inner) { } } } - diff --git a/templates/dotnet/Package/Models/InputFile.cs.twig b/templates/dotnet/Package/Models/InputFile.cs.twig index 241a3adad..4464608d0 100644 --- a/templates/dotnet/Package/Models/InputFile.cs.twig +++ b/templates/dotnet/Package/Models/InputFile.cs.twig @@ -1,5 +1,5 @@ using System.IO; -using Appwrite.Extensions; +using {{ spec.title | caseUcfirst }}.Extensions; namespace {{ spec.title | caseUcfirst }}.Models { diff --git a/templates/dotnet/Package/Role.cs.twig b/templates/dotnet/Package/Role.cs.twig index b3ecf2610..4dc45dcb7 100644 --- a/templates/dotnet/Package/Role.cs.twig +++ b/templates/dotnet/Package/Role.cs.twig @@ -1,4 +1,4 @@ -namespace Appwrite +namespace {{ spec.title | caseUcfirst }} { /// /// Helper class to generate role strings for Permission.