diff --git a/NTypewriter.CodeModel.Functions.Tests/Action/Url/expectedResult.txt b/NTypewriter.CodeModel.Functions.Tests/Action/Url/expectedResult.txt index 6577d61..e9307e7 100644 --- a/NTypewriter.CodeModel.Functions.Tests/Action/Url/expectedResult.txt +++ b/NTypewriter.CodeModel.Functions.Tests/Action/Url/expectedResult.txt @@ -5,3 +5,7 @@ WithRouteAttribute:sd SomeComplexTest:Url/akacja/${par1}/${par2}/${par3}?par4=${par4}&par5=${par5} WithEnumParam:Url?numbers=${numbers} WithOptionalParam:Url?optional=${optional} +WithOptionalObjectParam:Url/object-param?date=${optional?.date}&temperatureC=${optional?.temperatureC}&temperatureF=${optional?.temperatureF}&summary=${optional?.summary} +WithArrayParam:Url/array-param?${array.map(item => `array=${encodeURIComponent(item)}`).join('&')} +WithArrayInObjectParam:Url/array-in-object-param?${optional?.arrayStr.map(item => `arrayStr=${encodeURIComponent(item)}`).join('&')}&${optional?.listStr.map(item => `listStr=${encodeURIComponent(item)}`).join('&')}&${optional?.arrayEnum.map(item => `arrayEnum=${item}`).join('&')}&${optional?.listEnum.map(item => `listEnum=${item}`).join('&')} +WithNullableArrayInObjectParam:Url/nullable-array-in-object-param?${optional?.arrayStr?.map(item => `arrayStr=${encodeURIComponent(item)}`).join('&')}&${optional?.listStr?.map(item => `listStr=${encodeURIComponent(item)}`).join('&')}&${optional?.arrayEnum?.map(item => `arrayEnum=${item}`).join('&')}&${optional?.listEnum?.map(item => `listEnum=${item}`).join('&')} diff --git a/NTypewriter.CodeModel.Functions/ActionFunctions.Url.cs b/NTypewriter.CodeModel.Functions/ActionFunctions.Url.cs index 47066c0..5f30811 100644 --- a/NTypewriter.CodeModel.Functions/ActionFunctions.Url.cs +++ b/NTypewriter.CodeModel.Functions/ActionFunctions.Url.cs @@ -123,22 +123,49 @@ private static string AppendFromQuery(IMethod method, string route) { foreach (var prop in @class.Properties) { + string urlParam = prop.BareName.ToLowerFirst(); + string memberAccessOperator = IsNullable(parameter) ? "?." : "."; + string propertyAccess = string.Concat(parameter.BareName, memberAccessOperator, prop.BareName.ToLowerFirst()); + builder.Append(connector); - if (parameter.Type.Name == "string") + if (prop.Type.IsEnumerable && !prop.Type.IsSimple()) { - builder.Append($"{prop.BareName.ToLowerFirst()}=${{encodeURIComponent({parameter.BareName}.{prop.BareName.ToLowerFirst()})}}"); + string itemValue = GetEnumerableType(prop.Type)?.Name == "string" ? "${encodeURIComponent(item)}" : "${item}"; + builder.Append($"${{{propertyAccess}{(prop.Type.IsNullable ? "?." : ".")}map(item => `{urlParam}={itemValue}`).join('&')}}"); } else { - builder.Append($"{prop.BareName.ToLowerFirst()}=${{{parameter.BareName}.{prop.BareName.ToLowerFirst()}}}"); + string urlValue = parameter.Type.Name == "string" ? $"${{encodeURIComponent({propertyAccess})}}" : $"${{{propertyAccess}}}"; + builder.Append($"{urlParam}={urlValue}"); } connector = "&"; } } + else if (parameter.Type.IsEnumerable) + { + string urlParam = parameter.BareName.ToLowerFirst(); + string memberAccessOperator = IsNullable(parameter) ? "?." : "."; + string itemValue = GetEnumerableType(parameter.Type)?.Name == "string" ? "${encodeURIComponent(item)}" : "${item}"; + + builder.Append(connector).Append($"${{{parameter.BareName}{memberAccessOperator}map(item => `{urlParam}={itemValue}`).join('&')}}"); + connector = "&"; + } } } var postfix = builder.ToString(); return route + postfix; } + + private static IType GetEnumerableType(IType enumerableType) + { + if (!enumerableType.IsEnumerable) + return null; + return enumerableType.ArrayType ?? enumerableType.TypeArguments.FirstOrDefault(); + } + + private static bool IsNullable(IParameter parameter) + { + return parameter.Type.IsNullable || (parameter.HasDefaultValue && parameter.DefaultValue == null); + } } } \ No newline at end of file diff --git a/NTypewriter.CodeModel.Functions/Extensions/ITypeExtensions.cs b/NTypewriter.CodeModel.Functions/Extensions/ITypeExtensions.cs index 0fb8616..04e3237 100644 --- a/NTypewriter.CodeModel.Functions/Extensions/ITypeExtensions.cs +++ b/NTypewriter.CodeModel.Functions/Extensions/ITypeExtensions.cs @@ -10,12 +10,11 @@ internal static class ITypeExtensions // https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-5.0#simple-types public static bool IsSimple(this IType type) { - if (type.IsNullable) + if (type.IsNullable) { - if (type.TypeArguments.Any()) - { - return IsSimple(type.TypeArguments.First()); - } + var nonNullableType = GetUnderlyingNonNullableType(type); + if (nonNullableType != null) + return IsSimple(nonNullableType); } switch (type.FullName) { @@ -31,5 +30,14 @@ public static bool IsSimple(this IType type) return type.IsPrimitive || type.IsEnum; } + + public static IType GetUnderlyingNonNullableType(this IType type) + { + if (!type.IsNullable) + return null; + if (type.IsReferenceType) + return type.OriginalDefinition; + return type.TypeArguments.FirstOrDefault(); + } } } diff --git a/NTypewriter.CodeModel.Roslyn/Type.cs b/NTypewriter.CodeModel.Roslyn/Type.cs index 2ab1d6f..86b621c 100644 --- a/NTypewriter.CodeModel.Roslyn/Type.cs +++ b/NTypewriter.CodeModel.Roslyn/Type.cs @@ -52,6 +52,7 @@ public bool IsPrimitive public IEnumerable Interfaces => InterfaceCollection.Create(symbol.Interfaces); public IEnumerable AllInterfaces => InterfaceCollection.Create(symbol.AllInterfaces); public IType ArrayType => symbol is IArrayTypeSymbol arraySymbol ? NTypewriter.CodeModel.Roslyn.Type.Create(arraySymbol.ElementType) : null; + public IType OriginalDefinition => NTypewriter.CodeModel.Roslyn.Type.Create(symbol.OriginalDefinition); public IEnumerable TypeArguments => TypeCollection.CreateTypeArguments(symbol); public override string Name { diff --git a/NTypewriter.CodeModel/IType.cs b/NTypewriter.CodeModel/IType.cs index 0dac147..a14b321 100644 --- a/NTypewriter.CodeModel/IType.cs +++ b/NTypewriter.CodeModel/IType.cs @@ -89,6 +89,11 @@ public interface IType : ISymbolBase /// IType ArrayType { get; } + /// + /// The original definition of the type, or the type itself if not generic. + /// + IType OriginalDefinition { get; } + /// /// The set of interfaces that this type directly implements. This set does not include interfaces that are base interfaces of directly implemented interfaces. /// diff --git a/Tests.Assets.WebApi/Assets/ArrayDTO.cs b/Tests.Assets.WebApi/Assets/ArrayDTO.cs new file mode 100644 index 0000000..4ef02ba --- /dev/null +++ b/Tests.Assets.WebApi/Assets/ArrayDTO.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Tests.Assets.WebApi.Controllers +{ + public class ArrayDTO + { + public string[] ArrayStr { get; set; } + public List ListStr { get; set; } + public NumbersEnum[] ArrayEnum { get; set; } + public List ListEnum { get; set; } + } +} diff --git a/Tests.Assets.WebApi/Assets/NullableArrayDTO.cs b/Tests.Assets.WebApi/Assets/NullableArrayDTO.cs new file mode 100644 index 0000000..d3d17ba --- /dev/null +++ b/Tests.Assets.WebApi/Assets/NullableArrayDTO.cs @@ -0,0 +1,13 @@ +#nullable enable +using System.Collections.Generic; + +namespace Tests.Assets.WebApi.Controllers +{ + public class NullableArrayDTO + { + public string[]? ArrayStr { get; set; } + public List? ListStr { get; set; } + public NumbersEnum[]? ArrayEnum { get; set; } + public List? ListEnum { get; set; } + } +} diff --git a/Tests.Assets.WebApi/Controllers/UrlController.cs b/Tests.Assets.WebApi/Controllers/UrlController.cs index 6b96a1d..46a211e 100644 --- a/Tests.Assets.WebApi/Controllers/UrlController.cs +++ b/Tests.Assets.WebApi/Controllers/UrlController.cs @@ -68,5 +68,29 @@ public IActionResult WithOptionalParam(int? optional) { return null; } + + [HttpGet("object-param")] + public IActionResult WithOptionalObjectParam([FromQuery] WeatherForecast optional = null) + { + return null; + } + + [HttpGet("array-param")] + public IActionResult WithArrayParam([FromQuery] string[] array) + { + return null; + } + + [HttpGet("array-in-object-param")] + public IActionResult WithArrayInObjectParam([FromQuery] ArrayDTO optional = null) + { + return null; + } + + [HttpGet("nullable-array-in-object-param")] + public IActionResult WithNullableArrayInObjectParam([FromQuery] NullableArrayDTO optional = null) + { + return null; + } } -} \ No newline at end of file +}