diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs index ab26313736..5e2fa5d405 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace Volo.Abp.Reflection; @@ -229,4 +231,47 @@ public static class ReflectionHelper return publicConstants.ToArray(); } + + /// + /// Checks whether the property is nullable, including nullable reference types (NRT). + /// + /// Property info to check + public static bool IsNullable(PropertyInfo propertyInfo) + { + if (TypeHelper.IsNullable(propertyInfo.PropertyType)) + { + return true; + } + +#if NET6_0_OR_GREATER + var nullabilityInfoContext = new NullabilityInfoContext(); + var nullabilityInfo = nullabilityInfoContext.Create(propertyInfo); + return nullabilityInfo.ReadState == NullabilityState.Nullable; +#else + var attr = propertyInfo.GetCustomAttributes().FirstOrDefault(a => a.GetType().FullName == "System.Runtime.CompilerServices.NullableAttribute"); + if (attr != null) + { + var getter = NullableGetterCache.GetOrAdd(attr.GetType(), CreateNullableAccessor); + return getter(attr)?[0] == 2; + } + return false; +#endif + } + + private static readonly ConcurrentDictionary> NullableGetterCache = new (); + + private static Func CreateNullableAccessor(Type attrType) + { + var param = Expression.Parameter(typeof(object), "attr"); + var casted = Expression.Convert(param, attrType); + + var flagsField = attrType.GetField("NullableFlags"); + if (flagsField == null) + { + return _ => null; + } + + var access = Expression.Field(casted, flagsField); + return Expression.Lambda>(access, param).Compile(); + } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs index 0118fd9874..e423e2087f 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs @@ -86,7 +86,7 @@ public static class TypeHelper { return default; } - + if (IsPrimitiveExtended(typeof(TProperty), includeEnums: true)) { var conversionType = typeof(TProperty); diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs index 55891d8cd6..ed604793b0 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; using Volo.Abp.Http.ProxyScripting.Configuration; +using Volo.Abp.Reflection; namespace Volo.Abp.Http.Modeling; @@ -29,6 +30,8 @@ public class PropertyApiDescriptionModel public string? Regex { get; set; } + public bool IsNullable { get; set; } + public static PropertyApiDescriptionModel Create(PropertyInfo propertyInfo) { var customAttributes = propertyInfo.GetCustomAttributes(true); @@ -39,6 +42,7 @@ public class PropertyApiDescriptionModel Type = ApiTypeNameHelper.GetTypeName(propertyInfo.PropertyType), TypeSimple = ApiTypeNameHelper.GetSimpleTypeName(propertyInfo.PropertyType), IsRequired = customAttributes.OfType().Any() || propertyInfo.GetCustomAttributesData().Any(attr => attr.AttributeType.Name == "RequiredMemberAttribute"), + IsNullable = ReflectionHelper.IsNullable(propertyInfo), Minimum = customAttributes.OfType().Select(x => x.Minimum).FirstOrDefault()?.ToString(), Maximum = customAttributes.OfType().Select(x => x.Maximum).FirstOrDefault()?.ToString(), MinLength = customAttributes.OfType().FirstOrDefault()?.Length ?? customAttributes.OfType().FirstOrDefault()?.MinimumLength, diff --git a/framework/test/Volo.Abp.Core.Tests/Volo.Abp.Core.Tests.csproj b/framework/test/Volo.Abp.Core.Tests/Volo.Abp.Core.Tests.csproj index 941d5cf1da..4f91a9f854 100644 --- a/framework/test/Volo.Abp.Core.Tests/Volo.Abp.Core.Tests.csproj +++ b/framework/test/Volo.Abp.Core.Tests/Volo.Abp.Core.Tests.csproj @@ -5,6 +5,7 @@ net10.0 + enable diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/ReflectionHelper_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/ReflectionHelper_Tests.cs index 1f761d3438..3b2d0afab2 100644 --- a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/ReflectionHelper_Tests.cs +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/ReflectionHelper_Tests.cs @@ -84,8 +84,65 @@ public class ReflectionHelper_Tests constants.ShouldNotBeEmpty(); constants.Except(IdentityPermissions.GetAll()).Count().ShouldBe(0); } + + [Fact] + public void IsNullable_Test() + { + var prop1 = typeof(TestClass).GetProperty(nameof(TestClass.Prop1))!; + ReflectionHelper.IsNullable(prop1).ShouldBeFalse(); + + var prop2 = typeof(TestClass).GetProperty(nameof(TestClass.Prop2))!; + ReflectionHelper.IsNullable(prop2).ShouldBeTrue(); + + var prop3 = typeof(TestClass).GetProperty(nameof(TestClass.Prop3))!; + ReflectionHelper.IsNullable(prop3).ShouldBeFalse(); + + var prop4 = typeof(TestClass).GetProperty(nameof(TestClass.Prop4))!; + ReflectionHelper.IsNullable(prop4).ShouldBeTrue(); + + var prop5 = typeof(TestClass).GetProperty(nameof(TestClass.Prop5))!; + ReflectionHelper.IsNullable(prop5).ShouldBeFalse(); + + var prop6 = typeof(TestClass).GetProperty(nameof(TestClass.Prop6))!; + ReflectionHelper.IsNullable(prop6).ShouldBeTrue(); + + var prop7 = typeof(TestClass).GetProperty(nameof(TestClass.Prop7))!; + ReflectionHelper.IsNullable(prop7).ShouldBeFalse(); + + var prop8 = typeof(TestClass).GetProperty(nameof(TestClass.Prop8))!; + ReflectionHelper.IsNullable(prop8).ShouldBeTrue(); + + var prop9 = typeof(TestClass).GetProperty(nameof(TestClass.Prop9))!; + ReflectionHelper.IsNullable(prop9).ShouldBeFalse(); + + var prop10 = typeof(TestClass).GetProperty(nameof(TestClass.Prop10))!; + ReflectionHelper.IsNullable(prop10).ShouldBeTrue(); + + var prop11 = typeof(TestClass).GetProperty(nameof(TestClass.Prop11))!; + ReflectionHelper.IsNullable(prop11).ShouldBeFalse(); + + var prop12 = typeof(TestClass).GetProperty(nameof(TestClass.Prop12))!; + ReflectionHelper.IsNullable(prop12).ShouldBeTrue(); + } } +public class TestClass +{ + public string Prop1 { get; set; } = null!; + public string? Prop2 { get; set; } = null!; + public required string Prop3 { get; set; } + public required string? Prop4 { get; set; } + + public int Prop5 { get; set; } + public int? Prop6 { get; set; } + public required int Prop7 { get; set; } + public required int? Prop8 { get; set; } + + public int[] Prop9 { get; set; } = null!; + public int[]? Prop10 { get; set; } + public required int[] Prop11 { get; set; } + public required int[]? Prop12 { get; set; } +} public class BaseRole { diff --git a/npm/ng-packs/packages/schematics/src/models/api-definition.ts b/npm/ng-packs/packages/schematics/src/models/api-definition.ts index 513201f0a9..70dca68dd5 100644 --- a/npm/ng-packs/packages/schematics/src/models/api-definition.ts +++ b/npm/ng-packs/packages/schematics/src/models/api-definition.ts @@ -20,6 +20,7 @@ export interface PropertyDef { type: string; typeSimple: string; isRequired: boolean; + isNullable: boolean; } export interface Module { diff --git a/npm/ng-packs/packages/schematics/src/utils/model.ts b/npm/ng-packs/packages/schematics/src/utils/model.ts index 1ea9130ef2..4e5431da60 100644 --- a/npm/ng-packs/packages/schematics/src/utils/model.ts +++ b/npm/ng-packs/packages/schematics/src/utils/model.ts @@ -153,6 +153,10 @@ export function createImportRefToInterfaceReducerCreator(params: ModelGeneratorP type = simplifyType(prop.type); } + if (prop.isNullable) { + type = `${type} | null`; + } + const refs = parseType(prop.type).reduce( (acc: string[], r) => acc.concat(parseGenerics(r).toGenerics()), [], @@ -186,10 +190,7 @@ export function createRefToImportReducerCreator(params: ModelGeneratorParams) { } function isOptionalProperty(prop: PropertyDef) { - return ( - prop.typeSimple.endsWith('?') || - ((prop.typeSimple === 'string' || prop.typeSimple.includes('enum')) && !prop.isRequired) - ); + return !prop.isRequired; } export function parseBaseTypeWithGenericTypes(type: string): string[] {