diff --git a/src/SDK/Language/DotNet.php b/src/SDK/Language/DotNet.php index 1a3c64079e..5eb25ad4d0 100644 --- a/src/SDK/Language/DotNet.php +++ b/src/SDK/Language/DotNet.php @@ -522,6 +522,12 @@ public function getFilters(): array new TwigFilter('propertyType', function (array $property, array $spec = []) { return $this->getPropertyType($property, $spec); }), + new TwigFilter('propertyAssignment', function (array $property) { + return $this->getPropertyAssignment($property); + }), + new TwigFilter('toMapValue', function (array $property, string $definitionName) { + return $this->getToMapExpression($property, $definitionName); + }), new TwigFilter('enumExample', function (array $param) { $enumValues = $param['enumValues'] ?? []; if (empty($enumValues)) { @@ -580,7 +586,7 @@ public function getFilters(): array * @param array $spec * @return string */ - protected function getPropertyType(array $property, array $spec = []): string + protected function getPropertyType(array $property, array $spec = [], bool $fullyQualified = true): string { if (isset($property['sub_schema']) && !empty($property['sub_schema'])) { $type = $this->toPascalCase($property['sub_schema']); @@ -593,7 +599,8 @@ protected function getPropertyType(array $property, array $spec = []): string if (isset($property['enum']) && !empty($property['enum'])) { $enumName = $property['enumName'] ?? $property['name']; - return 'Appwrite.Enums.' . $this->toPascalCase($enumName); + $prefix = $fullyQualified ? 'Appwrite.Enums.' : ''; + return $prefix . $this->toPascalCase($enumName); } return $this->getTypeName($property, $spec); @@ -607,39 +614,171 @@ public function getFunctions(): array { return [ new TwigFunction('sub_schema', function (array $property) { - $result = ''; - - if (isset($property['sub_schema']) && !empty($property['sub_schema'])) { - if ($property['type'] === 'array') { - $result = 'List<' . $this->toPascalCase($property['sub_schema']) . '>'; - } else { - $result = $this->toPascalCase($property['sub_schema']); - } - } elseif (isset($property['enum']) && !empty($property['enum'])) { - $enumName = $property['enumName'] ?? $property['name']; - $result = $this->toPascalCase($enumName); - } else { - $result = $this->getTypeName($property); - } + $result = $this->getPropertyType($property, [], false); if (!($property['required'] ?? true)) { $result .= '?'; } return $result; - }), + }, ['is_safe' => ['html']]), new TwigFunction('property_name', function (array $definition, array $property) { - $name = $property['name']; - $name = \str_replace('$', '', $name); - $name = $this->toPascalCase($name); - if (\in_array($name, $this->getKeywords())) { - $name = '@' . $name; - } - return $name; + return $this->getPropertyName($property); }), ]; } + /** + * Generate property name for C# model + * + * @param array $property + * @return string + */ + protected function getPropertyName(array $property): string + { + $name = $property['name']; + $name = \str_replace('$', '', $name); + $name = $this->toPascalCase($name); + if (\in_array($name, $this->getKeywords())) { + $name = '@' . $name; + } + return $name; + } + + /** + * Resolved property name with overrides applied + * + * @param array $property + * @param string $definitionName + * @return string + */ + protected function getResolvedPropertyName(array $property, string $definitionName): string + { + $name = $this->getPropertyName($property); + $overrides = $this->getPropertyOverrides(); + if (isset($overrides[$definitionName][$name])) { + return $overrides[$definitionName][$name]; + } + return $name; + } + + /** + * Generate full property assignment expression for model deserialization (From method). + * Handles TryGetValue wrapping for optional properties internally. + * + * @param array $property + * @return string + */ + protected function getPropertyAssignment(array $property): string + { + $required = $property['required'] ?? false; + $propertyName = $property['name']; + $mapAccess = "map[\"{$propertyName}\"]"; + + if ($required) { + return $this->convertValue($property, $mapAccess); + } + + $v = 'v' . $this->toPascalCase(\str_replace('$', '', $propertyName)); + $tryGet = "map.TryGetValue(\"{$propertyName}\", out var {$v})"; + + // Sub_schema objects — use pattern matching for type-safe cast + if (!empty($property['sub_schema']) && $property['type'] !== 'array') { + $subSchema = $this->toPascalCase($property['sub_schema']); + return "{$tryGet} && {$v} is Dictionary {$v}Map ? {$subSchema}.From(map: {$v}Map) : null"; + } + + // Integer, number, enum — guard with null check to avoid Convert/constructor on null + if (\in_array($property['type'], ['integer', 'number']) || !empty($property['enum'])) { + $expr = $this->convertValue($property, $v); + return "{$tryGet} && {$v} != null ? {$expr} : null"; + } + + // String, boolean, arrays — null-safe conversion + $expr = $this->convertValue($property, $v, false); + return "{$tryGet} ? {$expr} : null"; + } + + /** + * Build type conversion expression for a single value. + * + * @param array $property Property definition + * @param string $src Source variable or expression + * @param bool $srcNonNull Whether $src is guaranteed non-null + * @return string + */ + private function convertValue(array $property, string $src, bool $srcNonNull = true): string + { + // Sub_schema (nested objects) + if (!empty($property['sub_schema'])) { + $subSchema = $this->toPascalCase($property['sub_schema']); + if ($property['type'] === 'array') { + return "{$src}.ToEnumerable().Select(it => {$subSchema}.From(map: (Dictionary)it)).ToList()"; + } + return "{$subSchema}.From(map: (Dictionary){$src})"; + } + + // Enum + if (!empty($property['enum'])) { + $enumClass = $this->toPascalCase($property['enumName'] ?? $property['name']); + return "new {$enumClass}({$src}.ToString())"; + } + + // Arrays + if ($property['type'] === 'array') { + $itemsType = $property['items']['type'] ?? 'object'; + $selectExpression = match ($itemsType) { + 'string' => 'x.ToString()', + 'integer' => 'Convert.ToInt64(x)', + 'number' => 'Convert.ToDouble(x)', + 'boolean' => '(bool)x', + default => 'x' + }; + return "{$src}.ToEnumerable().Select(x => {$selectExpression}).ToList()"; + } + + // Integer/Number + if ($property['type'] === 'integer' || $property['type'] === 'number') { + $convertMethod = $property['type'] === 'integer' ? 'Int64' : 'Double'; + return "Convert.To{$convertMethod}({$src})"; + } + + // Boolean + if ($property['type'] === 'boolean') { + return $srcNonNull ? "(bool){$src}" : "(bool?){$src}"; + } + + // String (default) + return $srcNonNull ? "{$src}.ToString()" : "{$src}?.ToString()"; + } + + /** + * Generate ToMap() value expression for a property. + * + * @param array $property + * @param string $definitionName + * @return string + */ + protected function getToMapExpression(array $property, string $definitionName): string + { + $propName = $this->getResolvedPropertyName($property, $definitionName); + $required = $property['required'] ?? true; + $nullOp = $required ? '' : '?'; + + if (!empty($property['sub_schema'])) { + if ($property['type'] === 'array') { + return "{$propName}{$nullOp}.Select(it => it.ToMap()).ToList()"; + } + return "{$propName}{$nullOp}.ToMap()"; + } + + if (!empty($property['enum'])) { + return "{$propName}{$nullOp}.Value"; + } + + return $propName; + } + /** * Format a PHP array as a C# anonymous object */ diff --git a/templates/dotnet/Package/Client.cs.twig b/templates/dotnet/Package/Client.cs.twig index 53bd7ba62c..d4bd56af0f 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; diff --git a/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig b/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig index 563f92992a..ce772c93df 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}"); } } diff --git a/templates/dotnet/Package/Exception.cs.twig b/templates/dotnet/Package/Exception.cs.twig index e78d78c2cc..857bd0eeb2 100644 --- a/templates/dotnet/Package/Exception.cs.twig +++ b/templates/dotnet/Package/Exception.cs.twig @@ -5,8 +5,8 @@ namespace {{spec.title | caseUcfirst}} public class {{spec.title | caseUcfirst}}Exception : Exception { public int? Code { get; set; } - public string? Type { get; set; } = null; - public string? Response { get; set; } = null; + public string? Type { get; set; } + public string? Response { get; set; } public {{spec.title | caseUcfirst}}Exception( string? message = null, @@ -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 0ac19f7ce7..55635f7bc1 100644 --- a/templates/dotnet/Package/Extensions/Extensions.cs.twig +++ b/templates/dotnet/Package/Extensions/Extensions.cs.twig @@ -13,15 +13,14 @@ namespace {{ spec.title | caseUcfirst }}.Extensions return JsonSerializer.Serialize(dict, Client.SerializerOptions); } - public static List ConvertToList(this object value) + public static IEnumerable ToEnumerable(this object value) { return value switch { - JsonElement jsonElement => jsonElement.Deserialize>() ?? throw new InvalidCastException($"Cannot deserialize {jsonElement} to List<{typeof(T)}>."), - object[] objArray => objArray.Cast().ToList(), - List list => list, - IEnumerable enumerable => enumerable.ToList(), - _ => throw new InvalidCastException($"Cannot convert {value.GetType()} to List<{typeof(T)}>") + object[] array => array, + IEnumerable enumerable => enumerable, + IEnumerable nonGeneric => nonGeneric.Cast(), + _ => throw new InvalidCastException($"Cannot convert {value?.GetType().Name ?? "null"} to IEnumerable") }; } @@ -50,7 +49,7 @@ namespace {{ spec.title | caseUcfirst }}.Extensions return Uri.EscapeUriString(string.Join("&", query)); } - private static IDictionary _mappings = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { + private static readonly IDictionary Mappings = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { #region Mime Types {".323", "text/h323"}, @@ -621,7 +620,7 @@ namespace {{ spec.title | caseUcfirst }}.Extensions { if (extension == null) { - throw new ArgumentNullException("extension"); + throw new ArgumentNullException(nameof(extension)); } if (!extension.StartsWith(".")) @@ -629,7 +628,7 @@ namespace {{ spec.title | caseUcfirst }}.Extensions extension = "." + extension; } - return _mappings.TryGetValue(extension, out var mime) ? mime : "application/octet-stream"; + return Mappings.TryGetValue(extension, out var mime) ? mime : "application/octet-stream"; } public static string GetMimeType(this string path) @@ -637,4 +636,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 241a3adad5..aaf7a66202 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 e3f0bd132d..3b77f1b160 100644 --- a/templates/dotnet/Package/Models/Model.cs.twig +++ b/templates/dotnet/Package/Models/Model.cs.twig @@ -1,28 +1,30 @@ - +{% set DefinitionClass = definition.name | caseUcfirst | overrideIdentifier %} using System; 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 %} using {{ spec.title | caseUcfirst }}.Extensions; namespace {{ spec.title | caseUcfirst }}.Models { - public class {{ definition.name | caseUcfirst | overrideIdentifier }} + public class {{ DefinitionClass }} { {%~ 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 %} public Dictionary Data { get; private set; } {%~ endif %} - public {{ definition.name | caseUcfirst | overrideIdentifier }}( + 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 %} @@ -37,57 +39,20 @@ 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 %} - {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}:{{' '}} - {%- if property.sub_schema %} - {%- if property.type == 'array' -%} - map["{{ property.name }}"].ConvertToList>().Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: 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 }}"]) - {%- 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' -%} - map["{{ property.name }}"].ConvertToList<{{ property | typeName | replace({'List<': '', '>': ''}) }}>() - {%- 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 }}"]) - {%- 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 %} - {%- endif %} - {%~ endif %} - {%~ endif %} - {%~ endif %} - {%- if not loop.last or (loop.last and definition.additionalProperties) %}, - {%~ endif %} + {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}: {{ property | propertyAssignment | raw }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} - {%- if definition.additionalProperties %} + {%~ if definition.additionalProperties %} data: map.TryGetValue("data", out var dataValue) ? (Dictionary)dataValue : map - {%- endif ~%} + {%~ endif %} ); public Dictionary ToMap() => new Dictionary() { {%~ for property in definition.properties %} - { "{{ property.name }}", {% if property.sub_schema %}{% if property.type == 'array' %}{{ property_name(definition, property) | overrideProperty(definition.name) }}.Select(it => it.ToMap()){% else %}{{ property_name(definition, property) | overrideProperty(definition.name) }}.ToMap(){% endif %}{% elseif property.enum %}{{ property_name(definition, property) | overrideProperty(definition.name) }}{% if not property.required %}?{% endif %}.Value{% else %}{{ property_name(definition, property) | overrideProperty(definition.name) }}{% endif %}{{ ' }' }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + { "{{ property.name }}", {{ property | toMapValue(definition.name) | raw }} }{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} {%~ endfor %} {%~ if definition.additionalProperties %} @@ -100,7 +65,7 @@ namespace {{ spec.title | caseUcfirst }}.Models fromJson.Invoke(Data); {%~ endif %} {%~ for property in definition.properties %} - {%~ if property.sub_schema %} + {%~ if property.sub_schema and not definition.additionalProperties %} {%~ for def in spec.definitions %} {%~ if def.name == property.sub_schema and def.additionalProperties and property.type == 'array' %} diff --git a/templates/dotnet/Package/Models/RequestModel.cs.twig b/templates/dotnet/Package/Models/RequestModel.cs.twig index 333320442e..e87c9bc41f 100644 --- a/templates/dotnet/Package/Models/RequestModel.cs.twig +++ b/templates/dotnet/Package/Models/RequestModel.cs.twig @@ -36,7 +36,7 @@ namespace {{ spec.title | caseUcfirst }}.Models return new Dictionary { {%~ for property in requestModel.properties %} - { "{{ property.name }}", {% if property.sub_schema %}{% if property.type == 'array' %}{{ property.name | caseUcfirst | overrideIdentifier }}?.ConvertAll(p => p.ToMap()){% else %}{{ property.name | caseUcfirst | overrideIdentifier }}?.ToMap(){% endif %}{% elseif property.enum %}{{ property.name | caseUcfirst | overrideIdentifier }}{% if not property.required %}?{% endif %}.Value{% else %}{{ property.name | caseUcfirst | overrideIdentifier }}{% endif %} }{% if not loop.last %},{% endif %} + { "{{ property.name }}", {{ property | toMapValue(requestModel.name) | raw }} }{% if not loop.last %},{% endif %} {%~ endfor %} }; diff --git a/templates/dotnet/Package/Models/UploadProgress.cs.twig b/templates/dotnet/Package/Models/UploadProgress.cs.twig index 47c78391ce..ee6fb58ba3 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 c4a7ee35db..7a147e970a 100644 --- a/templates/dotnet/Package/Query.cs.twig +++ b/templates/dotnet/Package/Query.cs.twig @@ -317,4 +317,4 @@ namespace {{ spec.title | caseUcfirst }} return new Query("notTouches", attribute, new List { values }).ToString(); } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Role.cs.twig b/templates/dotnet/Package/Role.cs.twig index b3ecf2610b..3c7b2b33f3 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 +} diff --git a/templates/dotnet/Package/Services/ServiceTemplate.cs.twig b/templates/dotnet/Package/Services/ServiceTemplate.cs.twig index 7252134bef..8a0086f04b 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;