From a1a60d5fbb6dd7609a6a9391ffdfaee3a52f0896 Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Thu, 4 Dec 2025 10:25:18 +0300 Subject: [PATCH 1/6] Add nullable property support to API modeling Introduces IsNullable detection in ReflectionHelper and propagates it to PropertyApiDescriptionModel and TypeScript models. Updates logic to consider nullable properties when determining optionality, improving API schema accuracy for nullable reference types. --- .../Volo/Abp/Reflection/ReflectionHelper.cs | 20 +++++++++++++++++++ .../Modeling/PropertyApiDescriptionModel.cs | 3 +++ .../schematics/src/models/api-definition.ts | 1 + .../packages/schematics/src/utils/model.ts | 1 + 4 files changed, 25 insertions(+) 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..8c2142c37f 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs @@ -8,6 +8,26 @@ namespace Volo.Abp.Reflection; //TODO: Consider to make internal public static class ReflectionHelper { + /// + /// Checks whether the property is nullable, including nullable reference types (NRT). + /// + /// Property info to check + public static bool IsNullable(PropertyInfo propertyInfo) + { + if (propertyInfo.PropertyType.IsValueType) + { + return false; + } + +#if NET6_0_OR_GREATER + var nullabilityInfoContext = new System.Reflection.NullabilityInfoContext(); + var nullabilityInfo = nullabilityInfoContext.Create(propertyInfo); + return nullabilityInfo.ReadState == System.Reflection.NullabilityState.Nullable; +#else + return false; +#endif + } + //TODO: Ehhance summary /// /// Checks whether implements/inherits . 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..cc2dcde767 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 @@ -29,6 +29,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 +41,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 = Volo.Abp.Reflection.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/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..40dacbdfa4 100644 --- a/npm/ng-packs/packages/schematics/src/utils/model.ts +++ b/npm/ng-packs/packages/schematics/src/utils/model.ts @@ -187,6 +187,7 @@ export function createRefToImportReducerCreator(params: ModelGeneratorParams) { function isOptionalProperty(prop: PropertyDef) { return ( + prop.isNullable || prop.typeSimple.endsWith('?') || ((prop.typeSimple === 'string' || prop.typeSimple.includes('enum')) && !prop.isRequired) ); From f8bff957a8aedba3a232545e20703716ff0637af Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 7 Dec 2025 11:35:01 +0800 Subject: [PATCH 2/6] Remove IsNullable from ReflectionHelper and update usage --- .../Volo/Abp/Reflection/ReflectionHelper.cs | 20 ------------------- .../Modeling/PropertyApiDescriptionModel.cs | 3 ++- 2 files changed, 2 insertions(+), 21 deletions(-) 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 8c2142c37f..ab26313736 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs @@ -8,26 +8,6 @@ namespace Volo.Abp.Reflection; //TODO: Consider to make internal public static class ReflectionHelper { - /// - /// Checks whether the property is nullable, including nullable reference types (NRT). - /// - /// Property info to check - public static bool IsNullable(PropertyInfo propertyInfo) - { - if (propertyInfo.PropertyType.IsValueType) - { - return false; - } - -#if NET6_0_OR_GREATER - var nullabilityInfoContext = new System.Reflection.NullabilityInfoContext(); - var nullabilityInfo = nullabilityInfoContext.Create(propertyInfo); - return nullabilityInfo.ReadState == System.Reflection.NullabilityState.Nullable; -#else - return false; -#endif - } - //TODO: Ehhance summary /// /// Checks whether implements/inherits . 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 cc2dcde767..e018782308 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; @@ -41,7 +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 = Volo.Abp.Reflection.ReflectionHelper.IsNullable(propertyInfo), + IsNullable = TypeHelper.IsNullable(propertyInfo.PropertyType), 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, From 8fee4a8a10b7e7352cd1ad8aee0ef9e3de76487d Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Mon, 8 Dec 2025 13:48:18 +0300 Subject: [PATCH 3/6] Handle nullable properties in model generator Adds support for marking properties as nullable by appending '| null' to their type in the model generator. Also refines the logic for determining optional properties to better align with 'isRequired' and 'isNullable' flags. --- npm/ng-packs/packages/schematics/src/utils/model.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/npm/ng-packs/packages/schematics/src/utils/model.ts b/npm/ng-packs/packages/schematics/src/utils/model.ts index 40dacbdfa4..e2213cb37c 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,11 +190,7 @@ export function createRefToImportReducerCreator(params: ModelGeneratorParams) { } function isOptionalProperty(prop: PropertyDef) { - return ( - prop.isNullable || - prop.typeSimple.endsWith('?') || - ((prop.typeSimple === 'string' || prop.typeSimple.includes('enum')) && !prop.isRequired) - ); + return !prop.isRequired && prop.isNullable; } export function parseBaseTypeWithGenericTypes(type: string): string[] { From 4a36812cd5543bf2e1cbef0a77af0567a805cda4 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 8 Dec 2025 20:59:23 +0800 Subject: [PATCH 4/6] Improve nullable type detection in API modeling --- .../src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs | 8 +++++++- .../Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) 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..f6375118ec 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); @@ -116,6 +116,12 @@ public static class TypeHelper return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); } + public static bool IsNullableOrNotValueType(Type type) + { + return (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) || + !type.IsValueType; + } + public static bool IsNullableEnum(Type type) { return type.IsGenericType && 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 e018782308..b30dc89615 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 @@ -42,7 +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 = TypeHelper.IsNullable(propertyInfo.PropertyType), + IsNullable = TypeHelper.IsNullableOrNotValueType(propertyInfo.PropertyType), 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, From ddfaf82496c1ff25d4b04c0475540562276aa0fb Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 10 Dec 2025 10:42:57 +0800 Subject: [PATCH 5/6] Add IsNullable property detection for PropertyInfo --- .../Volo/Abp/Reflection/ReflectionHelper.cs | 45 +++++++++++++++ .../Volo/Abp/Reflection/TypeHelper.cs | 6 -- .../Modeling/PropertyApiDescriptionModel.cs | 2 +- .../Volo.Abp.Core.Tests.csproj | 1 + .../Abp/Reflection/ReflectionHelper_Tests.cs | 57 +++++++++++++++++++ 5 files changed, 104 insertions(+), 7 deletions(-) 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 f6375118ec..e423e2087f 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs @@ -116,12 +116,6 @@ public static class TypeHelper return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); } - public static bool IsNullableOrNotValueType(Type type) - { - return (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) || - !type.IsValueType; - } - public static bool IsNullableEnum(Type type) { return type.IsGenericType && 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 b30dc89615..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 @@ -42,7 +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 = TypeHelper.IsNullableOrNotValueType(propertyInfo.PropertyType), + 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 { From d18a9e7006b11842ffefb1f1ce1433f94df35fe9 Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Wed, 10 Dec 2025 11:38:40 +0300 Subject: [PATCH 6/6] Update optional property check in model utils The isOptionalProperty function now only checks if a property is not required, removing the isNullable condition. This simplifies the logic for determining optional properties. --- npm/ng-packs/packages/schematics/src/utils/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/schematics/src/utils/model.ts b/npm/ng-packs/packages/schematics/src/utils/model.ts index e2213cb37c..4e5431da60 100644 --- a/npm/ng-packs/packages/schematics/src/utils/model.ts +++ b/npm/ng-packs/packages/schematics/src/utils/model.ts @@ -190,7 +190,7 @@ export function createRefToImportReducerCreator(params: ModelGeneratorParams) { } function isOptionalProperty(prop: PropertyDef) { - return !prop.isRequired && prop.isNullable; + return !prop.isRequired; } export function parseBaseTypeWithGenericTypes(type: string): string[] {