Skip to content
80 changes: 78 additions & 2 deletions src/SDK/Language/DotNet.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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<object>){$mapAccess})"
: "({$v} as IEnumerable<object>)?";
return "{$arraySource}.Select(it => {$subSchema}.From(map: (Dictionary<string, object>)it)).ToList()";
} else {
if ($required) {
return "{$subSchema}.From(map: (Dictionary<string, object>){$mapAccess})";
}
return "({$v} as Dictionary<string, object>) 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<object>){$src})"
: "({$src} as IEnumerable<object>)?";

$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}";
}
Comment on lines +567 to +575
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Boolean cast can throw InvalidCastException.

Lines 572 and 574 perform direct casts to bool or bool? without type validation. If the source value is not a boolean (e.g., numeric, string, or null for required), this will throw InvalidCastException.

Use safe conversion:

                 if ($property['type'] === 'boolean') {
-                    $typeName = $this->getTypeName($property);
                     
                     if ($required) {
-                        return "({$typeName}){$mapAccess}";
+                        return "Convert.ToBoolean({$mapAccess})";
                     }
-                    return "({$typeName}?){$v}";
+                    return "{$v} == null ? null : Convert.ToBoolean({$v})";
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle boolean
if ($property['type'] === 'boolean') {
$typeName = $this->getTypeName($property);
if ($required) {
return "({$typeName}){$mapAccess}";
}
return "({$typeName}?){$v}";
}
// Handle boolean
if ($property['type'] === 'boolean') {
if ($required) {
return "Convert.ToBoolean({$mapAccess})";
}
return "{$v} == null ? null : Convert.ToBoolean({$v})";
}
🤖 Prompt for AI Agents
In src/SDK/Language/DotNet.php around lines 567 to 575, the code generates
direct C# casts to bool/nullable bool which can throw InvalidCastException for
non-boolean sources; replace the direct cast generation with code that first
validates/typeswitches the source and performs a safe conversion—for example
generate logic that checks for null (return null for nullable), checks if value
is already a bool, attempts bool.TryParse for string inputs, and falls back to
Convert.ToBoolean for numeric types (or throws a controlled error) so that the
emitted C# uses TryParse/Convert.ToBoolean and explicit null handling instead of
direct (bool) or (bool?) casts.


// Handle string type
if ($required) {
return "{$mapAccess}.ToString()";
}
return "{$v}?.ToString()";
}, ['is_safe' => ['html']]),
];
}

Expand Down
1 change: 1 addition & 0 deletions templates/dotnet/Package/Client.cs.twig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,60 @@ namespace {{ spec.title | caseUcfirst }}.Converters
{
public class ObjectToInferredTypesConverter : JsonConverter<object>
{
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<string, object?>();
foreach (var property in element.EnumerateObject())
{
return l;
dictionary[property.Name] = ConvertElement(property.Value);
}
return dictionary;

case JsonValueKind.Array:
var list = new List<object?>();
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<Dictionary<string, object>>(ref reader, options)!;
case JsonTokenType.StartArray:
return JsonSerializer.Deserialize<object[]>(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}");
}
}

Expand Down
2 changes: 1 addition & 1 deletion templates/dotnet/Package/Exception.cs.twig
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
}
}
}

2 changes: 1 addition & 1 deletion templates/dotnet/Package/Extensions/Extensions.cs.twig
Original file line number Diff line number Diff line change
Expand Up @@ -624,4 +624,4 @@ namespace {{ spec.title | caseUcfirst }}.Extensions
return GetMimeTypeFromExtension(System.IO.Path.GetExtension(path));
}
}
}
}
4 changes: 2 additions & 2 deletions templates/dotnet/Package/Models/InputFile.cs.twig
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.IO;
using Appwrite.Extensions;
using {{ spec.title | caseUcfirst }}.Extensions;

namespace {{ spec.title | caseUcfirst }}.Models
{
Expand Down Expand Up @@ -38,4 +38,4 @@ namespace {{ spec.title | caseUcfirst }}.Models
SourceType = "bytes"
};
}
}
}
53 changes: 11 additions & 42 deletions templates/dotnet/Package/Models/Model.cs.twig
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

{% set DefinitionClass = definition.name | caseUcfirst | overrideIdentifier %}
using System;
using System.Linq;
using System.Collections.Generic;
Expand All @@ -8,20 +8,20 @@ using {{ spec.title | caseUcfirst }}.Enums;

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<string, object> 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 %}
Expand All @@ -36,45 +36,14 @@ namespace {{ spec.title | caseUcfirst }}.Models
{%~ endif %}
}

public static {{ definition.name | caseUcfirst | overrideIdentifier }} From(Dictionary<string, object> map) => new {{ definition.name | caseUcfirst | overrideIdentifier }}(
public static {{ DefinitionClass }} From(Dictionary<string, object> map) => new {{ DefinitionClass }}(
{%~ for property in definition.properties %}
{%~ set v = 'v' ~ loop.index0 %}
{%~ set mapAccess = 'map["' ~ property.name ~ '"]' %}
{{ 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<List<Dictionary<string, object>>>()!.Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: it)).ToList() : ((IEnumerable<Dictionary<string, object>>)map["{{ property.name }}"]).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<string, object>>()! : (Dictionary<string, object>)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 }}"] is JsonElement jsonArrayProp{{ loop.index }} ? jsonArrayProp{{ loop.index }}.Deserialize<{{ property | typeName }}>()! : ({{ property | typeName }})map["{{ property.name }}"]
{%- 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 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 %}
Expand Down
2 changes: 1 addition & 1 deletion templates/dotnet/Package/Models/UploadProgress.cs.twig
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ namespace {{ spec.title | caseUcfirst }}
ChunksUploaded = chunksUploaded;
}
}
}
}
2 changes: 1 addition & 1 deletion templates/dotnet/Package/Query.cs.twig
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,4 @@ namespace {{ spec.title | caseUcfirst }}
return new Query("notTouches", attribute, new List<object> { values }).ToString();
}
}
}
}
4 changes: 2 additions & 2 deletions templates/dotnet/Package/Role.cs.twig
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Appwrite
namespace {{ spec.title | caseUcfirst }}
{
/// <summary>
/// Helper class to generate role strings for Permission.
Expand Down Expand Up @@ -89,4 +89,4 @@ namespace Appwrite
return $"label:{name}";
}
}
}
}
1 change: 0 additions & 1 deletion templates/dotnet/Package/Services/ServiceTemplate.cs.twig
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{% import 'dotnet/base/utils.twig' as utils %}

using System;
using System.Collections.Generic;
using System.Linq;
Expand Down