Browse Source

Merge branch 'dev' into Passkey

pull/24278/head
maliming 2 months ago
parent
commit
05f1fce810
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. BIN
      docs/en/images/ai-management-widget.png
  2. 250
      docs/en/modules/ai-management/index.md
  3. 11
      framework/src/Volo.Abp.BlazoriseUI/Components/DataGridEntityActionsColumn.razor.cs
  4. 29
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/LocalReferenceConverter.cs
  5. 45
      framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs
  6. 2
      framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
  7. 4
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs
  8. 1
      framework/test/Volo.Abp.Core.Tests/Volo.Abp.Core.Tests.csproj
  9. 57
      framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/ReflectionHelper_Tests.cs
  10. 1
      npm/ng-packs/packages/schematics/src/models/api-definition.ts
  11. 9
      npm/ng-packs/packages/schematics/src/utils/model.ts

BIN
docs/en/images/ai-management-widget.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

250
docs/en/modules/ai-management/index.md

@ -28,7 +28,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.
@ -44,7 +43,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.
@ -87,21 +86,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**
@ -165,7 +164,7 @@ public class WorkspaceDataSeederContributor : IDataSeedContributor, ITransientDe
workspace.ApiKey = "your-api-key";
workspace.SystemPrompt = "You are a helpful customer support assistant.";
await _workspaceRepository.InsertAsync(workspace);
}
```
@ -180,13 +179,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
@ -203,6 +201,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
@ -220,6 +219,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`)
@ -257,7 +257,7 @@ public class MyService
{
_chatClient = chatClient;
}
public async Task<string> GetResponseAsync(string prompt)
{
var response = await _chatClient.CompleteAsync(prompt);
@ -277,10 +277,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`
@ -315,6 +317,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
@ -339,6 +342,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:**
@ -399,7 +403,7 @@ public class MyService
var response = await _chatService.ChatCompletionsAsync(workspaceName, request);
return response.Content;
}
// For streaming responses
public async IAsyncEnumerable<string> StreamAIResponseAsync(string workspaceName, string prompt)
{
@ -426,6 +430,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)
@ -438,22 +443,168 @@ 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
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 |
| ------------------------- | ----------------- | -------------- | -------------- | ----------- | ----------------------------------------- |
| **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 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`
- `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 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:
```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`: 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`: Default streaming behavior. Can be overridden by user when `ShowStreamCheckbox` is true.
```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 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
{
WorkspaceName = "mylama",
ConversationId = "my-support-conversation-" + @CurrentUser.Id
})
```
#### JavaScript API
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
{
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);
});
| 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 |
// Remove event listeners
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
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.
@ -470,7 +621,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(
@ -550,24 +701,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
@ -604,6 +754,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
@ -672,6 +823,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.

11
framework/src/Volo.Abp.BlazoriseUI/Components/DataGridEntityActionsColumn.razor.cs

@ -23,7 +23,16 @@ public partial class DataGridEntityActionsColumn<TItem> : DataGridColumn<TItem>
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";
}
}

29
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<LocalReferenceConverter> Logger { get; set; }
public async Task ConvertAsync(
[NotNull] string directory,
[NotNull] List<string> 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;

45
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();
}
/// <summary>
/// Checks whether the property is nullable, including nullable reference types (NRT).
/// </summary>
/// <param name="propertyInfo">Property info to check</param>
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<Type, Func<object, byte[]?>> NullableGetterCache = new ();
private static Func<object, byte[]?> 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<Func<object, byte[]?>>(access, param).Compile();
}
}

2
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);

4
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<RequiredAttribute>().Any() || propertyInfo.GetCustomAttributesData().Any(attr => attr.AttributeType.Name == "RequiredMemberAttribute"),
IsNullable = ReflectionHelper.IsNullable(propertyInfo),
Minimum = customAttributes.OfType<RangeAttribute>().Select(x => x.Minimum).FirstOrDefault()?.ToString(),
Maximum = customAttributes.OfType<RangeAttribute>().Select(x => x.Maximum).FirstOrDefault()?.ToString(),
MinLength = customAttributes.OfType<MinLengthAttribute>().FirstOrDefault()?.Length ?? customAttributes.OfType<StringLengthAttribute>().FirstOrDefault()?.MinimumLength,

1
framework/test/Volo.Abp.Core.Tests/Volo.Abp.Core.Tests.csproj

@ -5,6 +5,7 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace />
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>

57
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
{

1
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 {

9
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[] {

Loading…
Cancel
Save