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 01/11] 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 02/11] 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 03/11] 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 b071cbcfa8224cb41783c296ec5588eb606987bb 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 04/11] synchronization with the Unity template --- templates/dotnet/Package/Exception.cs.twig | 2 +- templates/dotnet/Package/Extensions/Extensions.cs.twig | 2 +- templates/dotnet/Package/Models/InputFile.cs.twig | 4 ++-- templates/dotnet/Package/Models/Model.cs.twig | 2 +- templates/dotnet/Package/Models/UploadProgress.cs.twig | 2 +- templates/dotnet/Package/Query.cs.twig | 2 +- templates/dotnet/Package/Role.cs.twig | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) 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/Extensions/Extensions.cs.twig b/templates/dotnet/Package/Extensions/Extensions.cs.twig index d57318077..ec325429f 100644 --- a/templates/dotnet/Package/Extensions/Extensions.cs.twig +++ b/templates/dotnet/Package/Extensions/Extensions.cs.twig @@ -624,4 +624,4 @@ namespace {{ spec.title | caseUcfirst }}.Extensions return GetMimeTypeFromExtension(System.IO.Path.GetExtension(path)); } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Models/InputFile.cs.twig b/templates/dotnet/Package/Models/InputFile.cs.twig index 241a3adad..aaf7a6620 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 { @@ -38,4 +38,4 @@ namespace {{ spec.title | caseUcfirst }}.Models SourceType = "bytes" }; } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Models/Model.cs.twig b/templates/dotnet/Package/Models/Model.cs.twig index f4eabaa7d..a142d474e 100644 --- a/templates/dotnet/Package/Models/Model.cs.twig +++ b/templates/dotnet/Package/Models/Model.cs.twig @@ -98,4 +98,4 @@ namespace {{ spec.title | caseUcfirst }}.Models {%~ endif %} {%~ endfor %} } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Models/UploadProgress.cs.twig b/templates/dotnet/Package/Models/UploadProgress.cs.twig index 47c78391c..ee6fb58ba 100644 --- a/templates/dotnet/Package/Models/UploadProgress.cs.twig +++ b/templates/dotnet/Package/Models/UploadProgress.cs.twig @@ -23,4 +23,4 @@ namespace {{ spec.title | caseUcfirst }} ChunksUploaded = chunksUploaded; } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Query.cs.twig b/templates/dotnet/Package/Query.cs.twig index 18359f30c..9c3ec9f82 100644 --- a/templates/dotnet/Package/Query.cs.twig +++ b/templates/dotnet/Package/Query.cs.twig @@ -158,4 +158,4 @@ namespace {{ spec.title | caseUcfirst }} return new Query("and", null, queries.Select(q => JsonSerializer.Deserialize(q, Client.DeserializerOptions)).ToList()).ToString(); } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Role.cs.twig b/templates/dotnet/Package/Role.cs.twig index b3ecf2610..3c7b2b33f 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. @@ -89,4 +89,4 @@ namespace Appwrite return $"label:{name}"; } } -} \ No newline at end of file +} From e9586d2acf1ea2191be450d90924f54fe632bb0e Mon Sep 17 00:00:00 2001 From: Fellmonkey <90258055+Fellmonkey@users.noreply.github.com> Date: Thu, 25 Sep 2025 22:26:15 +0300 Subject: [PATCH 05/11] Refactor model parsing for nullable and array properties Improves the From() method in Model.cs.twig to handle nullable and array properties more robustly, using helper macros for parsing arrays and sub-schemas. This change ensures correct handling of optional fields and type conversions, reducing runtime errors and improving code maintainability. Also removes an unnecessary blank line in ServiceTemplate.cs.twig. --- templates/dotnet/Package/Models/Model.cs.twig | 45 ++++++++++++++++--- .../Package/Services/ServiceTemplate.cs.twig | 1 - 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/templates/dotnet/Package/Models/Model.cs.twig b/templates/dotnet/Package/Models/Model.cs.twig index a142d474e..ef559eaa2 100644 --- a/templates/dotnet/Package/Models/Model.cs.twig +++ b/templates/dotnet/Package/Models/Model.cs.twig @@ -1,5 +1,12 @@ {% 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 %} +{% macro array_source(src, required) %}{% if required %}((IEnumerable){{ src | raw }}){% else %}({{ src | raw }} as IEnumerable ?? Array.Empty()){% endif %}{% endmacro %} +{%~ macro parse_primitive_array(items_type, src, required) -%} + {{ _self.array_source(src, required) }}.Select(x => {% if items_type == "string" %}x?.ToString(){% elseif items_type == "integer" %}{% if not required %}x == null ? (long?)null : {% endif %}Convert.ToInt64(x){% elseif items_type == "number" %}{% if not required %}x == null ? (double?)null : {% endif %}Convert.ToDouble(x){% elseif items_type == "boolean" %}{% if not required %}x == null ? (bool?)null : {% endif %}(bool)x{% else %}x{% endif %}){% if required and items_type == "string" %}.Where(x => x != null){% endif %}.ToList()! +{%- endmacro -%} +{%~ macro parse_subschema_array(sub_schema_name, src, required) -%} + {{ _self.array_source(src, required) }}.Select(it => {{ sub_schema_name | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary)it)).ToList() +{%- endmacro -%} using System; using System.Linq; using System.Collections.Generic; @@ -38,25 +45,49 @@ namespace {{ spec.title | caseUcfirst }}.Models public static {{ definition.name | caseUcfirst | overrideIdentifier }} From(Dictionary map) => new {{ definition.name | caseUcfirst | overrideIdentifier }}( {%~ for property in definition.properties %} + {%~ set v = 'v' ~ loop.index0 %} + {%~ set mapAccess = 'map["' ~ property.name ~ '"]' %} {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}:{{' '}} - {%- if not property.required -%}map.ContainsKey("{{ property.name }}") ? {% endif %} + {%- if not property.required -%}map.TryGetValue("{{ property.name }}", out var {{ v }}) ? {% 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() + {%- if property.required -%} + {{ _self.parse_subschema_array(property.sub_schema, mapAccess, true) }} + {%- else -%} + {{ _self.parse_subschema_array(property.sub_schema, v, false) }} + {%- endif %} {%- else -%} - {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary)map["{{ property.name }}"]) + {%- if property.required -%} + {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary){{ mapAccess | raw }}) + {%- else -%} + ({{ v }} as Dictionary) is { } obj + ? {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: obj) + : null + {%- endif %} {%- endif %} {%- else %} {%- if property.type == 'array' -%} - ((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()! + {%- if property.required -%} + {{ _self.parse_primitive_array(property.items.type, mapAccess, true) }} + {%- else -%} + {{ _self.parse_primitive_array(property.items.type, v, false) }} + {%- endif -%} {%- 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 -%}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}({{ v }}){% else %}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}({{ mapAccess | raw }}){%- endif %} {%- else %} {%- if property.type == "boolean" -%} - ({{ property | typeName }}{% if not property.required %}?{% endif %})map["{{ property.name }}"] + {%- if not property.required -%} + ({{ property | typeName }}?){{ v }} + {%- else -%} + ({{ property | typeName }}){{ mapAccess | raw }} + {%- endif %} {%- else -%} - map["{{ property.name }}"]{% if not property.required %}?{% endif %}.ToString() + {%- if not property.required -%} + {{ v }}?.ToString() + {%- else -%} + {{ mapAccess | raw }}.ToString() + {%- endif %} {%- endif %} {%~ endif %} {%~ endif %} diff --git a/templates/dotnet/Package/Services/ServiceTemplate.cs.twig b/templates/dotnet/Package/Services/ServiceTemplate.cs.twig index 99cf15653..804346973 100644 --- a/templates/dotnet/Package/Services/ServiceTemplate.cs.twig +++ b/templates/dotnet/Package/Services/ServiceTemplate.cs.twig @@ -1,5 +1,4 @@ {% import 'dotnet/base/utils.twig' as utils %} - using System; using System.Collections.Generic; using System.Linq; From 5ad9a4b49610064b3799852ba4680709e6757d1e Mon Sep 17 00:00:00 2001 From: Fellmonkey <90258055+Fellmonkey@users.noreply.github.com> Date: Thu, 25 Sep 2025 23:15:59 +0300 Subject: [PATCH 06/11] Skip null parameters in request parameter loop Fields with null values in multipart are now omitted (so they don't turn into empty strings). --- templates/dotnet/Package/Client.cs.twig | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/dotnet/Package/Client.cs.twig b/templates/dotnet/Package/Client.cs.twig index 8f9527790..6f349ee84 100644 --- a/templates/dotnet/Package/Client.cs.twig +++ b/templates/dotnet/Package/Client.cs.twig @@ -154,6 +154,7 @@ namespace {{ spec.title | caseUcfirst }} foreach (var parameter in parameters) { + if (parameter.Value == null) continue; if (parameter.Key == "file") { var fileContent = parameters["file"] as MultipartFormDataContent; From 953600d0cbdfa5f15fb80df09c0a5a866c961158 Mon Sep 17 00:00:00 2001 From: Fellmonkey <90258055+Fellmonkey@users.noreply.github.com> Date: Sun, 28 Sep 2025 15:14:32 +0300 Subject: [PATCH 07/11] Refactor model class name generation in template --- templates/dotnet/Package/Models/Model.cs.twig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/templates/dotnet/Package/Models/Model.cs.twig b/templates/dotnet/Package/Models/Model.cs.twig index ef559eaa2..7df4b45c6 100644 --- a/templates/dotnet/Package/Models/Model.cs.twig +++ b/templates/dotnet/Package/Models/Model.cs.twig @@ -7,6 +7,7 @@ {%~ macro parse_subschema_array(sub_schema_name, src, required) -%} {{ _self.array_source(src, required) }}.Select(it => {{ sub_schema_name | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary)it)).ToList() {%- endmacro -%} +{% set DefinitionClass = definition.name | caseUcfirst | overrideIdentifier %} using System; using System.Linq; using System.Collections.Generic; @@ -15,7 +16,7 @@ using System.Text.Json.Serialization; namespace {{ spec.title | caseUcfirst }}.Models { - public class {{ definition.name | caseUcfirst | overrideIdentifier }} + public class {{ DefinitionClass }} { {%~ for property in definition.properties %} [JsonPropertyName("{{ property.name }}")] @@ -26,7 +27,7 @@ namespace {{ spec.title | caseUcfirst }}.Models public Dictionary Data { get; private set; } {%~ endif %} - public {{ definition.name | caseUcfirst | overrideIdentifier }}( + public {{ DefinitionClass }}( {%~ for property in definition.properties %} {{ _self.sub_schema(property) }} {{ property.name | caseCamel | escapeKeyword }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} @@ -43,7 +44,7 @@ namespace {{ spec.title | caseUcfirst }}.Models {%~ endif %} } - public static {{ definition.name | caseUcfirst | overrideIdentifier }} From(Dictionary map) => new {{ definition.name | caseUcfirst | overrideIdentifier }}( + public static {{ DefinitionClass }} From(Dictionary map) => new {{ DefinitionClass }}( {%~ for property in definition.properties %} {%~ set v = 'v' ~ loop.index0 %} {%~ set mapAccess = 'map["' ~ property.name ~ '"]' %} From cc2cd62bebcf2f079266d163f7eaa672105de3ee Mon Sep 17 00:00:00 2001 From: Fellmonkey <90258055+Fellmonkey@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:54:08 +0300 Subject: [PATCH 08/11] Add parse_value Twig function for DotNet models Introduces a new parse_value Twig function in DotNet.php to centralize and simplify value parsing logic for model properties. Updates Model.cs.twig to use this function, reducing template complexity and improving maintainability. --- src/SDK/Language/DotNet.php | 80 ++++++++++++++++++- templates/dotnet/Package/Models/Model.cs.twig | 64 ++------------- 2 files changed, 84 insertions(+), 60 deletions(-) diff --git a/src/SDK/Language/DotNet.php b/src/SDK/Language/DotNet.php index 085a503a3..e8f725c43 100644 --- a/src/SDK/Language/DotNet.php +++ b/src/SDK/Language/DotNet.php @@ -467,7 +467,7 @@ public function getFilters(): array } /** - * get sub_scheme and property_name functions + * get sub_scheme, property_name and parse_value functions * @return TwigFunction[] */ public function getFunctions(): array @@ -494,7 +494,7 @@ public function getFunctions(): array } return $result; - }), + }, ['is_safe' => ['html']]), new TwigFunction('property_name', function (array $definition, array $property) { $name = $property['name']; $name = \str_replace('$', '', $name); @@ -504,6 +504,82 @@ public function getFunctions(): array } return $name; }), + new TwigFunction('parse_value', function (array $property, string $mapAccess, string $v) { + $required = $property['required'] ?? false; + + // Handle sub_schema + if (isset($property['sub_schema']) && !empty($property['sub_schema'])) { + $subSchema = \ucfirst($property['sub_schema']); + + if ($property['type'] === 'array') { + $arraySource = $required + ? "((IEnumerable){$mapAccess})" + : "({$v} as IEnumerable)"; + return "{$arraySource}?.Select(it => {$subSchema}.From(map: (Dictionary)it)).ToList()!"; + } else { + if ($required) { + return "{$subSchema}.From(map: (Dictionary){$mapAccess})"; + } + return "({$v} as Dictionary) is { } obj ? {$subSchema}.From(map: obj) : null"; + } + } + + // Handle enum + if (isset($property['enum']) && !empty($property['enum'])) { + $enumName = $property['enumName'] ?? $property['name']; + $enumClass = \ucfirst($enumName); + + if ($required) { + return "new {$enumClass}({$mapAccess}.ToString())"; + } + return "{$v} == null ? null : new {$enumClass}({$v}.ToString())"; + } + + // Handle arrays + if ($property['type'] === 'array') { + $itemsType = $property['items']['type'] ?? 'object'; + $src = $required ? $mapAccess : $v; + $arraySource = $required + ? "((IEnumerable){$src})" + : "({$src} as IEnumerable)"; + + $selectExpression = match($itemsType) { + 'string' => 'x.ToString()', + 'integer' => 'Convert.ToInt64(x)', + 'number' => 'Convert.ToDouble(x)', + 'boolean' => '(bool)x', + default => 'x' + }; + + return "{$arraySource}?.Select(x => {$selectExpression}).ToList()!"; + } + + // Handle integer/number + if ($property['type'] === 'integer' || $property['type'] === 'number') { + $convertMethod = $property['type'] === 'integer' ? 'Int64' : 'Double'; + + if ($required) { + return "Convert.To{$convertMethod}({$mapAccess})"; + } + return "{$v} == null ? null : Convert.To{$convertMethod}({$v})"; + } + + // Handle boolean + if ($property['type'] === 'boolean') { + $typeName = $this->getTypeName($property); + + if ($required) { + return "({$typeName}){$mapAccess}"; + } + return "({$typeName}?){$v}"; + } + + // Handle string type + if ($required) { + return "{$mapAccess}.ToString()"; + } + return "{$v}?.ToString()"; + }, ['is_safe' => ['html']]), ]; } diff --git a/templates/dotnet/Package/Models/Model.cs.twig b/templates/dotnet/Package/Models/Model.cs.twig index d4105c257..1f8c53407 100644 --- a/templates/dotnet/Package/Models/Model.cs.twig +++ b/templates/dotnet/Package/Models/Model.cs.twig @@ -1,3 +1,4 @@ +{% set DefinitionClass = definition.name | caseUcfirst | overrideIdentifier %} using System; using System.Linq; using System.Collections.Generic; @@ -11,7 +12,7 @@ namespace {{ spec.title | caseUcfirst }}.Models { {%~ for property in definition.properties %} [JsonPropertyName("{{ property.name }}")] - public {{ sub_schema(property) | raw }} {{ property_name(definition, property) | overrideProperty(definition.name) }} { get; private set; } + public {{ sub_schema(property) }} {{ property_name(definition, property) | overrideProperty(definition.name) }} { get; private set; } {%~ endfor %} {%~ if definition.additionalProperties %} @@ -20,7 +21,7 @@ namespace {{ spec.title | caseUcfirst }}.Models {%~ endif %} public {{ DefinitionClass }}( {%~ for property in definition.properties %} - {{ sub_schema(property) | raw }} {{ property.name | caseCamel | escapeKeyword }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {{ sub_schema(property) }} {{ property.name | caseCamel | escapeKeyword }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} {%~ endfor %} {%~ if definition.additionalProperties %} @@ -40,62 +41,9 @@ namespace {{ spec.title | caseUcfirst }}.Models {%~ set v = 'v' ~ loop.index0 %} {%~ set mapAccess = 'map["' ~ property.name ~ '"]' %} {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}:{{' '}} - {%- if not property.required -%}map.TryGetValue("{{ property.name }}", out var {{ v }}) ? {% endif %} - {%- if property.sub_schema %} - {%- if property.type == 'array' -%} - {%- if property.required -%} - {{ _self.parse_subschema_array(property.sub_schema, mapAccess, true) }} - {%- else -%} - {{ _self.parse_subschema_array(property.sub_schema, v, false) }} - {%- endif %} - {%- else -%} - {%- if property.required -%} - {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: (Dictionary){{ mapAccess | raw }}) - {%- else -%} - ({{ v }} as Dictionary) is { } obj - ? {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: obj) - : null - {%- endif %} - {%- endif %} - {%- elseif property.enum %} - {%- set enumName = property['enumName'] ?? property.name -%} - {%- if not property.required -%} - map.TryGetValue("{{ property.name }}", out var enumRaw{{ loop.index }}) - ? enumRaw{{ loop.index }} == null - ? null - : new {{ enumName | caseUcfirst }}(enumRaw{{ loop.index }}.ToString()!) - : null - {%- else -%} - new {{ enumName | caseUcfirst }}(map["{{ property.name }}"].ToString()!) - {%- endif %} - {%- else %} - {%- if property.type == 'array' -%} - {%- if property.required -%} - {{ _self.parse_primitive_array(property.items.type, mapAccess, true) }} - {%- else -%} - {{ _self.parse_primitive_array(property.items.type, v, false) }} - {%- endif -%} - {%- else %} - {%- if property.type == "integer" or property.type == "number" %} - {%- if not property.required -%}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}({{ v }}){% else %}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}({{ mapAccess | raw }}){%- endif %} - {%- else %} - {%- if property.type == "boolean" -%} - {%- if not property.required -%} - ({{ property | typeName }}?){{ v }} - {%- else -%} - ({{ property | typeName }}){{ mapAccess | raw }} - {%- endif %} - {%- else -%} - {%- if not property.required -%} - {{ v }}?.ToString() - {%- else -%} - {{ mapAccess | raw }}.ToString() - {%- endif %} - {%- endif %} - {%~ endif %} - {%~ endif %} - {%~ endif %} - {%- if not property.required %} : null{% endif %} + {%- if not property.required -%}map.TryGetValue("{{ property.name }}", out var {{ v }}) ? {% endif -%} +{{ parse_value(property, mapAccess, v) }} + {%- if not property.required %} : null{% endif -%} {%- if not loop.last or (loop.last and definition.additionalProperties) %}, {%~ endif %} {%~ endfor %} From ebbad72cb5c2daebf505a95f485292b131c0d665 Mon Sep 17 00:00:00 2001 From: Fellmonkey <90258055+Fellmonkey@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:34:22 +0300 Subject: [PATCH 09/11] make generated array mappings null-safe Remove null-forgiving operator (!) from optional array mappings and use null-safe casting to preserve null vs empty semantics in generated models. --- src/SDK/Language/DotNet.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SDK/Language/DotNet.php b/src/SDK/Language/DotNet.php index e8f725c43..3ed39f339 100644 --- a/src/SDK/Language/DotNet.php +++ b/src/SDK/Language/DotNet.php @@ -514,8 +514,8 @@ public function getFunctions(): array if ($property['type'] === 'array') { $arraySource = $required ? "((IEnumerable){$mapAccess})" - : "({$v} as IEnumerable)"; - return "{$arraySource}?.Select(it => {$subSchema}.From(map: (Dictionary)it)).ToList()!"; + : "({$v} as IEnumerable)?"; + return "{$arraySource}.Select(it => {$subSchema}.From(map: (Dictionary)it)).ToList()"; } else { if ($required) { return "{$subSchema}.From(map: (Dictionary){$mapAccess})"; @@ -541,7 +541,7 @@ public function getFunctions(): array $src = $required ? $mapAccess : $v; $arraySource = $required ? "((IEnumerable){$src})" - : "({$src} as IEnumerable)"; + : "({$src} as IEnumerable)?"; $selectExpression = match($itemsType) { 'string' => 'x.ToString()', @@ -551,7 +551,7 @@ public function getFunctions(): array default => 'x' }; - return "{$arraySource}?.Select(x => {$selectExpression}).ToList()!"; + return "{$arraySource}.Select(x => {$selectExpression}).ToList()"; } // Handle integer/number From ff2545a602d7d81aeb36f68123ec9d0aaac9d8e7 Mon Sep 17 00:00:00 2001 From: Fellmonkey <90258055+Fellmonkey@users.noreply.github.com> Date: Fri, 3 Oct 2025 07:13:18 +0300 Subject: [PATCH 10/11] lint --- src/SDK/Language/DotNet.php | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/SDK/Language/DotNet.php b/src/SDK/Language/DotNet.php index 3ed39f339..d304aa18c 100644 --- a/src/SDK/Language/DotNet.php +++ b/src/SDK/Language/DotNet.php @@ -506,14 +506,14 @@ public function getFunctions(): array }), new TwigFunction('parse_value', function (array $property, string $mapAccess, string $v) { $required = $property['required'] ?? false; - + // Handle sub_schema if (isset($property['sub_schema']) && !empty($property['sub_schema'])) { $subSchema = \ucfirst($property['sub_schema']); - + if ($property['type'] === 'array') { - $arraySource = $required - ? "((IEnumerable){$mapAccess})" + $arraySource = $required + ? "((IEnumerable){$mapAccess})" : "({$v} as IEnumerable)?"; return "{$arraySource}.Select(it => {$subSchema}.From(map: (Dictionary)it)).ToList()"; } else { @@ -523,7 +523,7 @@ public function getFunctions(): array return "({$v} as Dictionary) is { } obj ? {$subSchema}.From(map: obj) : null"; } } - + // Handle enum if (isset($property['enum']) && !empty($property['enum'])) { $enumName = $property['enumName'] ?? $property['name']; @@ -534,46 +534,46 @@ public function getFunctions(): array } return "{$v} == null ? null : new {$enumClass}({$v}.ToString())"; } - + // Handle arrays if ($property['type'] === 'array') { $itemsType = $property['items']['type'] ?? 'object'; $src = $required ? $mapAccess : $v; - $arraySource = $required - ? "((IEnumerable){$src})" + $arraySource = $required + ? "((IEnumerable){$src})" : "({$src} as IEnumerable)?"; - - $selectExpression = match($itemsType) { + + $selectExpression = match ($itemsType) { 'string' => 'x.ToString()', 'integer' => 'Convert.ToInt64(x)', 'number' => 'Convert.ToDouble(x)', 'boolean' => '(bool)x', default => 'x' }; - + return "{$arraySource}.Select(x => {$selectExpression}).ToList()"; } - + // Handle integer/number if ($property['type'] === 'integer' || $property['type'] === 'number') { $convertMethod = $property['type'] === 'integer' ? 'Int64' : 'Double'; - + if ($required) { return "Convert.To{$convertMethod}({$mapAccess})"; } return "{$v} == null ? null : Convert.To{$convertMethod}({$v})"; } - + // Handle boolean if ($property['type'] === 'boolean') { $typeName = $this->getTypeName($property); - + if ($required) { return "({$typeName}){$mapAccess}"; } return "({$typeName}?){$v}"; } - + // Handle string type if ($required) { return "{$mapAccess}.ToString()"; From faad58532cd4fa839c9f68dd650d0aa7ea11760f Mon Sep 17 00:00:00 2001 From: Fellmonkey <90258055+Fellmonkey@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:25:22 +0300 Subject: [PATCH 11/11] Import Enums namespace conditionally in model template Adds conditional import of the Enums namespace in the Model.cs.twig template only when the model definition contains enum properties. This prevents unnecessary imports and improves template clarity. --- templates/dotnet/Package/Models/Model.cs.twig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/dotnet/Package/Models/Model.cs.twig b/templates/dotnet/Package/Models/Model.cs.twig index 1f8c53407..978b6757d 100644 --- a/templates/dotnet/Package/Models/Model.cs.twig +++ b/templates/dotnet/Package/Models/Model.cs.twig @@ -4,7 +4,9 @@ using System.Linq; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; +{% if definition.properties | filter(p => p.enum) | length > 0 %} using {{ spec.title | caseUcfirst }}.Enums; +{% endif %} namespace {{ spec.title | caseUcfirst }}.Models {