Skip to content

Commit

Permalink
[C#] Gestion des types (non-)nullables dans le générateur
Browse files Browse the repository at this point in the history
  • Loading branch information
JabX committed Dec 3, 2023
1 parent 5aed200 commit c96d889
Show file tree
Hide file tree
Showing 18 changed files with 372 additions and 29 deletions.
12 changes: 6 additions & 6 deletions TopModel.Generator.Csharp/CSharpApiClientGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ protected override void HandleFile(string filePath, string fileName, string tag,
using var fw = new CSharpWriter(filePath, _logger);

var hasBody = endpoints.Any(e => e.GetJsonBodyParam() != null);
var hasReturn = endpoints.Any(e => e.Returns != null && !new[] { "string", "byte[]" }.Contains(Config.GetType(e.Returns)));
var hasReturn = endpoints.Any(e => e.Returns != null && !new[] { "string", "byte[]" }.Contains(Config.GetType(e.Returns)?.TrimEnd('?')));
var hasJson = hasReturn || hasBody;

var usings = new List<string>();
Expand Down Expand Up @@ -157,7 +157,7 @@ protected override void HandleFile(string filePath, string fileName, string tag,

fw.Write(" public async Task");

var returnType = endpoint.Returns != null ? Config.GetType(endpoint.Returns) : null;
var returnType = endpoint.Returns != null ? Config.GetType(endpoint.Returns, nonNullable: endpoint.Returns.Required) : null;
if (returnType?.StartsWith("IAsyncEnumerable") ?? false)
{
returnType = returnType.Replace("IAsyncEnumerable", "IEnumerable");
Expand All @@ -172,7 +172,7 @@ protected override void HandleFile(string filePath, string fileName, string tag,

foreach (var param in endpoint.Params)
{
fw.Write($"{Config.GetType(param, nonNullable: param.IsRouteParam() || param.IsQueryParam() && Config.GetValue(param, Classes) != "null")} {param.GetParamName().Verbatim()}");
fw.Write($"{Config.GetType(param, nonNullable: param.IsJsonBodyParam() || param.IsRouteParam() || param.IsQueryParam() && Config.GetValue(param, Classes) != "null")} {param.GetParamName().Verbatim()}");

if (param.IsQueryParam())
{
Expand All @@ -199,10 +199,10 @@ protected override void HandleFile(string filePath, string fileName, string tag,

foreach (var qp in endpoint.GetQueryParams().Where(qp => !Config.GetType(qp).Contains("[]")))
{
var toString = Config.GetType(qp) switch
var toString = Config.GetType(qp)?.TrimEnd('?') switch
{
"string" => string.Empty,
"Guid" or "Guid?" => "?.ToString()",
"Guid" => "?.ToString()",
_ => $"?.ToString(CultureInfo.InvariantCulture)"
};

Expand Down Expand Up @@ -240,7 +240,7 @@ protected override void HandleFile(string filePath, string fileName, string tag,

if (returnType != null)
{
if (returnType == "string")
if (returnType.TrimEnd('?') == "string")
{
fw.WriteLine(2, $"return (await res.Content.ReadAsStringAsync()).Trim('\"');");
}
Expand Down
2 changes: 1 addition & 1 deletion TopModel.Generator.Csharp/CSharpApiServerGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ private string GetParam(IProperty param)
sb.Append("[FromBody] ");
}

sb.Append($@"{Config.GetType(param, nonNullable: param.IsRouteParam() || param.IsQueryParam() && !param.Endpoint.IsMultipart && Config.GetValue(param, Classes) != "null")} {param.GetParamName().Verbatim()}");
sb.Append($@"{Config.GetType(param, nonNullable: param.IsJsonBodyParam() || param.IsRouteParam() || param.IsQueryParam() && !param.Endpoint.IsMultipart && Config.GetValue(param, Classes) != "null")} {param.GetParamName().Verbatim()}");

if (param.IsQueryParam() && !param.Endpoint.IsMultipart)
{
Expand Down
10 changes: 5 additions & 5 deletions TopModel.Generator.Csharp/CSharpClassGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ private void GenerateConstProperties(CSharpWriter w, Class item)

foreach (var uk in item.UniqueKeys.Where(uk =>
uk.Count == 1
&& Config.GetType(uk.Single()) == "string"
&& Config.GetType(uk.Single())?.TrimEnd('?') == "string"
&& refValue.Value.ContainsKey(uk.Single())))
{
var prop = uk.Single();
Expand Down Expand Up @@ -366,7 +366,7 @@ private void GenerateProperty(CSharpWriter w, IProperty property, HashSet<string
{
w.WriteSummary(1, property.Comment);

var type = Config.GetType(property);
var type = Config.GetType(property, nonNullable: property is CompositionProperty { Required: true });

if (!property.Class.Abstract)
{
Expand Down Expand Up @@ -409,7 +409,7 @@ private void GenerateProperty(CSharpWriter w, IProperty property, HashSet<string
w.WriteAttribute(1, "Domain", $@"Domains.{fp.Domain.CSharpName}");
}

if (type == "string" && fp.Domain.Length != null)
if (type?.TrimEnd('?') == "string" && fp.Domain.Length != null)
{
w.WriteAttribute(1, "StringLength", $"{fp.Domain.Length}");
}
Expand Down Expand Up @@ -466,7 +466,7 @@ private void GenerateUsings(CSharpWriter w, Class item, string tag)
usings.Add("System.ComponentModel");
}

if (item.Properties.OfType<IFieldProperty>().Any(p => p.Required || p.PrimaryKey || Config.GetType(p) == "string" && p.Domain.Length != null))
if (item.Properties.OfType<IFieldProperty>().Any(p => p.Required || p.PrimaryKey || Config.GetType(p)?.TrimEnd('?') == "string" && p.Domain.Length != null))
{
usings.Add("System.ComponentModel.DataAnnotations");
}
Expand Down Expand Up @@ -546,7 +546,7 @@ private string GetNamespace(Class classe, string tag)
{
if (property is CompositionProperty cp)
{
var type = Config.GetType(property);
var type = Config.GetType(property, nonNullable: true);
var genericType = type.Split('<').First();

if (cp.Domain == null)
Expand Down
27 changes: 21 additions & 6 deletions TopModel.Generator.Csharp/CsharpConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ namespace TopModel.Generator.Csharp;
/// </summary>
public class CsharpConfig : GeneratorConfigBase
{
private readonly string[] _builtInNonNullableTypes = ["bool", "short", "ushort", "int", "uint", "long", "ulong", "double", "float", "decimal", "Guid", "DateTime", "DateOnly", "TimeOnly", "TimeSpan"];

private string? _referencesModelPath;

/// <summary>
Expand Down Expand Up @@ -107,6 +109,23 @@ public class CsharpConfig : GeneratorConfigBase
/// </summary>
public string DomainNamespace { get; set; } = "{app}.Common";

[YamlMember(Alias = "nonNullableTypes")]
public object? NonNullableTypesParam { get; set; }

/// <summary>
/// Types C# que le générateur doit considérer comme étant non nullables (nécessitant donc l'ajout d'un '?' pour l'être).
/// La plupart des types standard comme 'int', 'bool' ou 'DateTime' sont déjà connus du générateur.
/// Ce paramètre permet soit de spécifier une liste de types non-nullables supplémentaires,
/// soit 'true' pour considérer que tous les types sont non-nullables (pour correspondre à &lt;nullable&gt;enable&lt;/nullable&gt;).
/// </summary>
public IList<string> NonNullableTypes => NonNullableTypesParam switch
{
IEnumerable<object> list => _builtInNonNullableTypes.Concat(list.OfType<string>()).Distinct().ToList(),
_ => _builtInNonNullableTypes
};

public bool IsNullableEnabled => NonNullableTypesParam?.Equals(true) ?? false;

/// <summary>
/// Retire les attributs de colonnes sur les alias.
/// </summary>
Expand Down Expand Up @@ -470,7 +489,7 @@ public string GetReturnTypeName(IProperty? prop)
return NoAsyncControllers ? "void" : "async Task";
}

var typeName = GetType(prop, nonNullable: (prop as IFieldProperty)?.Required ?? true);
var typeName = GetType(prop, nonNullable: prop.Required);
return typeName.StartsWith("IAsyncEnumerable") || NoAsyncControllers
? typeName
: $"async Task<{typeName}>";
Expand All @@ -480,14 +499,10 @@ public string GetType(IProperty prop, IEnumerable<Class>? availableClasses = nul
{
var type = base.GetType(prop, availableClasses, useClassForAssociation);

if (!nonNullable && prop is IFieldProperty f && GetEnumType(f, f is RegularProperty) == type)
if (!nonNullable && (IsNullableEnabled || NonNullableTypes.Contains(type) || prop is IFieldProperty f && GetEnumType(f, f is RegularProperty) == type))
{
type += "?";
}
else if (nonNullable && type.EndsWith("?"))
{
type = type[0..^1];
}

return type;
}
Expand Down
8 changes: 4 additions & 4 deletions TopModel.Generator.Csharp/MapperGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ protected override void HandleFile(string fileName, string tag, IList<(Class Cla
}

w.WriteLine($"({string.Join(", ", mapper.Params.Select(mp => mp.Match(
c => $"{(c.Class.Abstract ? "I" : string.Empty)}{c.Class.NamePascal} {c.Name}{(!c.Required ? " = null" : string.Empty)}",
p => $"{Config.GetType(p.Property)} {p.Property.NameCamel}{(!mp.GetRequired() ? $" = {Config.GetValue(p.Property, Classes)}" : string.Empty)}")))})");
c => $"{(c.Class.Abstract ? "I" : string.Empty)}{c.Class.NamePascal}{(!c.Required && Config.IsNullableEnabled ? "?" : string.Empty)} {c.Name}{(!c.Required ? " = null" : string.Empty)}",
p => $"{Config.GetType(p.Property, nonNullable: mp.GetRequired() || Config.GetValue(p.Property, Classes) != "null")} {p.Property.NameCamel}{(!mp.GetRequired() ? $" = {Config.GetValue(p.Property, Classes)}" : string.Empty)}")))})");

if (classe.Abstract)
{
Expand All @@ -113,7 +113,7 @@ protected override void HandleFile(string fileName, string tag, IList<(Class Cla

w.WriteLine(1, "{");

foreach (var param in mapper.Params.Where(p => p.GetRequired()))
foreach (var param in mapper.Params.Where(p => p.GetRequired() && (p.IsT0 || !Config.NonNullableTypes.Contains(Config.GetImplementation(p.AsT1.Property.Domain)?.Type ?? string.Empty))))
{
w.WriteLine(2, $"ArgumentNullException.ThrowIfNull({param.GetNameCamel()});");
}
Expand Down Expand Up @@ -237,7 +237,7 @@ protected override void HandleFile(string fileName, string tag, IList<(Class Cla
}
else
{
w.WriteLine(1, $"public static {mapper.Class.NamePascal} {mapper.Name}(this {(classe.Abstract ? "I" : string.Empty)}{classe.NamePascal} source, {mapper.Class.NamePascal} dest = null)");
w.WriteLine(1, $"public static {mapper.Class.NamePascal} {mapper.Name}(this {(classe.Abstract ? "I" : string.Empty)}{classe.NamePascal} source, {mapper.Class.NamePascal}{(Config.IsNullableEnabled ? "?" : string.Empty)} dest = null)");
}

w.WriteLine(1, "{");
Expand Down
16 changes: 16 additions & 0 deletions TopModel.Generator.Csharp/csharp.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,22 @@
"type": "string",
"description": "Namespace de l'enum de domaine pour Kinetix. Par défaut : '{app}.Common'."
},
"nonNullableTypes": {
"oneOf": [
{
"type": "boolean",
"description": "Types C# que le générateur doit considérer comme étant non nullables (nécessitant donc l'ajout d'un '?' pour l'être).\r\nLa plupart des types standard comme 'int', 'bool' ou 'DateTime' sont déjà connus du générateur.\r\nCe paramètre permet soit de spécifier une liste de types non-nullables supplémentaires, soit 'true' pour considérer que tous les types sont non-nullables (pour correspondre à <nullable>enable</nullable>).",
"enum": [ true ]
},
{
"type": "array",
"description": "Types C# que le générateur doit considérer comme étant non nullables (nécessitant donc l'ajout d'un '?' pour l'être).\r\nLa plupart des types standard comme 'int', 'bool' ou 'DateTime' sont déjà connus du générateur.\r\nCe paramètre permet soit de spécifier une liste de types non-nullables supplémentaires, soit 'true' pour considérer que tous les types sont non-nullables (pour correspondre à <nullable>enable</nullable>).",
"items": {
"type": "string"
}
}
]
},
"noColumnOnAlias": {
"type": "boolean",
"description": "Retire les attributs de colonnes sur les alias."
Expand Down
12 changes: 12 additions & 0 deletions docs/generator/csharp.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Le générateur C# peut générer les fichiers suivants :

Le code généré n'a aucune dépendance externe à part EF Core et Kinetix, et uniquement s'ils sont explicitement demandés dans la configuration.

### Types C#

Tous les types C# écrits par le générateur doivent être nullables (sauf si le type est utilisé pour un paramètre ou un type de retour obligatoire d'endpoint ou de mapper). C'est le générateur qui va s'occuper d'ajouter un `?` derrière le type si nécessaire, donc en particulier tous les types définis dans les domaines ne doivent pas le renseigner (ce qui n'était pas le cas pour avant la 1.41). Le générateur connaît la plupart des types non-nullables courants (comme `int`, `bool`, `DateTime`), mais il est possible de lui en spécifier d'autre via la propriété `nonNullableTypes`. Enfin, si vous souhaitez utiliser `nullable: enable` en C# pour rendre tous les types non-nullables par défaut, vous pouvez renseigner `nonNullableTypes: true` pour que le code généré soit compatible.

### Génération des classes

Le générateur C# fait peu de différences à la génération entre une classe persistée et non persistée. Seule l'annotation `[Table]` est ajoutée en plus sur une classe persistée, et les annotations `[Column]` sont conservées à moins de les désactiver explicitement sur les alias via le paramètre de config `noColumnOnAlias`.
Expand Down Expand Up @@ -234,6 +238,14 @@ _(en preview, documentation à venir)_

_Valeur par défaut_: `{app}.Common`

- `nonNullableTypes`

Types C# que le générateur doit considérer comme étant non nullables (nécessitant donc l'ajout d'un `?` pour l'être).

La plupart des types standard comme `int`, `bool` ou `DateTime` sont déjà connus du générateur.

Ce paramètre permet soit de spécifier une liste de types non-nullables supplémentaires, soit 'true' pour considérer que tous les types sont non-nullables (pour correspondre à &lt;nullable&gt;enable&lt;/nullable&gt;).

- `noColumnOnAlias`

Retire les attributs de colonnes sur les alias (ça ne plaît pas à EF Core mais ça peut être utile pour d'autres ORMs pour mapper directement les colonnes)
Expand Down
30 changes: 30 additions & 0 deletions samples/generators/angular/topmodel.config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,24 @@
"type": "string",
"description": "Namespace de l'enum de domaine pour Kinetix. Par défaut : '{app}.Common'."
},
"nonNullableTypes": {
"oneOf": [
{
"type": "boolean",
"description": "Types C# que le générateur doit considérer comme étant non nullables (nécessitant donc l'ajout d'un '?' pour l'être).\r\nLa plupart des types standard comme 'int', 'bool' ou 'DateTime' sont déjà connus du générateur.\r\nCe paramètre permet soit de spécifier une liste de types non-nullables supplémentaires, soit 'true' pour considérer que tous les types sont non-nullables (pour correspondre à <nullable>enable</nullable>).",
"enum": [
true
]
},
{
"type": "array",
"description": "Types C# que le générateur doit considérer comme étant non nullables (nécessitant donc l'ajout d'un '?' pour l'être).\r\nLa plupart des types standard comme 'int', 'bool' ou 'DateTime' sont déjà connus du générateur.\r\nCe paramètre permet soit de spécifier une liste de types non-nullables supplémentaires, soit 'true' pour considérer que tous les types sont non-nullables (pour correspondre à <nullable>enable</nullable>).",
"items": {
"type": "string"
}
}
]
},
"noColumnOnAlias": {
"type": "boolean",
"description": "Retire les attributs de colonnes sur les alias."
Expand All @@ -276,6 +294,14 @@
"type": "string"
}
},
"mapperLocationPriority": {
"type": "string",
"description": "Détermine le type de classe prioritaire pour déterminer la localisation des mappers générés (`persistent` ou `non-persistent`). Par défaut : 'persistent'",
"enum": [
"persistent",
"non-persistent"
]
},
"enumsForStaticReferences": {
"type": "boolean",
"description": "Utilise des enums au lieu de strings pour les PKs de listes de référence statiques."
Expand All @@ -295,6 +321,10 @@
false,
"dtos-only"
]
},
"usePrimaryConstructors": {
"type": "boolean",
"description": "Utilise les constructeurs principaux pour la génération des classes avec dépendances (clients d'API, accesseurs de références)."
}
}
}
Expand Down
30 changes: 30 additions & 0 deletions samples/generators/csharp/topmodel.config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,24 @@
"type": "string",
"description": "Namespace de l'enum de domaine pour Kinetix. Par défaut : '{app}.Common'."
},
"nonNullableTypes": {
"oneOf": [
{
"type": "boolean",
"description": "Types C# que le générateur doit considérer comme étant non nullables (nécessitant donc l'ajout d'un '?' pour l'être).\r\nLa plupart des types standard comme 'int', 'bool' ou 'DateTime' sont déjà connus du générateur.\r\nCe paramètre permet soit de spécifier une liste de types non-nullables supplémentaires, soit 'true' pour considérer que tous les types sont non-nullables (pour correspondre à <nullable>enable</nullable>).",
"enum": [
true
]
},
{
"type": "array",
"description": "Types C# que le générateur doit considérer comme étant non nullables (nécessitant donc l'ajout d'un '?' pour l'être).\r\nLa plupart des types standard comme 'int', 'bool' ou 'DateTime' sont déjà connus du générateur.\r\nCe paramètre permet soit de spécifier une liste de types non-nullables supplémentaires, soit 'true' pour considérer que tous les types sont non-nullables (pour correspondre à <nullable>enable</nullable>).",
"items": {
"type": "string"
}
}
]
},
"noColumnOnAlias": {
"type": "boolean",
"description": "Retire les attributs de colonnes sur les alias."
Expand All @@ -276,6 +294,14 @@
"type": "string"
}
},
"mapperLocationPriority": {
"type": "string",
"description": "Détermine le type de classe prioritaire pour déterminer la localisation des mappers générés (`persistent` ou `non-persistent`). Par défaut : 'persistent'",
"enum": [
"persistent",
"non-persistent"
]
},
"enumsForStaticReferences": {
"type": "boolean",
"description": "Utilise des enums au lieu de strings pour les PKs de listes de référence statiques."
Expand All @@ -295,6 +321,10 @@
false,
"dtos-only"
]
},
"usePrimaryConstructors": {
"type": "boolean",
"description": "Utilise les constructeurs principaux pour la génération des classes avec dépendances (clients d'API, accesseurs de références)."
}
}
}
Expand Down
Loading

0 comments on commit c96d889

Please sign in to comment.