From a1a60d5fbb6dd7609a6a9391ffdfaee3a52f0896 Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Thu, 4 Dec 2025 10:25:18 +0300 Subject: [PATCH 01/18] 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 02/18] 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 03/18] 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 04/18] 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 05/18] 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 06/18] 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[] { From a80d87bf07c35a2616834c087be99afe1ef848cb Mon Sep 17 00:00:00 2001 From: enisn Date: Thu, 11 Dec 2025 16:39:13 +0300 Subject: [PATCH 07/18] Update index.md --- docs/en/modules/ai-management/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/modules/ai-management/index.md b/docs/en/modules/ai-management/index.md index ad75029b8b..cff4c992f4 100644 --- a/docs/en/modules/ai-management/index.md +++ b/docs/en/modules/ai-management/index.md @@ -436,7 +436,7 @@ Once configured, other applications can call your application's endpoints: Your application acts as a proxy, forwarding these requests to the AI Management microservice. -## Comparison Table +### Comparison Table | Scenario | Database Required | Manages Config | Executes AI | Exposes API | Use Case | |----------|------------------|----------------|-------------|-------------|----------| From b91700cec7292dec61742ec9cc227aa4e14df8bc Mon Sep 17 00:00:00 2001 From: enisn Date: Thu, 11 Dec 2025 17:17:20 +0300 Subject: [PATCH 08/18] Add AI management chat widget docs and improve formatting Added documentation and image for the AI Management chat widget, including usage examples and JavaScript API details. Improved table formatting and consistency throughout the AI Management module documentation for better readability. --- docs/en/images/ai-management-widget.png | Bin 0 -> 14597 bytes docs/en/modules/ai-management/index.md | 221 +++++++++++++++++++----- 2 files changed, 173 insertions(+), 48 deletions(-) create mode 100644 docs/en/images/ai-management-widget.png diff --git a/docs/en/images/ai-management-widget.png b/docs/en/images/ai-management-widget.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d9bbdbf8d0edf60c833cfe54dfb14f842c5b55 GIT binary patch literal 14597 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 literal 0 HcmV?d00001 diff --git a/docs/en/modules/ai-management/index.md b/docs/en/modules/ai-management/index.md index cff4c992f4..a1817d0c9a 100644 --- a/docs/en/modules/ai-management/index.md +++ b/docs/en/modules/ai-management/index.md @@ -21,7 +21,6 @@ abp add-module Volo.AIManagement Open ABP Studio, navigate to your solution explorer, **Right Click** on the project and select **Import Module**. Choose `Volo.AIManagement` from `NuGet` tab and check the "Install this Module" checkbox. Click the "OK" button to install the module. - ## Packages This module follows the [module development best practices guide](../../framework/architecture/best-practices) and consists of several NuGet and NPM packages. See the guide if you want to understand the packages and relations between them. @@ -37,7 +36,7 @@ AI Management module packages are designed for various usage scenarios. Packages AI Management module adds the following items to the "Main" menu: * **AI Management**: Root menu item for AI Management module. (`AIManagement`) - * **Workspaces**: Workspace management page. (`AIManagement.Workspaces`) + * **Workspaces**: Workspace management page. (`AIManagement.Workspaces`) `AIManagementMenus` class has the constants for the menu item names. @@ -80,21 +79,21 @@ Workspaces are the core concept of the AI Management module. A workspace represe When creating or managing a workspace, you can configure the following properties: -| Property | Required | Description | -|----------|----------|-------------| -| `Name` | Yes | Unique workspace identifier (cannot contain spaces) | -| `Provider` | Yes* | AI provider name (e.g., "OpenAI", "Ollama") | -| `ModelName` | Yes* | Model identifier (e.g., "gpt-4", "mistral") | -| `ApiKey` | No | API authentication key (required by some providers) | -| `ApiBaseUrl` | No | Custom endpoint URL (defaults to provider's default) | -| `SystemPrompt` | No | Default system prompt for all conversations | -| `Temperature` | No | Response randomness (0.0-1.0, defaults to provider default) | -| `Description` | No | Workspace description | -| `IsActive` | No | Enable/disable the workspace (default: true) | -| `ApplicationName` | No | Associate workspace with specific application | -| `RequiredPermissionName` | No | Permission required to use this workspace | -| `IsSystem` | No | Whether it's a system workspace (read-only) | -| `OverrideSystemConfiguration` | No | Allow database configuration to override code-defined settings | +| Property | Required | Description | +| ----------------------------- | -------- | -------------------------------------------------------------- | +| `Name` | Yes | Unique workspace identifier (cannot contain spaces) | +| `Provider` | Yes* | AI provider name (e.g., "OpenAI", "Ollama") | +| `ModelName` | Yes* | Model identifier (e.g., "gpt-4", "mistral") | +| `ApiKey` | No | API authentication key (required by some providers) | +| `ApiBaseUrl` | No | Custom endpoint URL (defaults to provider's default) | +| `SystemPrompt` | No | Default system prompt for all conversations | +| `Temperature` | No | Response randomness (0.0-1.0, defaults to provider default) | +| `Description` | No | Workspace description | +| `IsActive` | No | Enable/disable the workspace (default: true) | +| `ApplicationName` | No | Associate workspace with specific application | +| `RequiredPermissionName` | No | Permission required to use this workspace | +| `IsSystem` | No | Whether it's a system workspace (read-only) | +| `OverrideSystemConfiguration` | No | Allow database configuration to override code-defined settings | **\*Not required for system workspaces** @@ -158,7 +157,7 @@ public class WorkspaceDataSeederContributor : IDataSeedContributor, ITransientDe workspace.ApiKey = "your-api-key"; workspace.SystemPrompt = "You are a helpful customer support assistant."; - + await _workspaceRepository.InsertAsync(workspace); } ``` @@ -173,13 +172,12 @@ public class WorkspaceDataSeederContributor : IDataSeedContributor, ITransientDe The AI Management module defines the following permissions: -| Permission | Description | Default Granted To | -|------------|-------------|-------------------| -| `AIManagement.Workspaces` | View workspaces | Admin role | -| `AIManagement.Workspaces.Create` | Create new workspaces | Admin role | -| `AIManagement.Workspaces.Update` | Edit existing workspaces | Admin role | -| `AIManagement.Workspaces.Delete` | Delete workspaces | Admin role | - +| Permission | Description | Default Granted To | +| -------------------------------- | ------------------------ | ------------------ | +| `AIManagement.Workspaces` | View workspaces | Admin role | +| `AIManagement.Workspaces.Create` | Create new workspaces | Admin role | +| `AIManagement.Workspaces.Update` | Edit existing workspaces | Admin role | +| `AIManagement.Workspaces.Delete` | Delete workspaces | Admin role | ### Workspace-Level Permissions @@ -196,6 +194,7 @@ workspace.RequiredPermissionName = MyAppPermissions.AccessPremiumWorkspaces; ``` When a workspace has a required permission: + * Only authorized users with that permission can access the workspace endpoints * Users without the permission will receive an authorization error @@ -213,6 +212,7 @@ The AI Management module is designed to support various usage patterns, from sim In this scenario, you only use the ABP Framework's AI features directly. You configure AI providers (like OpenAI) in your code and don't need any database or management UI. **Required Packages:** + - `Volo.Abp.AI` - Any Microsoft AI extensions (e.g., `Microsoft.Extensions.AI.OpenAI`) @@ -250,7 +250,7 @@ public class MyService { _chatClient = chatClient; } - + public async Task GetResponseAsync(string prompt) { var response = await _chatClient.CompleteAsync(prompt); @@ -270,10 +270,12 @@ In this scenario, you install the AI Management module with its database layer, **Required Packages:** **Minimum (backend only):** + - `Volo.AIManagement.EntityFrameworkCore` (or `Volo.AIManagement.MongoDB`) - `Volo.AIManagement.OpenAI` (or another AI provider package) **Full installation (with UI and API):** + - `Volo.AIManagement.EntityFrameworkCore` (or `Volo.AIManagement.MongoDB`) - `Volo.AIManagement.Application` - `Volo.AIManagement.HttpApi` @@ -308,6 +310,7 @@ public class YourModule : AbpModule **Option 2 - Dynamic Workspace (UI-based):** No code configuration needed. Define workspaces through: + - The AI Management UI (navigate to AI Management > Workspaces) - Data seeding in your `DataSeeder` class @@ -332,6 +335,7 @@ public class MyService In this scenario, your application communicates with a separate AI Management microservice that manages configurations and communicates with AI providers on your behalf. The AI Management service handles all AI provider interactions. **Required Packages:** + - `Volo.AIManagement.Client.HttpApi.Client` **Configuration:** @@ -392,7 +396,7 @@ public class MyService var response = await _chatService.ChatCompletionsAsync(workspaceName, request); return response.Content; } - + // For streaming responses public async IAsyncEnumerable StreamAIResponseAsync(string workspaceName, string prompt) { @@ -419,6 +423,7 @@ public class MyService This scenario builds on Scenario 3, but your application exposes its own HTTP endpoints that other applications can call. Your application then forwards these requests to the AI Management service. **Required Packages:** + - `Volo.AIManagement.Client.HttpApi.Client` (to communicate with AI Management service) - `Volo.AIManagement.Client.Application` (application services) - `Volo.AIManagement.Client.HttpApi` (to expose HTTP endpoints) @@ -431,6 +436,7 @@ Same as Scenario 3, configure the remote AI Management service in `appsettings.j **Usage:** Once configured, other applications can call your application's endpoints: + - `POST /api/ai-management-client/chat-completion` for chat completions - `POST /api/ai-management-client/stream-chat-completion` for streaming responses @@ -438,15 +444,133 @@ Your application acts as a proxy, forwarding these requests to the AI Management ### Comparison Table -| Scenario | Database Required | Manages Config | Executes AI | Exposes API | Use Case | -|----------|------------------|----------------|-------------|-------------|----------| -| **1. No AI Management** | No | Code | Local | Optional | Simple apps, no config management needed | -| **2. Full AI Management** | Yes | Database/UI | Local | Optional | Monoliths, services managing their own AI | -| **3. Client Remote** | No | Remote Service | Remote Service | No | Microservices consuming AI centrally | -| **4. Client Proxy** | No | Remote Service | Remote Service | Yes | API Gateway pattern, proxy services | +| Scenario | Database Required | Manages Config | Executes AI | Exposes API | Use Case | +| ------------------------- | ----------------- | -------------- | -------------- | ----------- | ----------------------------------------- | +| **1. No AI Management** | No | Code | Local | Optional | Simple apps, no config management needed | +| **2. Full AI Management** | Yes | Database/UI | Local | Optional | Monoliths, services managing their own AI | +| **3. Client Remote** | No | Remote Service | Remote Service | No | Microservices consuming AI centrally | +| **4. Client Proxy** | No | Remote Service | Remote Service | Yes | API Gateway pattern, proxy services | + + + +## Client Usage (MVC UI) + +AI Management has a distinction in the naming of the packages. The `Volo.AIManagement.*` packages are for the main AI operations in the process and application itself. The `Volo.AIManagement.Client.*` packages are designed to consume the AI services from out of the application that is hosting the AI features. `Volo.AIManagement.*` packages don't expose any application service and endpoints to be consumed by default. You'll need to install the `Volo.AIManagement.Client.*` packages to both of your applications to expose the AI services and to consume the AI services to both of your applications. + +**List of packages:** +- `Volo.AIManagement.Client.Application` +- `Volo.AIManagement.Client.Application.Contracts` +- `Volo.AIManagement.Client.HttpApi` +- `Volo.AIManagement.Client.HttpApi.Client` +- `Volo.AIManagement.Client.Web` + +### The Chat Widget +The `Volo.AIManagement.Client.Web` package provides a chat widget to allow you easily integrate a chat interface into your application that uses a specific AI workspace named `ChatClientChatViewComponent`. + +#### Basic Usage +You can invoke the `ChatClientChatViewComponent` Widget in your razor page with the following code: + +```csharp +@await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel +{ + WorkspaceName = "mylama", +}) +``` + +![ai-management-workspaces](../../images/ai-management-widget.png) + +#### Properties +You can customize the chat widget with the following properties: +- `WorkspaceName`: The name of the workspace to use. +- `ComponentId`: The id of the component to use. +- `ConversationId`: The id of the conversation to store the chat history at the client side. +- `Title`: The title of the chat widget. +- `ShowStreamCheckbox`: Whether to show the stream checkbox. Allows user to toggle streaming on and off. Default is `false`. +- `UseStreaming`: Whether to use streaming by default. Default is `true`. _(overridden by `ShowStreamCheckbox` at UI)_ + +```csharp +@await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel +{ + WorkspaceName = "mylama", + ComponentId = "mylama-chat", + ConversationId = "mylama-conversation-" + @CurrentUser.Id, + Title = "My Custom Title", + ShowStreamCheckbox = true, + UseStreaming = true +}) +``` + +#### Using Conversation Id +You can use the `ConversationId` property to specify the id of the conversation to use. When the Conversation Id is provided, the chat will be stored at the client side and will be retrieved when the user revisits the page that contains the chat widget. +```csharp +@await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel +{ + WorkspaceName = "mylama", + ConversationId = "my-support-conversation-" + @CurrentUser.Id +}) +``` + +#### JavaScript API +All the initialized components in the page is stored in the `abp.chatComponents` object. You can retrieve a specific component by its `ComponentId` which is defined while invoking the ViewComponent. + +```csharp +@await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel +{ + WorkspaceName = "mylama", + ComponentId = "mylama-chat" +}) +``` + +You can then use the JavaScript API to interact with the component. +```js +var chatComponent = abp.chatComponents.get('mylama-chat'); +``` + +Once you have the component, you can use the following functions to interact with it: + +```js +// Switch to a different conversation +chatComponent.switchConversation(conversationId); + +// Create a new conversation with a specific model +chatComponent.createConversation(conversationId, modelName); + +// Clear the current conversation history +chatComponent.clearConversation(); + +// Get the current conversation ID (returns null for ephemeral conversations) +var currentId = chatComponent.getCurrentConversationId(); + +// Initialize with a specific conversation ID +chatComponent.initialize(conversationId); + +// Send a message programmatically +chatComponent.sendMessage(); + +// Listen to events +chatComponent.on('messageSent', function(data) { + console.log('Message sent:', data.message); + console.log('Conversation ID:', data.conversationId); + console.log('Is first message:', data.isFirstMessage); +}); + +chatComponent.on('messageReceived', function(data) { + console.log('AI response:', data.message); + console.log('Conversation ID:', data.conversationId); + console.log('Is streaming:', data.isStreaming); +}); + +chatComponent.on('streamStarted', function(data) { + console.log('Streaming started for conversation:', data.conversationId); +}); + +// Remove event listeners +chatComponent.off('messageSent', callbackFunction); +``` ## Using Dynamic Workspace Configurations for custom requirements + The AI Management module allows you to access only configuration of a workspace without resolving pre-constructed chat client. This is useful when you want to use a workspace for your own purposes and you don't need to use the chat client. The `IWorkspaceConfigurationStore` service is used to access the configuration of a workspace. It has multiple implementaations according to the usage scenario. @@ -463,7 +587,7 @@ public class MyService { // Get the configuration of the workspace that can be managed dynamically. var configuration = await _workspaceConfigurationStore.GetAsync("MyWorkspace"); - + // Do something with the configuration var kernel = Kernel.CreateBuilder() .AddAzureOpenAIChatClient( @@ -543,24 +667,23 @@ public override void ConfigureServices(ServiceConfigurationContext context) > [!TIP] > For production scenarios, you may want to add validation for the factory configuration. - ### Available Configuration Properties The `ChatClientCreationConfiguration` object provides the following properties from the database: -| Property | Type | Description | -|----------|------|-------------| -| `Name` | string | Workspace name | -| `Provider` | string | Provider name (e.g., "OpenAI", "Ollama") | -| `ApiKey` | string? | API key for authentication | -| `ModelName` | string | Model identifier (e.g., "gpt-4", "mistral") | -| `SystemPrompt` | string? | Default system prompt for the workspace | -| `Temperature` | float? | Temperature setting for response generation | -| `ApiBaseUrl` | string? | Custom API endpoint URL | -| `Description` | string? | Workspace description | -| `IsActive` | bool | Whether the workspace is active | -| `IsSystem` | bool | Whether it's a system workspace | -| `RequiredPermissionName` | string? | Permission required to use this workspace | +| Property | Type | Description | +| ------------------------ | ------- | ------------------------------------------- | +| `Name` | string | Workspace name | +| `Provider` | string | Provider name (e.g., "OpenAI", "Ollama") | +| `ApiKey` | string? | API key for authentication | +| `ModelName` | string | Model identifier (e.g., "gpt-4", "mistral") | +| `SystemPrompt` | string? | Default system prompt for the workspace | +| `Temperature` | float? | Temperature setting for response generation | +| `ApiBaseUrl` | string? | Custom API endpoint URL | +| `Description` | string? | Workspace description | +| `IsActive` | bool | Whether the workspace is active | +| `IsSystem` | bool | Whether it's a system workspace | +| `RequiredPermissionName` | string? | Permission required to use this workspace | ### Example: Azure OpenAI Factory @@ -597,6 +720,7 @@ public class AzureOpenAIChatClientFactory : IChatClientFactory, ITransientDepend After implementing and registering your factory: 1. **Through UI**: Navigate to the AI Management workspaces page and create a new workspace: + - Select your provider name (e.g., "Ollama", "AzureOpenAI") - Configure the API settings - Set the model name @@ -665,6 +789,7 @@ WorkspaceConfiguration:{ApplicationName}:{WorkspaceName} ``` ### HttpApi Client Layer + - `IntegrationWorkspaceConfigurationStore`: Integration service for remote workspace configuration retrieval. Implements `IWorkspaceConfigurationStore` interface. The cache is automatically invalidated when workspaces are created, updated, or deleted. From 1ff7243039ed127d64a89c86b6fb7288a93217bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:19:22 +0000 Subject: [PATCH 09/18] Optimised images with calibre/image-actions --- docs/en/images/ai-management-widget.png | Bin 14597 -> 3748 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/en/images/ai-management-widget.png b/docs/en/images/ai-management-widget.png index f3d9bbdbf8d0edf60c833cfe54dfb14f842c5b55..2f396f3e5aab629a7ff87cf48857f844494fa396 100644 GIT binary patch literal 3748 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 HX}U~Hf?1*J$GBSED1Py@qQ5J*5N(p69_6a&&B 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 From a5bbe1998aa712a4112db01ace7672bf4f202d36 Mon Sep 17 00:00:00 2001 From: enisn Date: Thu, 11 Dec 2025 17:20:43 +0300 Subject: [PATCH 10/18] Extend Conversation Id usage --- docs/en/modules/ai-management/index.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/en/modules/ai-management/index.md b/docs/en/modules/ai-management/index.md index a1817d0c9a..a5a1ae0b2d 100644 --- a/docs/en/modules/ai-management/index.md +++ b/docs/en/modules/ai-management/index.md @@ -500,8 +500,9 @@ You can customize the chat widget with the following properties: }) ``` -#### Using Conversation Id -You can use the `ConversationId` property to specify the id of the conversation to use. When the Conversation Id is provided, the chat will be stored at the client side and will be retrieved when the user revisits the page that contains the chat widget. +#### Using the Conversation Id +You can use the `ConversationId` property to specify the id of the conversation to use. When the Conversation Id is provided, the chat will be stored at the client side and will be retrieved when the user revisits the page that contains the chat widget. If it's not provided or provided as **null**, the chat will be temporary and will not be saved, it'll be lost when the component lifetime ends. + ```csharp @await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel { From 5ac8986be8d16208259ab69b500c0592c9f8f93e Mon Sep 17 00:00:00 2001 From: enisn Date: Thu, 11 Dec 2025 17:26:29 +0300 Subject: [PATCH 11/18] Clarify package usage in AI Management docs Updated the documentation to better explain the distinction between `Volo.AIManagement.*` and `Volo.AIManagement.Client.*` packages, detailing their intended usage scenarios and roles. --- docs/en/modules/ai-management/index.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/en/modules/ai-management/index.md b/docs/en/modules/ai-management/index.md index a5a1ae0b2d..00c78a8944 100644 --- a/docs/en/modules/ai-management/index.md +++ b/docs/en/modules/ai-management/index.md @@ -455,7 +455,11 @@ Your application acts as a proxy, forwarding these requests to the AI Management ## Client Usage (MVC UI) -AI Management has a distinction in the naming of the packages. The `Volo.AIManagement.*` packages are for the main AI operations in the process and application itself. The `Volo.AIManagement.Client.*` packages are designed to consume the AI services from out of the application that is hosting the AI features. `Volo.AIManagement.*` packages don't expose any application service and endpoints to be consumed by default. You'll need to install the `Volo.AIManagement.Client.*` packages to both of your applications to expose the AI services and to consume the AI services to both of your applications. +AI Management uses different packages depending on the usage scenario: + +- **`Volo.AIManagement.*` packages**: These contain the core AI functionality and are used when your application hosts and manages its own AI operations. These packages don't expose any application service and endpoints to be consumed by default. + +- **`Volo.AIManagement.Client.*` packages**: These are designed for applications that need to consume AI services from a remote application. They provide both server and client side of remote access to the AI services. **List of packages:** - `Volo.AIManagement.Client.Application` From ea47c500abcb39b4c7bb6c001be73b8e799c4fbc Mon Sep 17 00:00:00 2001 From: enisn Date: Thu, 11 Dec 2025 17:33:23 +0300 Subject: [PATCH 12/18] Update chat component JS API docs with best practices Expanded the JavaScript API documentation for chat components to clarify initialization timing and added best-practice examples for accessing components after user interaction, rather than at page load. --- docs/en/modules/ai-management/index.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/en/modules/ai-management/index.md b/docs/en/modules/ai-management/index.md index 00c78a8944..a699ea1688 100644 --- a/docs/en/modules/ai-management/index.md +++ b/docs/en/modules/ai-management/index.md @@ -516,7 +516,7 @@ You can use the `ConversationId` property to specify the id of the conversation ``` #### JavaScript API -All the initialized components in the page is stored in the `abp.chatComponents` object. You can retrieve a specific component by its `ComponentId` which is defined while invoking the ViewComponent. +The chat components are initialized automatically when the ViewComponent is rendered in the page. All the initialized components in the page are stored in the `abp.chatComponents` object. You can retrieve a specific component by its `ComponentId` which is defined while invoking the ViewComponent. ```csharp @await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel @@ -573,6 +573,28 @@ chatComponent.on('streamStarted', function(data) { chatComponent.off('messageSent', callbackFunction); ``` +**Best-practices:** +- Don't try to access the component at the page load time, it's not guaranteed to be initialized yet. Get the component whenever you need it to make sure it's **initialized** and the **latest state** is applied. + +❌ Don't do this +```js +(function(){ + var chatComponent = abp.chatComponents.get('mylama-chat'); + $('#my-button').on('click', function() { + chatComponent.clearConversation(); + }); +}); +``` + +✅ Do this +```js +(function(){ + $('#my-button').on('click', function() { + var chatComponent = abp.chatComponents.get('mylama-chat'); + chatComponent.clearConversation(); + }); +}); +``` ## Using Dynamic Workspace Configurations for custom requirements From 8110e9467af316911ec58617fc5782a700a002f8 Mon Sep 17 00:00:00 2001 From: Enis Necipoglu Date: Thu, 11 Dec 2025 17:40:27 +0300 Subject: [PATCH 13/18] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/en/modules/ai-management/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/modules/ai-management/index.md b/docs/en/modules/ai-management/index.md index a699ea1688..ee6236549f 100644 --- a/docs/en/modules/ai-management/index.md +++ b/docs/en/modules/ai-management/index.md @@ -469,7 +469,7 @@ AI Management uses different packages depending on the usage scenario: - `Volo.AIManagement.Client.Web` ### The Chat Widget -The `Volo.AIManagement.Client.Web` package provides a chat widget to allow you easily integrate a chat interface into your application that uses a specific AI workspace named `ChatClientChatViewComponent`. +The `Volo.AIManagement.Client.Web` package provides a chat widget to allow you to easily integrate a chat interface into your application that uses a specific AI workspace named `ChatClientChatViewComponent`. #### Basic Usage You can invoke the `ChatClientChatViewComponent` Widget in your razor page with the following code: @@ -486,11 +486,11 @@ You can invoke the `ChatClientChatViewComponent` Widget in your razor page with #### Properties You can customize the chat widget with the following properties: - `WorkspaceName`: The name of the workspace to use. -- `ComponentId`: The id of the component to use. -- `ConversationId`: The id of the conversation to store the chat history at the client side. +- `ComponentId`: Unique identifier for accessing the component via JavaScript API (stored in abp.chatComponents). +- `ConversationId`: The unique identifier for persisting and retrieving chat history from client-side storage. - `Title`: The title of the chat widget. - `ShowStreamCheckbox`: Whether to show the stream checkbox. Allows user to toggle streaming on and off. Default is `false`. -- `UseStreaming`: Whether to use streaming by default. Default is `true`. _(overridden by `ShowStreamCheckbox` at UI)_ +- `UseStreaming`: Default streaming behavior. Can be overridden by user when `ShowStreamCheckbox` is true. ```csharp @await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel From 3622ea7b6c9138878cc337f6bb30807e76ec9f5a Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 12 Dec 2025 15:02:20 +0800 Subject: [PATCH 14/18] Exclude common build folders when searching for .csproj files --- .../LocalReferenceConverter.cs | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/LocalReferenceConverter.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/LocalReferenceConverter.cs index 67debab2eb..4188a75b68 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/LocalReferenceConverter.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/LocalReferenceConverter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; @@ -12,9 +13,9 @@ namespace Volo.Abp.Cli.ProjectModification; public class LocalReferenceConverter : ITransientDependency { - + public ILogger Logger { get; set; } - + public async Task ConvertAsync( [NotNull] string directory, [NotNull] List localPaths) @@ -26,14 +27,14 @@ public class LocalReferenceConverter : ITransientDependency var targetProjects = Directory.GetFiles(directory, "*.csproj", SearchOption.AllDirectories); Logger.LogInformation($"Converting projects to local reference."); - + foreach (var targetProject in targetProjects) { Logger.LogInformation($"Converting to local reference: {targetProject}"); - + await ConvertProjectToLocalReferences(targetProject, localProjects); } - + Logger.LogInformation($"Converted {targetProjects.Length} projects to local references."); } @@ -41,14 +42,14 @@ public class LocalReferenceConverter : ITransientDependency { var xmlDocument = new XmlDocument() { PreserveWhitespace = true }; xmlDocument.Load(GenerateStreamFromString(File.ReadAllText(targetProject))); - + var matchedNodes = xmlDocument.SelectNodes($"/Project/ItemGroup/PackageReference[@Include]"); if (matchedNodes == null || matchedNodes.Count == 0) { return; } - + foreach (XmlNode matchedNode in matchedNodes) { var packageName = matchedNode!.Attributes!["Include"].Value; @@ -62,7 +63,7 @@ public class LocalReferenceConverter : ITransientDependency { continue; } - + var parentNode = matchedNode.ParentNode; parentNode!.RemoveChild(matchedNode); @@ -72,10 +73,10 @@ public class LocalReferenceConverter : ITransientDependency newNode.Attributes.Append(includeAttr); parentNode.AppendChild(newNode); } - + File.WriteAllText(targetProject, XDocument.Parse(xmlDocument.OuterXml).ToString()); } - + private string CalculateRelativePath(string targetProject, string localProject) { return new Uri(targetProject).MakeRelativeUri(new Uri(localProject)).ToString(); @@ -91,8 +92,12 @@ public class LocalReferenceConverter : ITransientDependency { continue; } - - list.AddRange(Directory.GetFiles(localPath, "*.csproj", SearchOption.AllDirectories)); + + var ignoreFolders = new[] { "bin", "obj", ".vs", ".idea", ".vscode", ".git" }; + var csprojFiles = Directory.GetFiles(localPath, "*.csproj", SearchOption.AllDirectories) + .Where(x => !ignoreFolders.Any(x.Contains)) + .ToList(); + list.AddRange(csprojFiles); } return list; From 546b12fa8eda8ee629df090be44292a5bc6f24eb Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Fri, 12 Dec 2025 11:17:04 +0300 Subject: [PATCH 15/18] docs added --- docs/en/docs-nav.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 79cd320920..e1569064dc 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -1512,6 +1512,10 @@ "text": "Routing", "path": "framework/ui/blazor/routing.md" }, + { + "text": "SSR Configuration", + "path": "framework/ui/angular/ssr-configuration.md" + }, { "text": "PWA Configuration", "path": "framework/ui/blazor/pwa-configuration.md" From 0969c0ebcb08b3ea7b048f9cb4e883ed96e86973 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Fri, 12 Dec 2025 11:20:37 +0300 Subject: [PATCH 16/18] docs added --- .../framework/ui/angular/ssr-configuration.md | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 docs/en/framework/ui/angular/ssr-configuration.md diff --git a/docs/en/framework/ui/angular/ssr-configuration.md b/docs/en/framework/ui/angular/ssr-configuration.md new file mode 100644 index 0000000000..258fce99b4 --- /dev/null +++ b/docs/en/framework/ui/angular/ssr-configuration.md @@ -0,0 +1,280 @@ +```json +//[doc-seo] +{ + "Description": "Learn how to configure Server-Side Rendering (SSR) for your Angular application in the ABP Framework to improve performance and SEO." +} +``` + +# SSR Configuration + +[Server-Side Rendering (SSR)](https://angular.io/guide/ssr) is a process that involves rendering pages on the server, resulting in initial HTML content that contains the page state. This allows the browser to show the page to the user immediately, before the JavaScript bundles are downloaded and executed. + +SSR improves the **performance** (First Contentful Paint) and **SEO** (Search Engine Optimization) of your application. + +## 1. Install ABP Angular SSR + +The ABP Framework provides a schematic to easily add SSR support to your Angular application. + +Run the following command in the root folder of your Angular application: + +```shell +yarn ng generate @abp/ng.schematics:ssr-add +``` + +Alternatively, you can specify the project name if you have a multi-project workspace: + +```shell +yarn ng generate @abp/ng.schematics:ssr-add --project MyProjectName +``` + +This command automates the setup process by installing necessary dependencies, creating server-side entry points, and updating your configuration files. + +## 2. What Changes? + +When you run the schematic, it performs the following actions: + +### 2.1. Dependencies + +It adds the following packages to your `package.json`: + +- **express**: A minimal and flexible Node.js web application framework. +- **@types/express**: Type definitions for Express. +- **openid-client**: A library for OpenID Connect (OIDC) relying party (RP) implementation, used for authentication on the server. + +```json +{ + "dependencies": { + "express": "^4.18.2", + "openid-client": "^5.6.4" + }, + "devDependencies": { + "@types/express": "^4.17.17" + } +} +``` + +**For Webpack projects only:** +- **browser-sync** (Dev dependency): Used for live reloading during development. + +### 2.2. Scripts & Configuration + +The changes depend on the builder used in your project (Application Builder or Webpack). + +#### Application Builder (esbuild) + +If your project uses the **Application Builder** (`@angular/build:application`), the schematic: + +- **Scripts**: Adds `serve:ssr:project-name` to serve the SSR application. +- **angular.json**: Updates the `build` target to enable SSR (`outputMode: 'server'`) and sets the SSR entry point. + +```json +{ + "projects": { + "MyProjectName": { + "architect": { + "build": { + "options": { + "outputPath": "dist/MyProjectName", + "outputMode": "server", + "ssr": { + "entry": "src/server.ts" + } + } + } + } + } + } +} +``` + +- **tsconfig**: Updates the application's `tsconfig` to include `server.ts`. + +#### Webpack Builder + +If your project uses the **Webpack Builder** (`@angular-devkit/build-angular:browser`), the schematic: + +- **Scripts**: Adds `dev:ssr`, `serve:ssr`, `build:ssr`, and `prerender` scripts. +- **angular.json**: Adds new targets: `server`, `serve-ssr`, and `prerender`. +- **tsconfig**: Updates the server's `tsconfig` to include `server.ts`. + +### 2.3. Files + +- **server.ts**: This file is the main entry point for the server-side application. + - **Standalone Projects**: Generates a server entry point compatible with `bootstrapApplication`. + - **NgModule Projects**: Generates a server entry point compatible with `platformBrowserDynamic`. + +```typescript +import { + AngularNodeAppEngine, + createNodeRequestHandler, + isMainModule, + writeResponseToNodeResponse, +} from '@angular/ssr/node'; +import express from 'express'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { environment } from './environments/environment'; +import { ServerCookieParser } from '@abp/ng.core'; +import * as oidc from 'openid-client'; + +// ... (OIDC configuration and setup) + +const app = express(); +const angularApp = new AngularNodeAppEngine(); + +// ... (OIDC routes: /authorize, /logout, /) + +/** + * Serve static files from /browser + */ +app.use( + express.static(browserDistFolder, { + maxAge: '1y', + index: false, + redirect: false, + }), +); + +/** + * Handle all other requests by rendering the Angular application. + */ +app.use((req, res, next) => { + angularApp + .handle(req) + .then(response => { + if (response) { + res.cookie('ssr-init', 'true', {...secureCookie, httpOnly: false}); + return writeResponseToNodeResponse(response, res); + } else { + return next() + } + }) + .catch(next); +}); + +// ... (Start server logic) + +export const reqHandler = createNodeRequestHandler(app); +``` +- **app.routes.server.ts**: Defines server-side routes and render modes (e.g., Prerender, Server, Client). This allows fine-grained control over how each route is rendered. + +```typescript +import { RenderMode, ServerRoute } from '@angular/ssr'; + +export const serverRoutes: ServerRoute[] = [ + { + path: '**', + renderMode: RenderMode.Server + } +]; +``` + +- **app.config.server.ts**: Merges the application configuration with server-specific providers. + +```typescript +import { mergeApplicationConfig, ApplicationConfig, provideAppInitializer, inject, PLATFORM_ID, TransferState } from '@angular/core'; +import { isPlatformServer } from '@angular/common'; +import { provideServerRendering, withRoutes } from '@angular/ssr'; +import { appConfig } from './app.config'; +import { serverRoutes } from './app.routes.server'; +import { SSR_FLAG } from '@abp/ng.core'; + +const serverConfig: ApplicationConfig = { + providers: [ + provideAppInitializer(() => { + const platformId = inject(PLATFORM_ID); + const transferState = inject(TransferState); + if (isPlatformServer(platformId)) { + transferState.set(SSR_FLAG, true); + } + }), + provideServerRendering(withRoutes(serverRoutes)), + ], +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); +``` +- **index.html**: Removes the loading spinner (`
`) to prevent hydration mismatches. + +## 3. Running the Application + +After the installation is complete, you can run your application with SSR support. + +### Application Builder + +To serve the application with SSR in development: + +```shell +yarn start +# or +yarn ng serve +``` + +To serve the built application (production): + +```shell +yarn run serve:ssr:project-name +``` + +### Webpack Builder + +**Development:** + +```shell +yarn run dev:ssr +``` + +**Production:** + +```shell +yarn run build:ssr +yarn run serve:ssr +``` + +## 4. Authentication & SSR + +The schematic installs `openid-client` to handle authentication on the server side. This ensures that when a user accesses a protected route, the server can validate their session or redirect them to the login page before rendering the content. + +> Ensure your OpenID Connect configuration (in `environment.ts` or `app.config.ts`) is compatible with the server environment. + +## 5. Deployment + +To deploy your Angular SSR application to a production server, follow these steps: + +### 5.1. Build the Application + +Run the build command to generate the production artifacts: + +```shell +yarn build +# or if using Webpack builder +yarn run build:ssr +``` + +### 5.2. Prepare Artifacts + +After the build is complete, you will find the output in the `dist` folder. +For the **Application Builder**, the output structure typically looks like this: + +``` +dist/MyProjectName/ +├── browser/ # Client-side bundles +└── server/ # Server-side bundles and entry point (server.mjs) +``` + +You need to copy the entire `dist/MyProjectName` folder to your server. + +### 5.3. Run the Server + +On your server, navigate to the folder where you copied the artifacts and run the server using Node.js: + +```shell +node server/server.mjs +``` + +> [!TIP] +> It is recommended to use a process manager like [PM2](https://pm2.keymetrics.io/) to keep your application alive and handle restarts. + +```shell +pm2 start server/server.mjs --name "my-app" +``` From a89d6b86987f7117ef7265f79e2f3dea9b3f009d Mon Sep 17 00:00:00 2001 From: sumeyye Date: Fri, 12 Dec 2025 14:07:22 +0300 Subject: [PATCH 17/18] update: move ssr nav item under angular ui --- docs/en/docs-nav.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index e1569064dc..a8d009ca70 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -1512,10 +1512,6 @@ "text": "Routing", "path": "framework/ui/blazor/routing.md" }, - { - "text": "SSR Configuration", - "path": "framework/ui/angular/ssr-configuration.md" - }, { "text": "PWA Configuration", "path": "framework/ui/blazor/pwa-configuration.md" @@ -1553,6 +1549,10 @@ "text": "Service Proxies", "path": "framework/ui/angular/service-proxies.md" }, + { + "text": "SSR Configuration", + "path": "framework/ui/angular/ssr-configuration.md" + }, { "text": "PWA Configuration", "path": "framework/ui/angular/pwa-configuration.md" From f9c6c006b875afcc3d658682f327e8013e36e577 Mon Sep 17 00:00:00 2001 From: berkansasmaz Date: Fri, 12 Dec 2025 16:07:29 +0300 Subject: [PATCH 18/18] fix(blazorise-datagrid): make entity actions column field resolution safer --- .../Components/DataGridEntityActionsColumn.razor.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/DataGridEntityActionsColumn.razor.cs b/framework/src/Volo.Abp.BlazoriseUI/Components/DataGridEntityActionsColumn.razor.cs index 9a39acd4fc..0180833054 100644 --- a/framework/src/Volo.Abp.BlazoriseUI/Components/DataGridEntityActionsColumn.razor.cs +++ b/framework/src/Volo.Abp.BlazoriseUI/Components/DataGridEntityActionsColumn.razor.cs @@ -23,7 +23,16 @@ public partial class DataGridEntityActionsColumn : DataGridColumn Caption = UiLocalizer["Actions"]; Width = "150px"; Sortable = false; - Field = typeof(TItem).GetProperties().First().Name; + Field = ResolveFieldName(); + return ValueTask.CompletedTask; } + + protected virtual string ResolveFieldName() + { + var props = typeof(TItem).GetProperties(); + return props.Length > 0 + ? props[0].Name + : "Id"; + } }