diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000000..b88c4e1588 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,270 @@ +# ABP Framework – Cursor Rules +# Scope: ABP Framework repository (abpframework/abp) — for developing ABP itself, not ABP-based applications. +# Goal: Enforce ABP module architecture best practices (DDD, layering, DB/ORM independence), +# maintain backward compatibility, ensure extensibility, and align with ABP contribution guidelines. + +## Global Defaults +- Follow existing patterns in this repository first. Before generating new code, search for similar implementations and mirror their structure, naming, and conventions. +- Prefer minimal, focused diffs. Avoid drive-by refactors and formatting churn. +- Preserve public APIs. Avoid breaking changes unless explicitly requested and justified. +- Keep layers clean. Do not introduce forbidden dependencies between packages. + +## Module / Package Architecture (Layering) +- Use a layered module structure with explicit dependencies: + - *.Domain.Shared: constants, enums, shared types safe for all layers and 3rd-party clients. MUST NOT contain entities, repositories, domain services, or business objects. + - *.Domain: entities/aggregate roots, repository interfaces, domain services. + - *.Application.Contracts: application service interfaces and DTOs. + - *.Application: application service implementations. + - *.EntityFrameworkCore / *.MongoDb: ORM integration packages depend on *.Domain only. MUST NOT depend on other layers. + - *.HttpApi: REST controllers. MUST depend ONLY on *.Application.Contracts (NOT *.Application). + - *.HttpApi.Client: remote client proxies. MUST depend ONLY on *.Application.Contracts. + - *.Web: UI. MUST depend ONLY on *.HttpApi. +- Enforce dependency direction: + - Web -> HttpApi -> Application.Contracts + - Application -> Domain + Application.Contracts + - Domain -> Domain.Shared + - ORM integration -> Domain +- Do not leak web concerns into application/domain. + +## Domain Layer – Entities & Aggregate Roots +- Define entities in the domain layer. +- Entities must be valid at creation: + - Provide a primary constructor that enforces invariants. + - Always include a protected parameterless constructor for ORMs. + - Always initialize sub-collections in the primary constructor. + - Do NOT generate Guid keys inside constructors; accept `id` and generate using `IGuidGenerator` from the calling code. +- Make members `virtual` where appropriate (ORM/proxy compatibility). +- Protect consistency: + - Use non-public setters (private/protected/internal) when needed. + - Provide meaningful domain methods for state transitions; prefer returning `this` from setters when applicable. +- Aggregate roots: + - Always use a single `Id` property. Do NOT use composite keys. + - Prefer `Guid` keys for aggregate roots. + - Inherit from `AggregateRoot` or audited base classes as required. +- Aggregate boundaries: + - Keep aggregates small. Avoid large sub-collections unless necessary. +- References: + - Reference other aggregate roots by Id only. + - Do NOT add navigation properties to other aggregate roots. + +## Repositories +- Define repository interfaces in the domain layer. +- Create one dedicated repository interface per aggregate root (e.g., `IProductRepository`). +- Public repository interfaces exposed by modules: + - SHOULD inherit from `IBasicRepository` (or `IReadOnlyRepository<...>` when suitable). + - SHOULD NOT expose `IQueryable` in the public contract. + - Internal implementations MAY use `IRepository` and `IQueryable` as needed. +- Do NOT define repositories for non-aggregate-root entities. +- Repository method conventions: + - All methods async. + - Include optional `CancellationToken cancellationToken = default` in every method. + - For single-entity returning methods: include `bool includeDetails = true`. + - For list returning methods: include `bool includeDetails = false`. + - Do NOT return composite projection classes like `UserWithRoles`. Use `includeDetails` for eager-loading. + - Avoid projection-only view models from repositories by default; only allow when performance is critical. + +## Domain Services +- Define domain services in the domain layer. +- Default: do NOT create interfaces for domain services unless necessary (mocking/multiple implementations). +- Naming: use `*Manager` suffix. +- Domain service methods: + - Focus on operations that enforce domain invariants and business rules. + - Query methods are acceptable when they encapsulate domain-specific lookup logic (e.g., normalized lookups, caching, complex resolution). Simple queries belong in repositories. + - Define methods that mutate state and enforce domain rules. + - Use specific, intention-revealing names (avoid generic `UpdateXAsync`). + - Accept valid domain objects as parameters; do NOT accept/return DTOs. + - On rule violations, throw `BusinessException` (or custom business exceptions). + - Use unique, namespaced error codes suitable for localization (e.g., `IssueTracking:ConcurrentOpenIssueLimit`). + - Do NOT depend on authenticated user logic; pass required values from application layer. + +## Application Services (Contracts + Implementation) +### Contracts +- Define one interface per application service in *.Application.Contracts. +- Interfaces must inherit from `IApplicationService`. +- Naming: `I*AppService`. +- Do NOT accept/return entities. Use DTOs and primitive parameters. + +### Method Naming & Shapes +- All service methods async and end with `Async`. +- Do not repeat entity names in method names (use `GetAsync`, not `GetProductAsync`). +- Standard CRUD: + - `GetAsync(Guid id)` returns a detailed DTO. + - `GetListAsync(QueryDto queryDto)` returns a list of detailed DTOs. + - `CreateAsync(CreateDto dto)` returns detailed DTO. + - `UpdateAsync(Guid id, UpdateDto dto)` returns detailed DTO (id MUST NOT be inside update DTO). + - `DeleteAsync(Guid id)` returns void/Task. +- `GetListAsync` query DTO: + - Filtering/sorting/paging fields optional with defaults. + - Enforce a maximum page size for performance. + +### DTO Usage +- Inputs: + - Do not include unused properties. + - Do NOT share input DTOs between methods. + - Do NOT use inheritance between input DTOs (except rare abstract base DTO cases; be very cautious). + +### Implementation +- Application layer must be independent of web. +- Implement interfaces in *.Application, name `ProductAppService` for `IProductAppService`. +- Inherit from `ApplicationService`. +- Make all public methods `virtual`. +- Avoid private helper methods; prefer `protected virtual` helpers for extensibility. +- Data access: + - Use dedicated repositories (e.g., `IProductRepository`). + - Do NOT use generic repositories. + - Do NOT put LINQ/SQL queries inside application service methods; repositories perform queries. +- Entity mutation: + - Load required entities from repositories. + - Mutate using domain methods. + - Call repository `UpdateAsync` after updates (do not assume change tracking). +- Extra properties: + - Use `MapExtraPropertiesTo` or configure object mapper for `MapExtraProperties`. +- Files: + - Do NOT use web types like `IFormFile` or `Stream` in application services. + - Controllers handle upload; pass `byte[]` (or similar) to application services. +- Cross-application-service calls: + - Do NOT call other application services within the same module. + - For reuse, push logic into domain layer or extract shared helpers carefully. + - You MAY call other modules’ application services only via their Application.Contracts. + +## DTO Conventions +- Define DTOs in *.Application.Contracts. +- Prefer ABP base DTO types (`EntityDto`, audited DTOs). +- For aggregate roots, prefer extensible DTO base types so extra properties can map. +- DTO properties: public getters/setters. +- Input DTO validation: + - Use data annotations. + - Reuse constants from Domain.Shared wherever possible. +- Avoid logic in DTOs; only implement `IValidatableObject` when necessary. +- Do NOT use `[Serializable]` attribute (BinaryFormatter is obsolete); ABP uses JSON serialization. +- Output DTO strategy: + - Prefer a Basic DTO and a Detailed DTO; avoid many variants. + - Detailed DTOs: include reference details as nested basic DTOs; avoid duplicating raw FK ids unnecessarily. + +## EF Core Integration +- Define a separate DbContext interface + class per module. +- Do NOT rely on lazy loading; do NOT enable lazy loading. +- DbContext interface: + - Inherit from `IEfCoreDbContext`. + - Add `[ConnectionStringName("...")]`. + - Expose `DbSet` ONLY for aggregate roots. + - Do NOT include setters in the interface. +- DbContext class: + - Inherit `AbpDbContext`. + - Add `[ConnectionStringName("...")]` and implement the interface. +- Table prefix/schema: + - Provide static `TablePrefix` and `Schema` defaulted from constants. + - Use short prefixes; `Abp` prefix reserved for ABP core modules. + - Default schema should be `null`. +- Model mapping: + - Do NOT configure entities directly inside `OnModelCreating`. + - Create `ModelBuilder` extension method `ConfigureX()` and call it. + - Call `b.ConfigureByConvention()` for each entity. +- Repository implementations: + - Inherit from `EfCoreRepository`. + - Use DbContext interface as generic parameter. + - Pass cancellation tokens using `GetCancellationToken(cancellationToken)`. + - Implement `IncludeDetails(include)` extension per aggregate root with sub-collections. + - Override `WithDetailsAsync()` where needed. + +## MongoDB Integration +- Define a separate MongoDbContext interface + class per module. +- MongoDbContext interface: + - Inherit from `IAbpMongoDbContext`. + - Add `[ConnectionStringName("...")]`. + - Expose `IMongoCollection` ONLY for aggregate roots. +- MongoDbContext class: + - Inherit `AbpMongoDbContext` and implement the interface. +- Collection prefix: + - Provide static `CollectionPrefix` defaulted from constants. + - Use short prefixes; `Abp` prefix reserved for ABP core modules. +- Mapping: + - Do NOT configure directly inside `CreateModel`. + - Create `IMongoModelBuilder` extension method `ConfigureX()` and call it. +- Repository implementations: + - Inherit from `MongoDbRepository`. + - Pass cancellation tokens using `GetCancellationToken(cancellationToken)`. + - Ignore `includeDetails` for MongoDB in most cases (documents load sub-collections). + - Prefer `GetQueryableAsync()` to ensure ABP data filters are applied. + +## ABP Module Classes +- Every package must have exactly one `AbpModule` class. +- Naming: `Abp[ModuleName][Layer]Module` (e.g., `AbpIdentityDomainModule`, `AbpIdentityApplicationModule`). +- Use `[DependsOn(typeof(...))]` to declare module dependencies explicitly. +- Override `ConfigureServices` for DI registration and configuration. +- Override `OnApplicationInitialization` sparingly; prefer `ConfigureServices` when possible. +- Each module must be usable standalone; avoid hidden cross-module coupling. + +## Framework Extensibility +- All public and protected members should be `virtual` for inheritance-based extensibility. +- Prefer `protected virtual` over `private` for helper methods to allow overriding. +- Use `[Dependency(ReplaceServices = true)]` patterns for services intended to be replaceable. +- Provide extension points via interfaces and virtual methods. +- Document extension points with XML comments explaining intended usage. +- Consider providing `*Options` classes for configuration-based extensibility. + +## Backward Compatibility +- Do NOT remove or rename public API members without a deprecation cycle. +- Use `[Obsolete("Message. Use X instead.")]` with clear migration guidance before removal. +- Maintain binary and source compatibility within major versions. +- Add new optional parameters with defaults; do not change existing method signatures. +- When adding new abstract members to base classes, provide default implementations if possible. +- Prefer adding new interfaces over modifying existing ones. + +## Localization Resources +- Define localization resources in Domain.Shared. +- Resource class naming: `[ModuleName]Resource` (e.g., `IdentityResource`, `PermissionManagementResource`). +- JSON files under `/Localization/[ModuleName]/` directory. +- Use `LocalizableString.Create("Key")` for localizable exceptions and messages. +- All user-facing strings must be localized; no hardcoded English text in code. +- Error codes should be namespaced: `ModuleName:ErrorCode` (e.g., `Identity:UserNameAlreadyExists`). + +## Settings & Features +- Define settings in `*SettingDefinitionProvider` in Domain.Shared or Domain. +- Setting names must follow `Abp.[ModuleName].[SettingName]` convention. +- Define features in `*FeatureDefinitionProvider` in Domain.Shared. +- Feature names must follow `[ModuleName].[FeatureName]` convention. +- Use constants for setting/feature names; never hardcode strings. + +## Permissions +- Define permissions in `*PermissionDefinitionProvider` in Application.Contracts. +- Permission names must follow `[ModuleName].[Permission]` convention. +- Use constants for permission names (e.g., `IdentityPermissions.Users.Create`). +- Group related permissions logically. + +## Event Bus & Distributed Events +- Use `ILocalEventBus` for intra-module communication within the same process. +- Use `IDistributedEventBus` for cross-module or cross-service communication. +- Define Event Transfer Objects (ETOs) in Domain.Shared for distributed events. +- ETO naming: `[EntityName][Action]Eto` (e.g., `UserCreatedEto`, `OrderCompletedEto`). +- Event handlers belong in the Application layer. +- ETOs should be simple, serializable, and contain only primitive types or nested ETOs. + +## Testing +- Unit tests: `*.Tests` projects for isolated logic testing with mocked dependencies. +- Integration tests: `*.EntityFrameworkCore.Tests` / `*.MongoDB.Tests` for repository and DB tests. +- Use `AbpIntegratedTest` or `AbpApplicationTestBase` base classes. +- Test modules should use `[DependsOn]` on the module under test. +- Use `Shouldly` assertions (ABP convention). +- Test both EF Core and MongoDB implementations when the module supports both. +- Include tests for permission checks, validation, and edge cases. +- Name test methods: `MethodName_Scenario_ExpectedResult` or `Should_ExpectedBehavior_When_Condition`. + +## Contribution Discipline (PR / Issues / Tests) +- Before significant changes, align via GitHub issue/discussion. +- PRs: + - Keep changes scoped and reviewable. + - Add/update unit/integration tests relevant to the change. + - Build and run tests for the impacted area when possible. +- Localization: + - Prefer the `abp translate` workflow for adding missing translations (generate `abp-translation.json`, fill, apply, then PR). + +## Review Checklist +- Layer dependencies respected (no forbidden references). +- No `IQueryable` or generic repository usage leaking into application/domain. +- Entities maintain invariants; Guid id generation not inside constructors. +- Repositories follow async + CancellationToken + includeDetails conventions. +- No web types in application services. +- DTOs in contracts, serializable, validated, minimal, no logic. +- EF/Mongo integration follows context + mapping + repository patterns. +- Minimal diff; no unnecessary API surface expansion. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..a754a2b5ea --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,372 @@ +# ABP Framework – GitHub Copilot Instructions + +> **Scope**: ABP Framework repository (abpframework/abp) — for developing ABP itself, not ABP-based applications. +> +> **Goal**: Enforce ABP module architecture best practices (DDD, layering, DB/ORM independence), maintain backward compatibility, ensure extensibility, and align with ABP contribution guidelines. + +--- + +## Global Defaults + +- Follow existing patterns in this repository first. Before generating new code, search for similar implementations and mirror their structure, naming, and conventions. +- Prefer minimal, focused diffs. Avoid drive-by refactors and formatting churn. +- Preserve public APIs. Avoid breaking changes unless explicitly requested and justified. +- Keep layers clean. Do not introduce forbidden dependencies between packages. + +--- + +## Module / Package Architecture (Layering) + +Use a layered module structure with explicit dependencies: + +| Layer | Purpose | Allowed Dependencies | +|-------|---------|---------------------| +| `*.Domain.Shared` | Constants, enums, shared types safe for all layers and 3rd-party clients. MUST NOT contain entities, repositories, domain services, or business objects. | None | +| `*.Domain` | Entities/aggregate roots, repository interfaces, domain services. | Domain.Shared | +| `*.Application.Contracts` | Application service interfaces and DTOs. | Domain.Shared | +| `*.Application` | Application service implementations. | Domain, Application.Contracts | +| `*.EntityFrameworkCore` / `*.MongoDb` | ORM integration packages. MUST NOT depend on other layers. | Domain only | +| `*.HttpApi` | REST controllers. MUST depend ONLY on Application.Contracts (NOT Application). | Application.Contracts | +| `*.HttpApi.Client` | Remote client proxies. MUST depend ONLY on Application.Contracts. | Application.Contracts | +| `*.Web` | UI layer. MUST depend ONLY on HttpApi. | HttpApi | + +### Dependency Direction +``` +Web -> HttpApi -> Application.Contracts +Application -> Domain + Application.Contracts +Domain -> Domain.Shared +ORM integration -> Domain +``` + +Do not leak web concerns into application/domain. + +--- + +## Domain Layer – Entities & Aggregate Roots + +- Define entities in the domain layer. +- Entities must be valid at creation: + - Provide a primary constructor that enforces invariants. + - Always include a `protected` parameterless constructor for ORMs. + - Always initialize sub-collections in the primary constructor. + - Do NOT generate Guid keys inside constructors; accept `id` and generate using `IGuidGenerator` from the calling code. +- Make members `virtual` where appropriate (ORM/proxy compatibility). +- Protect consistency: + - Use non-public setters (`private`/`protected`/`internal`) when needed. + - Provide meaningful domain methods for state transitions. + +### Aggregate Roots +- Always use a single `Id` property. Do NOT use composite keys. +- Prefer `Guid` keys for aggregate roots. +- Inherit from `AggregateRoot` or audited base classes as required. +- Keep aggregates small. Avoid large sub-collections unless necessary. + +### References +- Reference other aggregate roots by Id only. +- Do NOT add navigation properties to other aggregate roots. + +--- + +## Repositories + +- Define repository interfaces in the domain layer. +- Create one dedicated repository interface per aggregate root (e.g., `IProductRepository`). +- Public repository interfaces exposed by modules: + - SHOULD inherit from `IBasicRepository` (or `IReadOnlyRepository<...>` when suitable). + - SHOULD NOT expose `IQueryable` in the public contract. + - Internal implementations MAY use `IRepository` and `IQueryable` as needed. +- Do NOT define repositories for non-aggregate-root entities. + +### Method Conventions +- All methods async. +- Include optional `CancellationToken cancellationToken = default` in every method. +- For single-entity returning methods: include `bool includeDetails = true`. +- For list returning methods: include `bool includeDetails = false`. +- Do NOT return composite projection classes like `UserWithRoles`. Use `includeDetails` for eager-loading. +- Avoid projection-only view models from repositories by default; only allow when performance is critical. + +--- + +## Domain Services + +- Define domain services in the domain layer. +- Default: do NOT create interfaces for domain services unless necessary (mocking/multiple implementations). +- Naming: use `*Manager` suffix. + +### Method Guidelines +- Focus on operations that enforce domain invariants and business rules. +- Query methods are acceptable when they encapsulate domain-specific lookup logic (e.g., normalized lookups, caching, complex resolution). Simple queries belong in repositories. +- Define methods that mutate state and enforce domain rules. +- Use specific, intention-revealing names (avoid generic `UpdateXAsync`). +- Accept valid domain objects as parameters; do NOT accept/return DTOs. +- On rule violations, throw `BusinessException` (or custom business exceptions). +- Use unique, namespaced error codes suitable for localization (e.g., `IssueTracking:ConcurrentOpenIssueLimit`). +- Do NOT depend on authenticated user logic; pass required values from application layer. + +--- + +## Application Services + +### Contracts +- Define one interface per application service in `*.Application.Contracts`. +- Interfaces must inherit from `IApplicationService`. +- Naming: `I*AppService`. +- Do NOT accept/return entities. Use DTOs and primitive parameters. + +### Method Naming & Shapes +- All service methods async and end with `Async`. +- Do not repeat entity names in method names (use `GetAsync`, not `GetProductAsync`). + +**Standard CRUD:** +```csharp +Task GetAsync(Guid id); +Task> GetListAsync(GetProductListInput input); +Task CreateAsync(CreateProductInput input); +Task UpdateAsync(Guid id, UpdateProductInput input); // id NOT inside DTO +Task DeleteAsync(Guid id); +``` + +### DTO Usage (Inputs) +- Do not include unused properties. +- Do NOT share input DTOs between methods. +- Do NOT use inheritance between input DTOs (except rare abstract base DTO cases; be very cautious). + +### Implementation +- Application layer must be independent of web. +- Implement interfaces in `*.Application`, name `ProductAppService` for `IProductAppService`. +- Inherit from `ApplicationService`. +- Make all public methods `virtual`. +- Avoid private helper methods; prefer `protected virtual` helpers for extensibility. + +### Data Access +- Use dedicated repositories (e.g., `IProductRepository`). +- Do NOT put LINQ/SQL queries inside application service methods; repositories perform queries. + +### Entity Mutation +- Load required entities from repositories. +- Mutate using domain methods. +- Call repository `UpdateAsync` after updates (do not assume change tracking). + +### Files +- Do NOT use web types like `IFormFile` or `Stream` in application services. +- Controllers handle upload; pass `byte[]` (or similar) to application services. + +### Cross-Service Calls +- Do NOT call other application services within the same module. +- For reuse, push logic into domain layer or extract shared helpers carefully. +- You MAY call other modules' application services only via their Application.Contracts. + +--- + +## DTO Conventions + +- Define DTOs in `*.Application.Contracts`. +- Prefer ABP base DTO types (`EntityDto`, audited DTOs). +- For aggregate roots, prefer extensible DTO base types so extra properties can map. +- DTO properties: public getters/setters. + +### Input DTO Validation +- Use data annotations. +- Reuse constants from Domain.Shared wherever possible. + +### General Rules +- Avoid logic in DTOs; only implement `IValidatableObject` when necessary. +- Do NOT use `[Serializable]` attribute (BinaryFormatter is obsolete); ABP uses JSON serialization. + +### Output DTO Strategy +- Prefer a Basic DTO and a Detailed DTO; avoid many variants. +- Detailed DTOs: include reference details as nested basic DTOs; avoid duplicating raw FK ids unnecessarily. + +--- + +## EF Core Integration + +- Define a separate DbContext interface + class per module. +- Do NOT rely on lazy loading; do NOT enable lazy loading. + +### DbContext Interface +```csharp +[ConnectionStringName("ModuleName")] +public interface IModuleNameDbContext : IEfCoreDbContext +{ + DbSet Products { get; } // No setters, aggregate roots only +} +``` + +### DbContext Class +```csharp +[ConnectionStringName("ModuleName")] +public class ModuleNameDbContext : AbpDbContext, IModuleNameDbContext +{ + public static string TablePrefix { get; set; } = ModuleNameConsts.DefaultDbTablePrefix; + public static string? Schema { get; set; } = ModuleNameConsts.DefaultDbSchema; + + public DbSet Products { get; set; } +} +``` + +### Table Prefix/Schema +- Provide static `TablePrefix` and `Schema` defaulted from constants. +- Use short prefixes; `Abp` prefix reserved for ABP core modules. +- Default schema should be `null`. + +### Model Mapping +- Do NOT configure entities directly inside `OnModelCreating`. +- Create `ModelBuilder` extension method `ConfigureX()` and call it. +- Call `b.ConfigureByConvention()` for each entity. + +### Repository Implementations +- Inherit from `EfCoreRepository`. +- Use DbContext interface as generic parameter. +- Pass cancellation tokens using `GetCancellationToken(cancellationToken)`. +- Implement `IncludeDetails(include)` extension per aggregate root with sub-collections. +- Override `WithDetailsAsync()` where needed. + +--- + +## MongoDB Integration + +- Define a separate MongoDbContext interface + class per module. + +### MongoDbContext Interface +```csharp +[ConnectionStringName("ModuleName")] +public interface IModuleNameMongoDbContext : IAbpMongoDbContext +{ + IMongoCollection Products { get; } // Aggregate roots only +} +``` + +### MongoDbContext Class +```csharp +public class ModuleNameMongoDbContext : AbpMongoDbContext, IModuleNameMongoDbContext +{ + public static string CollectionPrefix { get; set; } = ModuleNameConsts.DefaultDbTablePrefix; +} +``` + +### Mapping +- Do NOT configure directly inside `CreateModel`. +- Create `IMongoModelBuilder` extension method `ConfigureX()` and call it. + +### Repository Implementations +- Inherit from `MongoDbRepository`. +- Pass cancellation tokens using `GetCancellationToken(cancellationToken)`. +- Ignore `includeDetails` for MongoDB in most cases (documents load sub-collections). +- Prefer `GetQueryableAsync()` to ensure ABP data filters are applied. + +--- + +## ABP Module Classes + +- Every package must have exactly one `AbpModule` class. +- Naming: `Abp[ModuleName][Layer]Module` (e.g., `AbpIdentityDomainModule`, `AbpIdentityApplicationModule`). +- Use `[DependsOn(typeof(...))]` to declare module dependencies explicitly. +- Override `ConfigureServices` for DI registration and configuration. +- Override `OnApplicationInitialization` sparingly; prefer `ConfigureServices` when possible. +- Each module must be usable standalone; avoid hidden cross-module coupling. + +--- + +## Framework Extensibility + +- All public and protected members should be `virtual` for inheritance-based extensibility. +- Prefer `protected virtual` over `private` for helper methods to allow overriding. +- Use `[Dependency(ReplaceServices = true)]` patterns for services intended to be replaceable. +- Provide extension points via interfaces and virtual methods. +- Document extension points with XML comments explaining intended usage. +- Consider providing `*Options` classes for configuration-based extensibility. + +--- + +## Backward Compatibility + +- Do NOT remove or rename public API members without a deprecation cycle. +- Use `[Obsolete("Message. Use X instead.")]` with clear migration guidance before removal. +- Maintain binary and source compatibility within major versions. +- Add new optional parameters with defaults; do not change existing method signatures. +- When adding new abstract members to base classes, provide default implementations if possible. +- Prefer adding new interfaces over modifying existing ones. + +--- + +## Localization Resources + +- Define localization resources in Domain.Shared. +- Resource class naming: `[ModuleName]Resource` (e.g., `IdentityResource`, `PermissionManagementResource`). +- JSON files under `/Localization/[ModuleName]/` directory. +- Use `LocalizableString.Create("Key")` for localizable exceptions and messages. +- All user-facing strings must be localized; no hardcoded English text in code. +- Error codes should be namespaced: `ModuleName:ErrorCode` (e.g., `Identity:UserNameAlreadyExists`). + +--- + +## Settings & Features + +- Define settings in `*SettingDefinitionProvider` in Domain.Shared or Domain. +- Setting names must follow `Abp.[ModuleName].[SettingName]` convention. +- Define features in `*FeatureDefinitionProvider` in Domain.Shared. +- Feature names must follow `[ModuleName].[FeatureName]` convention. +- Use constants for setting/feature names; never hardcode strings. + +--- + +## Permissions + +- Define permissions in `*PermissionDefinitionProvider` in Application.Contracts. +- Permission names must follow `[ModuleName].[Permission]` convention. +- Use constants for permission names (e.g., `IdentityPermissions.Users.Create`). +- Group related permissions logically. + +--- + +## Event Bus & Distributed Events + +- Use `ILocalEventBus` for intra-module communication within the same process. +- Use `IDistributedEventBus` for cross-module or cross-service communication. +- Define Event Transfer Objects (ETOs) in Domain.Shared for distributed events. +- ETO naming: `[EntityName][Action]Eto` (e.g., `UserCreatedEto`, `OrderCompletedEto`). +- Event handlers belong in the Application layer. +- ETOs should be simple, serializable, and contain only primitive types or nested ETOs. + +--- + +## Testing + +- Unit tests: `*.Tests` projects for isolated logic testing with mocked dependencies. +- Integration tests: `*.EntityFrameworkCore.Tests` / `*.MongoDB.Tests` for repository and DB tests. +- Use `AbpIntegratedTest` or `AbpApplicationTestBase` base classes. +- Test modules should use `[DependsOn]` on the module under test. +- Use `Shouldly` assertions (ABP convention). +- Test both EF Core and MongoDB implementations when the module supports both. +- Include tests for permission checks, validation, and edge cases. +- Name test methods: `MethodName_Scenario_ExpectedResult` or `Should_ExpectedBehavior_When_Condition`. + +--- + +## Contribution Discipline (PR / Issues / Tests) + +- Before significant changes, align via GitHub issue/discussion. + +### PRs +- Keep changes scoped and reviewable. +- Add/update unit/integration tests relevant to the change. +- Build and run tests for the impacted area when possible. + +### Localization +- Prefer the `abp translate` workflow for adding missing translations (generate `abp-translation.json`, fill, apply, then PR). + +--- + +## Review Checklist + +- [ ] Layer dependencies respected (no forbidden references). +- [ ] No `IQueryable` leaking into public repository contracts. +- [ ] Entities maintain invariants; Guid id generation not inside constructors. +- [ ] Repositories follow async + CancellationToken + includeDetails conventions. +- [ ] No web types in application services. +- [ ] DTOs in contracts, validated, minimal, no logic. +- [ ] EF/Mongo integration follows context + mapping + repository patterns. +- [ ] Public members are `virtual` for extensibility. +- [ ] Backward compatibility maintained; no breaking changes without deprecation. +- [ ] Minimal diff; no unnecessary API surface expansion. diff --git a/Directory.Packages.props b/Directory.Packages.props index fa951f8d7d..45b24e171e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -31,7 +31,7 @@ - + @@ -56,63 +56,63 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - + + + - + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -168,20 +168,20 @@ - + - - + + - - - + + + - + @@ -193,6 +193,6 @@ - + diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index 981500451a..d95d021836 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -1229,6 +1229,7 @@ "Pricing_Page_HurryUp": "Hurry Up!", "Pricing_Page_BuyLicense": "Buy a license at 2021 prices until January 16!", "Pricing_Page_ValidForExistingCustomers": "Also valid for existing customers and license renewals.", + "Pricing_Page_AdditionalDevCost": "The cost of an additional developer seat for the {0} License is {1}.", "Pricing_Page_Hint1": "The license price includes a certain number of developer seats. If you have more developers, you can always purchase additional seats.", "Pricing_Page_Hint2": "You can purchase more developer licenses now or in the future. Licenses are seat-based, so you can transfer a seat from one developer to another.", "Pricing_Page_Hint3": "You can develop an unlimited count of different products with your license.", diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/POST.md b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/POST.md new file mode 100644 index 0000000000..db18ecde7e --- /dev/null +++ b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/POST.md @@ -0,0 +1,342 @@ +# ABP Platform 10.1 RC Has Been Released + +We are happy to release [ABP](https://abp.io) version **10.1 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version. + +Try this version and provide feedback for a more stable version of ABP v10.1! Thanks to you in advance. + +## Get Started with the 10.1 RC + +You can check the [Get Started page](https://abp.io/get-started) to see how to get started with ABP. You can either download [ABP Studio](https://abp.io/get-started#abp-studio-tab) (**recommended**, if you prefer a user-friendly GUI application - desktop application) or use the [ABP CLI](https://abp.io/docs/latest/cli). + +By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI: + +![studio-switch-to-preview.png](studio-switch-to-preview.png) + +## Migration Guide + +There are a few breaking changes in this version that may affect your application. Please read the migration guide carefully, if you are upgrading from v10.0 or earlier: [ABP Version 10.1 Migration Guide](https://abp.io/docs/10.1/release-info/migration-guides/abp-10-1). + +## What's New with ABP v10.1? + +In this section, I will introduce some major features released in this version. +Here is a brief list of titles explained in the next sections: + +- Resource-Based Authorization +- Introducing the TickerQ Background Worker Provider +- Angular UI: Improving Authentication Token Handling +- Angular Version Upgrade to v21 +- File Management Module: Public File Sharing Support +- Payment Module: Public Page Implementation for Blazor & Angular UIs +- AI Management Module: Blazor & Angular UIs +- Identity PRO Module: Password History Support +- Account PRO Module: Introducing WebAuthn Passkeys + +### Resource-Based Authorization + +ABP v10.1 introduces **Resource-Based Authorization**, a powerful feature that enables fine-grained access control based on specific resource instances. This enhancement addresses a long-requested feature ([#236](https://github.com/abpframework/abp/issues/236)) that allows you to implement authorization logic that depends on the resource being accessed, not just static roles or permissions. + +**What is Resource-Based Authorization?** + +Unlike traditional permission-based authorization where you check if a user has a general permission (like "CanEditDocuments"), resource-based authorization allows you to make authorization decisions based on the specific resource instance. For example: + +- Allow users to edit only their own blog posts +- Grant access to documents based on ownership or sharing settings +- Implement complex authorization rules that depend on resource properties + +![](ai-management-demo.gif) + +#### How It Works? + +**1. Define resource permissions (`AddResourcePermission`)**: + +```csharp +public class MyPermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + //other permissions... + + context.AddResourcePermission( + name: BookManagementPermissions.Manage.Resources.Consume, + resourceName: BookManagementPermissions.Manage.Resources.Name, + managementPermissionName: BookManagementPermissions.Manage.ManagePermissions, + L("LocalizedPermissionDisplayName") + ); + } +} +``` + +**2. Use `IResourcePermissionChecker.IsGrantedAsync` in your code to perform the resource permission check**: + +```csharp +protected IResourcePermissionChecker ResourcePermissionChecker { get; } + +public async Task MyService() +{ + if(await ResourcePermissionChecker.IsGrantedAsync( + BookManagementPermissions.Manage.Resources.Consume, + BookManagementPermissions.Manage.Resources.Name, + workspaceConfiguration.WorkspaceId!.Value.ToString())) + { + return; + } + + //... +} +``` + +**3. Use the relevant `ResourcePermissionManagementModel` in your UI:** + +> The following code block demonstrates its usage in the Blazor UI, but the same component is also implemented for MVC & Angular UIs (however, component name might be different, please refer to the documentation before using the component). + +```xml + + +@code { + ResourcePermissionManagementModal PermissionManagementModal { get; set; } = null!; + + private Task OpenResourcePermissionModel() + { + await PermissionManagementModal.OpenAsync( + resourceName: BookManagementPermissions.Manage.Resources.Name, + resourceKey: entity.Id.ToString(), + resourceDisplayName: entity.Name + ); + } +} +``` + +This feature integrates perfectly with ABP's existing authorization infrastructure and provides a standard way to implement complex, context-aware authorization scenarios in your applications. + +### Introducing the TickerQ Background Worker Provider + +ABP v10.1 now includes **[TickerQ](https://tickerq.net/)** as a new background job and background worker provider option. TickerQ is a fast, reflection-free background task scheduler for .NET — built with source generators, EF Core integration, cron + time-based execution, and a real-time dashboard. It offers reliable job execution with built-in retry mechanisms, persistent job storage, and efficient resource usage. + +To use TickerQ in your ABP-based solution, refer to the following documentation: + +- [TickerQ Background Job Integration](https://abp.io/docs/10.1/framework/infrastructure/background-jobs/tickerq) +- [TickerQ Background Worker Integration](https://abp.io/docs/10.1/framework/infrastructure/background-workers/tickerq) + +### Angular UI: Improving Authentication Token Handling + +ABP v10.1 brings significant improvements to **Angular authentication token handling**, making token refresh more reliable and providing better error handling for expired or invalid tokens. + +#### What's Improved? + +Prior to this version, access tokens issued by the auth-server were stored in localStorage, making them vulnerable to XSS attacks. We've made the following enhancements to improve safety and reduce security risks: + +- Store sensitive tokens in memory +- Use web-workers for state sharing between tabs + +These enhancements are automatically available in new Angular projects and can be applied to existing projects by updating ABP packages. + +> See [#23930](https://github.com/abpframework/abp/issues/23930) for more details. + +### Angular Version Upgrade to v21 + +ABP v10.1 **upgrades Angular to version 21**, bringing the latest improvements and features from the Angular ecosystem to your ABP applications. We've upgraded the relevant core Angular packages and 3rd party packages such as **angular-oauth2-oidc** and **ng-bootstrap**. We will also update the ABP Studio templates along with the stable v10.1 release. + +> See [#24384](https://github.com/abpframework/abp/issues/24384) for the complete change list. + +### File Management Module: Public File Sharing Support + +_This is a **PRO** feature available for ABP Commercial customers._ + +The **File Management Module** now supports **public file sharing** via shareable links, similar to popular cloud storage services like Google Drive or Dropbox. This feature enables you to generate public URLs for files that can be accessed without authentication. + +![](file-sharing.gif) + +**Example Share URL:** + +```text +https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8AK%2BOEpCD... +``` + +**Configuration:** + +You can configure the public share domain through options: + +```csharp +Configure(options => +{ + options.FileDownloadRootUrl = "https://files.yourdomain.com"; +}); +``` + +This feature is available for all supported UI types (MVC, Angular, Blazor) and integrates seamlessly with the existing [File Management Module](https://abp.io/docs/latest/modules/file-management). + +### Payment Module: Public Page Implementation for Blazor & Angular UIs + +The **Payment Module** now includes **public page implementations for Angular and Blazor UIs**, completing UI coverage across all ABP-supported frameworks. Previously, public payment pages (payment gateway selection, pre-payment, and post-payment pages) were only available for MVC/Razor Pages UI. With this version, both admin and public pages are now available for MVC, Angular, and Blazor UIs. + +The public payment pages seamlessly integrate with ABP's [Payment Module](https://abp.io/docs/latest/modules/payment) and support all configured payment gateways. The documentation will be updated soon with detailed integration guides and examples at [abp.io/docs/latest/modules/payment](https://abp.io/docs/latest/modules/payment). + +### AI Management Module: Blazor & Angular UIs + +With this version, Angular and Blazor UIs for the [AI Management module](https://abp.io/docs/latest/modules/ai-management) have been implemented, completing the cross-platform support for this powerful AI integration module. + +![AI Management Workspaces](ai-management-workspaces.png) + +The AI Management Module builds on top of [ABP's AI Infrastructure](https://abp.io/docs/latest/framework/infrastructure/artificial-intelligence) and provides: + +- **Multi-Provider Support**: Integrate with OpenAI, Google Gemini, Anthropic Claude, and more from a unified API +- **Workspace-Based Organization**: Organize AI capabilities into separate workspaces for different use cases +- **Built-In Chat Interface**: Ready-to-use chat UI for conversational AI +- **Chat Widget**: Drop-in chat widget component for customer support or AI assistance +- **Resource-Based Permissions**: Control access to specific AI workspaces for users, roles, or clients + +Learn more about the AI Management Module in the [announcement post](https://abp.io/community/announcements/introducing-the-ai-management-module-nz9404a9) and [official documentation](https://abp.io/docs/latest/modules/ai-management). + +### Identity PRO Module: Password History Support + +The [**Identity PRO Module**](https://abp.io/docs/latest/modules/identity-pro) now includes **Password History** support, preventing users from reusing previous passwords. This security feature helps enforce stronger password policies and meet compliance requirements for your organization. + +Administrators can enable password reuse prevention by toggling the related setting on the _Administration -> Settings -> Identity Management_ page: + +![Password History Settings](password-history-settings.png) + +When changing a password, the system checks the specified number of previous passwords and displays an error message if the new password matches any of them: + +![](set-password-error-modal.png) + +![](reset-password-error-modal.png) + +### Account PRO Module: Introducing WebAuthn Passkeys + +ABP v10.1 introduces **Passkey authentication**, enabling passwordless sign-in using modern biometric authentication methods. Built on the **WebAuthn standard (FIDO2)**, this feature allows users to authenticate using Face ID, Touch ID, Windows Hello, Android biometrics, security keys, or other platform authenticators. + +**What are Passkeys?** + +Passkeys are a modern, phishing-resistant authentication method that replaces traditional passwords: + +- **Passwordless**: No passwords to remember, type, or manage +- **Secure**: Uses public/private key cryptography stored on the user's device +- **Convenient**: Sign in with a fingerprint, face scan, or device PIN +- **Cross-Platform**: Can sync across devices depending on platform support (Apple, Google, Microsoft) + +**How It Works:** + +**1. Enable or disable the WebAuthn passkeys feature in the _Settings -> Account -> Passkeys_ page:** + +![Passkey Setting](passkey-setting.png) + +**2. Add your passkeys in the _Account/Manage_ page:** + +![My Passkeys](my-passkey.png) + +![Passkey registration](passkey-registration.png) + +**3. Use the _Passkey login_ option for passwordless authentication the next time you log in:** + +![Passkey Login](passkey-login.png) + +> For more information, refer to the [Web Authentication API (WebAuthn) passkeys](https://abp.io/docs/10.1/modules/account/passkey) documentation. + +## Community News + +### Special Offer: Level Up Your ABP Skills with 33% Off Live Trainings! + +![ABP Live Training Discount](./live-training-discount.png) + +We're excited to announce a special limited-time offer for developers looking to master the ABP Platform! Get **33% OFF** on all ABP live training sessions and accelerate your learning journey with hands-on guidance from ABP experts. + +**Why Join ABP Live Trainings?** + +Our live training sessions provide an immersive learning experience where you can: + +- **Learn from the Experts**: Get direct instruction from ABP team members and experienced trainers who know the platform inside and out. +- **Hands-On Practice**: Work through real-world scenarios and build actual applications during the sessions. +- **Interactive Q&A**: Ask questions in real-time and get immediate answers to your specific challenges. +- **Comprehensive Coverage**: From fundamentals to advanced topics, our trainings cover everything you need to build production-ready applications with ABP. +- **Certificate of Completion**: Receive a certificate upon completing the training to showcase your ABP expertise. + +Don't miss this opportunity to invest in your skills and career. Whether you're new to ABP or looking to advance your expertise, our live trainings provide the structured learning path you need to succeed. + +> 👉 [Learn more and claim your discount here](https://abp.io/community/announcements/improve-your-abp-skills-with-33-off-live-trainings-hjnw57xu) + +### Introducing the ABP Referral Program + +![ABP.IO Referral Program](./referral-program.png) + +We're thrilled to announce the launch of the **ABP.IO Referral Program**, a new way for our community members to earn rewards while helping others discover the ABP Platform! + +**How It Works:** + +ABP's Referral Program is simple and rewarding: + +1. **Get Your Unique Referral Link**: Sign up for the program and receive your personalized referral link. +2. **Share with Your Network**: Share your link with colleagues, friends, and fellow developers who could benefit from ABP. +3. **Earn Rewards**: When someone purchases an ABP Commercial license through your referral link, **you earn 5% commission**! + +By joining the referral program, you're not just earning rewards and also you're helping other developers discover a platform that can significantly improve their productivity and project success. + +> 👉 [Join the ABP.IO Referral Program](https://abp.io/community/announcements/introducing-abp.io-referral-program-b59obhe7) + +### Announcing AI Management Module + +We are excited to announce the [AI Management Module](https://abp.io/docs/10.0/modules/ai-management), a powerful new module to the ABP Platform that makes managing AI capabilities in your applications easier than ever! + +![ABP - AI Management Module Workspaces](ai-management-workspaces.png) + +**What is the AI Management Module?** + +Built on top of the [ABP Framework's AI infrastructure](https://abp.io/docs/latest/framework/infrastructure/artificial-intelligence), the **AI Management Module** allows you to manage AI workspaces dynamically without touching your code. Whether you're building a customer support chatbot, adding AI-powered search, or creating intelligent automation workflows, this module provides everything you need to manage AI integrations through a user-friendly interface. + +**Key Features:** + +- **Multi-Provider Support**: Allows integrating with multiple AI providers including OpenAI, Google Gemini, Anthropic Claude, and more from a single unified API. +- **Buit-In Chat Interface** +- **Ready to Use Chat Widget** +- and more... (RAG & MCP supports are on the way!) + +👉 [Read the announcement post for more...](https://abp.io/community/announcements/introducing-the-ai-management-module-nz9404a9) + +### We Were At .NET Conf China 2025! + +![.NET Conf China 2025](./dotnet-conf-china-2025.png) + +The ABP team participated in **.NET Conf China 2025** in Shanghai, celebrating the release of .NET 10 (LTS) and the achievements of the .NET community in China. + +**Event Highlights:** + +The conference brought together hundereds of developers and featured Scott Hanselman's opening keynote announcing .NET 10's availability, focused on four pillars: AI, cloud-native, cross-platform, and performance. The event covered three main themes: performance improvements, AI integration, and cross-platform development, with in-depth sessions on topics ranging from Avalonia and Blazor to AI agents and enterprise adoption. + +**ABP's Participation:** + +At the ABP booth, we showcased our developer platform with live demonstrations of modular architecture, multi-tenancy support, and built-in authentication systems. We hosted interactive raffles with prizes including ABP stickers, the _Mastering ABP Framework_ book, and Bluetooth headphones. The booth was a hub for sharing experiences, impromptu code walkthroughs, and meaningful conversations with Chinese developers about ABP's future. + +> 👉 [Read the full event recap](https://abp.io/community/announcements/.net-conf-china-2025-fz03gfge) + +### Community Talks 2025.10: AI-Powered .NET Apps with ABP & Microsoft Agent Framework + +![ABP Community Talks - AI-Powered .NET Apps](./community-talk-2025-10-ai.png) + +In our latest ABP Community Talks session, we dove deep into the world of **Artificial Intelligence** and its integration with the ABP Framework. This session explored Microsoft's cutting-edge AI libraries: **Extensions AI**, **Semantic Kernel**, and the **Microsoft Agent Framework**. + +**What We Covered:** + +We introduced the new **AI Management Module**, discussing its current status and roadmap. The session included practical demonstrations on building intelligent applications with the Microsoft Agent Framework within ABP projects, showing how these technologies empower developers to create AI-powered .NET applications. + +> 👉 [Missed the live session? Click here to watch the full session](https://www.youtube.com/live/tEcd2H6yXQk) + +### New ABP Community Articles + +There are exciting articles contributed by the ABP community as always. I will highlight some of them here: + +- [Salih Özkara](https://github.com/salihozkara) has published 3 new articles: + - [Building Dynamic XML Sitemaps with ABP Framework](https://abp.io/community/articles/building-dynamic-xml-sitemaps-with-abp-framework-n3q6schd) + - [Implement Automatic Method-Level Caching in ABP Framework](https://abp.io/community/articles/implement-automatic-methodlevel-caching-in-abp-framework-4uzd3wx8) + - [Building Production-Ready LLM Applications with .NET: A Practical Guide](https://abp.io/community/articles/building-production-ready-llm-applications-with-net-ya7qemfa) +- [Adnan Ali](https://abp.io/community/members/adnanaldaim) has published 2 new articles: + - [Integrating AI into ABP.IO Applications: The Complete Guide to Volo.Abp.AI and AI Management Module](https://abp.io/community/articles/integrating-ai-into-abp.io-applications-the-complete-guide-jc9fbjq0) + - [How ABP.IO Framework Cuts Your MVP Development Time by 60%](https://abp.io/community/articles/how-abp.io-framework-cuts-your-mvp-development-time-by-60-8l7m3ugj) +- [My First Look and Experience with Google AntiGravity](https://abp.io/community/articles/my-first-look-and-experience-with-google-antigravity-0hr4sjtf) by [Alper Ebiçoğlu](https://twitter.com/alperebicoglu) +- [TOON vs JSON for LLM Prompts in ABP: Token-Efficient Structured Context](https://abp.io/community/articles/toon-vs-json-b4rn2avd) by [Suhaib Mousa](https://abp.io/community/members/suhaib-mousa) + +Thanks to the ABP Community for all the content they have published. You can also [post your ABP-related (text or video) content](https://abp.io/community/posts/create) to the ABP Community. + +## Conclusion + +This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://abp.io/docs/10.1/release-info/road-map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v10.1 RC and provide feedback to help us release a more stable version. + +Thanks for being a part of this community! \ No newline at end of file diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/ai-management-demo.gif b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/ai-management-demo.gif new file mode 100644 index 0000000000..7b1a7f54cc Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/ai-management-demo.gif differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/ai-management-workspaces.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/ai-management-workspaces.png new file mode 100644 index 0000000000..b8924e5045 Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/ai-management-workspaces.png differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/community-talk-2025-10-ai.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/community-talk-2025-10-ai.png new file mode 100644 index 0000000000..c370086279 Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/community-talk-2025-10-ai.png differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/cover-image.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/cover-image.png new file mode 100644 index 0000000000..b2d4353f40 Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/cover-image.png differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/dotnet-conf-china-2025.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/dotnet-conf-china-2025.png new file mode 100644 index 0000000000..0e502dd8d7 Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/dotnet-conf-china-2025.png differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/file-sharing.gif b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/file-sharing.gif new file mode 100644 index 0000000000..c959b4aad2 Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/file-sharing.gif differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/live-training-discount.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/live-training-discount.png new file mode 100644 index 0000000000..1b11e0efab Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/live-training-discount.png differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/my-passkey.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/my-passkey.png new file mode 100644 index 0000000000..31a3b6e362 Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/my-passkey.png differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-login.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-login.png new file mode 100644 index 0000000000..b1114e4c50 Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-login.png differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-registration.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-registration.png new file mode 100644 index 0000000000..a009b746cf Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-registration.png differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-setting.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-setting.png new file mode 100644 index 0000000000..3c42236f8b Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-setting.png differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/password-history-settings.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/password-history-settings.png new file mode 100644 index 0000000000..34ec453cbe Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/password-history-settings.png differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/password-history-warning.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/password-history-warning.png new file mode 100644 index 0000000000..13a5a64753 Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/password-history-warning.png differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/referral-program.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/referral-program.png new file mode 100644 index 0000000000..f6db6b1dc4 Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/referral-program.png differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/reset-password-error-modal.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/reset-password-error-modal.png new file mode 100644 index 0000000000..13a5a64753 Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/reset-password-error-modal.png differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/set-password-error-modal.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/set-password-error-modal.png new file mode 100644 index 0000000000..595d3b12eb Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/set-password-error-modal.png differ diff --git a/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/studio-switch-to-preview.png b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/studio-switch-to-preview.png new file mode 100644 index 0000000000..7025b92cce Binary files /dev/null and b/docs/en/Blog-Posts/2026-01-08 v10_1_Preview/studio-switch-to-preview.png differ diff --git a/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/post.md b/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/post.md index 80c4a8fbeb..4eb82e0f9e 100644 --- a/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/post.md +++ b/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/post.md @@ -76,7 +76,7 @@ Installation is straightforward using the [ABP Studio](https://abp.io/studio). Y - Client Components - Integration to Startup Templates -### v10.1 +### v10.1 ✅ - Blazor UI - Angular UI - Resource based authorization on Workspaces @@ -103,4 +103,4 @@ The AI Management Module is available now for ABP Team and higher license holder --- -*The AI Management Module is currently in preview. We're excited to hear your feedback as we continue to improve and add new features!* \ No newline at end of file +*The AI Management Module is currently in preview. We're excited to hear your feedback as we continue to improve and add new features!* diff --git a/docs/en/Community-Articles/2026-01-11/article.md b/docs/en/Community-Articles/2026-01-11/article.md new file mode 100644 index 0000000000..2f1f25db45 --- /dev/null +++ b/docs/en/Community-Articles/2026-01-11/article.md @@ -0,0 +1,170 @@ +# Async Chain of Persistence Pattern: Designing for Failure in Event-Driven Systems + +## Introduction +Messages can get lost while being processed when you use asynchronous messaging or event handling. +The Async Chain of Persistence Pattern makes sure that no message is ever lost from the system by making sure that the message is always stored at every step of the workflow. + +## The Fundamental Principle of Pattern +The Async Chain of Persistence Pattern guarantees that no message is ever lost by ensuring that the message is always persistently stored at every step of the workflow. This is where the pattern gets its name. A message cannot be removed from its previous location until it is confirmed to be persistently stored in the subsequent stages of the chain. + +It is commonly used in event-driven systems and message-driven systems. + +### Event-Driven versus Message-Driven Systems +To understand the pattern, it's important to know the differences between events and messages. + +#### The Core Difference + +**Event:** Says "something happened", describes the past. Example: `OrderPlaced`, `PaymentCompleted` + +**Message:** Says "do this", commands for the future. Example: `CreateOrder`, `SendEmail` + +| Property | Event | Message | +|----------|-------|---------| +| **Coupling** | Loose - no one knows who's listening | Tighter - there's a specific receiver | +| **Publishing** | Pub/Sub - 0-N services listen | Point-to-Point - usually 1 service | +| **Tense** | Past tense, immutable | Present/future tense | +| **Error Handling** | If one consumer fails, others continue | If not processed, system breaks | + + +### Relationship with Async Chain of Persistence + +**In event-driven systems:** Each service receives the event → persists it → publishes a new event + +![Event-Driven Systems](event-driven-systems.png) + +**In message-driven systems:** At each step, the message is kept safe on queue + disk + +In both systems, the goal is the same: no message should be lost! + +![Message-Driven Systems](message-driven-systems.png) + +--- + +## When Do Messages Get Lost? +There are 3 main scenarios for message loss: + +### 1. While Processing a Message (Receiving a Message) +While processing a message in a reply, by default, there is an automatic acknowledgment and subsequent deletion of a message that is in a queue. But in cases where there is a fatal or unrecoverable error in the message processing operation or a service instance crashes, this message gets lost. + +### 2. Message Broker Crashes +In the majority of message brokers, the default option for the message persistence state is set to nonpersisting. In other words, the message will be held in the memory of the message broker without any persistence. It has been used for the purpose of obtaining rapid responses and improved throughput. However, in the event of failure of the message broker process, the nonpersistent message will be lost permanently. + +### 3. Event Chaining +In event-driven systems, an event is published as a derived message after an operation is done by a service. Message loss in two ways is possible in this scenario: +1. **Risk of Asynchronous Send:** The publish operation is often carried out using the asynchronous send feature. When a fatal error takes place prior to the receipt of acknowledgment of the message publish operation by the publish services, it becomes difficult to determine if the message has been successfully transmitted to the message broker. +2. **Transaction Coordination:** In case an error is detected after the commit of the database but before the derived event is published, the derived event might be lost. + +## Implementing the Pattern: 4 Critical Steps +Four critical steps are required to implement the Async Chain of Persistence Pattern: + +### 1. Message Persistence +The initial step to ensure that there are no lost messages is to specify the messages as PERSISTENT: When the message broker receives the message, it is saved on disk. + +```javascript +var delivery_mode = PERSISTENT +var producer = create_producer(delivery_mode) + +// All sent messages are persisted on the message broker +producer.send_message(APPLY_PAYMENT) + +// Alternatively +var delivery_mode = PERSISTENT +var producer = create_producer() +producer.send_message(APPLY_PAYMENT, delivery_mode) +``` +With this approach, even if the message broker crashes, all messages will still be there when it comes back up. + +### 2. Client Acknowledgement Mode +Instead, client-acknowledgement mode needs to be employed. When a message is received in this mode, that message will be retained in the queue until a proper acknowledgment from the processing service has been made. Instead of "auto acknowledge" mode, "client-acknowledgement" mode + +Client acknowledgement mode ensures that the message is not lost while processing by the service. When a fatal error is detected during message processing, the service exits without sending an acknowledgement message and thus results in a message redispatched. + +**Important Note:** The message has to be acknowledged while the message processing operation is completed so that a repeat message will not be processed. + +### 3. Synchronous Send +The synchronous send must be preferred over the asynchronous send. + +Although it takes longer to send via synchronous send, since it's a blocking call, it ensures that the message broker received and persisted the message on disk. + +```javascript +// Blocking call to publish the derived event +var ack = publish_event(PAYMENT_APPLIED) +if (ack not_successful) { + retry_or_persist(PAYMENT_APPLIED) +} +``` +With this approach, the risk of message loss during event chaining is eliminated. + +### 4. Last Participant Support +This is the most complex step of the Async Chain of Persistence pattern. It determines when the message should be acknowledged. + +#### For Message-Driven Systems: +```javascript +var message = receive_message() +process_message(message) +database.commit() +message.acknowledge() +``` +Recommended order: **commit first, ack last.** Otherwise, if the database operation fails, the message will be lost because it has already been removed from the queue. + +#### For Event-Driven Systems +In event-driven systems, there are two distinct parties involved: one that acknowledges an event and another that publishes the event that’s been derived. The party that publishes the event that’s been derived should be treated as “last participant.” +```javascript +var event = receive_event() +process_event(event) +database.commit() +message.acknowledge() + +var ack = publish_event(PAYMENT_APPLIED) +if (ack not_successful) { + retry_or_persist(PAYMENT_APPLIED) +} +``` +In this sequence, the original event is acknowledged after it is completed. If publishing the derived event fails, it can be retried or persisted for later delivery. + +--- + +## Trade-offs + +### Advantages + +**Preventing Message Loss:** +The major benefit of this pattern is that it doesn't let a message get lost while messages are under processing. This issue, being a serious concern in asynchronous systems, is ruled out due to the pattern. + +### Disadvantages + +#### 1. Possible Duplicate Messages +Enabling the client-acknowledgement mode can result in the processing of the same message multiple times. If the service instance fails after the database commit but before the message could be acknowledged, the message will be repeated and duplicates can be processed. + +**Solution:** In order to determine whether the arriving messages are previously processed or not, the message IDs can be logged and traced back. This also has the drawback of requiring one extra read operation per message in the system. + +#### 2. Performance and Throughput +It has also been noted that the time duration in which the persistent message takes to be sent from the message broker could be **up to four times longer** than in the nonpersistent message transmission. + +Persisted messages also affect performance in terms of sending and reading the message. It is also common for message brokers to maintain message data in their memory for efficient reading, but there is no assurance that messages will always be resident in memory, depending on memory size, among other factors. + +#### 3. Impact of Synchronous Send +Because a synchronous send makes a blocking call until a confirmation is received, there is no other work that can be accomplished before a confirmation is received from the broker for a synchronous send. A persisted message makes this even more apparent. + +#### 4. Overall Scalability +This is because, upon receipt, the message broker has to spend more time persisting messages to disk, thus negatively affecting scalability. Persistent messages always give lower total throughput, which can limit scalability under high usage loads and high message volumes. + +--- + +## Conclusion + +The Async Chain of Persistence Pattern provides a powerful solution for preventing message loss. Although it has negative effects on performance, throughput, and scalability, these trade-offs are generally acceptable in systems where data loss is critical. + +Before implementing the pattern, carefully analyze your system requirements: + +- **How critical is message loss?** +- **What are the performance and throughput requirements?** +- **How should the system behave in case of duplicate processing?** + +The answers to these questions will help you determine whether the Async Chain of Persistence Pattern is suitable for your system. + +## Sample Project + +To see an example project where this pattern is implemented, you can check out the repository: + +🔗 **[GitHub Repository](https://github.com/fahrigedik/SoftwareArchitecturePatterns)** diff --git a/docs/en/Community-Articles/2026-01-11/event-driven-systems.png b/docs/en/Community-Articles/2026-01-11/event-driven-systems.png new file mode 100644 index 0000000000..b593ceceed Binary files /dev/null and b/docs/en/Community-Articles/2026-01-11/event-driven-systems.png differ diff --git a/docs/en/Community-Articles/2026-01-11/message-driven-systems.png b/docs/en/Community-Articles/2026-01-11/message-driven-systems.png new file mode 100644 index 0000000000..9424aa2974 Binary files /dev/null and b/docs/en/Community-Articles/2026-01-11/message-driven-systems.png differ diff --git a/docs/en/Community-Articles/2026-01-16-meet-abio-at-ndc-london/post.md b/docs/en/Community-Articles/2026-01-16-meet-abio-at-ndc-london/post.md new file mode 100644 index 0000000000..4890b42174 --- /dev/null +++ b/docs/en/Community-Articles/2026-01-16-meet-abio-at-ndc-london/post.md @@ -0,0 +1,17 @@ +We are thrilled to announce that **ABP.IO will be sponsoring [NDC London 2026](https://ndclondon.com/),** making the start of 2026 a very exciting time for us\! + +NDC London is going to take place from **26th-30th January 2026 at Queen Elizabeth II Center.** This 5-Day event for software developers will have over 90 speakers and 100 sessions. We are excited to be a part of this amazing event once more as devoted supporters of the software development community\! + +## Conference Tracks, Topics, and What Developers Can Expect + +Developers attending **NDC London 2026** can expect five focused tracks packed with practical, real-world sessions. The conference covers the modern development stack, including **.NET, JavaScript, Cloud, DevOps, Security, Testing, UX, Web**, and emerging technologies, delivered by industry experts with actionable insights developers can apply immediately. + +## Discover Previous NDC Events + +We have shared **our takeaways from past NDC events** and other conferences [**here**](https://abp.io/community/events/sponsored#gsc.tab=0). You can check them out to learn what we discovered along the way\! + +## Stop By Our Booth and Say Hello + +We can’t wait to meet fellow developers at NDC London 2026, have meaningful conversations and connect in person. **If you are stopping by our booth, don’t miss our raffle\!** We will be giving away a nice surprise during the event\! + +We are looking forward to meeting you there and sharing a few great days focused on software development. See you there\! diff --git a/docs/en/framework/architecture/microservices/index.md b/docs/en/framework/architecture/microservices/index.md index c014190437..9d8643eedf 100644 --- a/docs/en/framework/architecture/microservices/index.md +++ b/docs/en/framework/architecture/microservices/index.md @@ -7,6 +7,16 @@ # Microservice Architecture +````json +//[doc-nav] +{ + "Next": { + "Name": "Microservice Solution Template", + "Path": "solution-templates/microservice/index" + } +} +```` + *"Microservices are a software development technique—a variant of the **service-oriented architecture** (SOA) architectural style that structures an application as a collection of **loosely coupled services**. In a microservices architecture, services are **fine-grained** and the protocols are **lightweight**. The benefit of decomposing an application into different smaller services is that it improves **modularity**. This makes the application easier to understand, develop, test, and become more resilient to architecture erosion. It **parallelizes development** by enabling small autonomous teams to **develop, deploy and scale** their respective services independently. It also allows the architecture of an individual service to emerge through **continuous refactoring**. Microservices-based architectures enable **continuous delivery and deployment**."* — [Wikipedia](https://en.wikipedia.org/wiki/Microservices) @@ -24,11 +34,49 @@ One of the major goals of the ABP is to provide a convenient infrastructure to c * Provides a [distributed event bus](../../infrastructure/event-bus) to communicate your services. * Provides many other services to make your daily development easier. +## ABP Studio for Microservice Development + +[ABP Studio](../../../studio/overview.md) is a comprehensive desktop application that significantly simplifies microservice solution development and management. It provides powerful tools specifically designed for distributed systems: + +### Solution Runner + +The [Solution Runner](../../../studio/running-applications.md) allows you to run all your microservices with a single click. You can create different profiles to organize services based on your team's needs. For example, `team-1` might only need to run the *Administration* and *Identity* services, while `team-2` works with *SaaS* and *Audit Logging* services. This approach saves resources and speeds up development by allowing each team to run only the services they need. + +### Kubernetes Integration + +The [Kubernetes Integration](../../../studio/kubernetes.md) panel enables you to deploy your microservices to a Kubernetes cluster and manage them directly from ABP Studio. Key features include: + +* **Deploy to Kubernetes**: Build Docker images and install Helm charts with a few clicks. +* **Intercept Services**: Debug and develop specific services locally while the rest of the system runs in Kubernetes. This eliminates the need to run all microservices on your local machine. +* **Redeploy Charts**: Quickly redeploy individual services after making changes. +* **Connect to Cluster Resources**: Access databases, message queues, and other infrastructure services running in the cluster. + +### Application Monitoring + +The [Application Monitoring](../../../studio/monitoring-applications.md) area provides a centralized view of all your running microservices: + +* **HTTP Requests**: View all HTTP requests across services with detailed information including headers, payloads, and response times. +* **Distributed Events**: Monitor all distributed events sent and received by your services, making it easy to debug inter-service communication. +* **Exceptions**: Track exceptions thrown by any service in real-time. +* **Logs**: Access logs from all services in a single place with filtering capabilities. +* **Built-in Browser**: Browse and test your APIs without leaving ABP Studio. + +### Creating New Microservices + +ABP Studio's [Solution Explorer](../../../studio/solution-explorer.md) makes it easy to [add new microservices to your solution](../../../solution-templates/microservice/adding-new-microservices). Right-click on the `services` folder and select *Add* -> *New Module* -> *Microservice*. ABP Studio will: + +* Create the microservice with proper project structure. +* Configure database connections and migrations. +* Set up authentication and authorization. +* Integrate with API gateways. +* Configure distributed event bus connections. +* Add the service to Kubernetes Helm charts. + ## Microservice for New Applications -One common advise to start a new solution is **always to start with a monolith**, keep it modular and split into microservices once the monolith becomes a problem. This makes your progress fast in the beginning especially if your team is small and you don't want to deal with challenges of the microservice architecture. +One common advice to start a new solution is **always to start with a monolith**, keep it modular and split into microservices once the monolith becomes a problem. This makes your progress fast in the beginning especially if your team is small and you don't want to deal with challenges of the microservice architecture. -However, developing such a well-modular application can be a problem since it is **hard to keep modules isolated** from each other as you would do it for microservices (see [Stefan Tilkov's article](https://martinfowler.com/articles/dont-start-monolith.html) about that). Microservice architecture naturally forces you to develop well isolated services, but in a modular monolithic application it's easy to tight couple modules to each other and design **weak module boundaries** and API contracts. +However, developing such a well-modular application can be a problem since it is **hard to keep modules isolated** from each other as you would do it for microservices (see [Stefan Tilkov's article](https://martinfowler.com/articles/dont-start-monolith.html) about that). Microservice architecture naturally forces you to develop well isolated services, but in a modular monolithic application it's easy to tightly couple modules to each other and design **weak module boundaries** and API contracts. ABP can help you in that point by offering a **microservice-compatible, strict module architecture** where your module is split into multiple layers/projects and developed in its own VS solution completely isolated and independent from other modules. Such a developed module is a natural microservice yet it can be easily plugged-in a monolithic application. See the [module development best practice guide](../best-practices) that offers a **microservice-first module design**. All [standard ABP modules](https://github.com/abpframework/abp/tree/master/modules) are developed based on this guide. So, you can use these modules by embedding into your monolithic solution or deploy them separately and use via remote APIs. They can share a single database or can have their own database based on your simple configuration. @@ -37,3 +85,20 @@ ABP can help you in that point by offering a **microservice-compatible, strict m ABP provides a pre-architected and production-ready microservice solution template that includes multiple services, API gateways and applications well integrated with each other. This template helps you quickly start building distributed systems with common microservice patterns. See the [Microservice Solution Template](../../../solution-templates/microservice/index.md) documentation for details. + +## Tutorials + +For a hands-on experience, follow the [Microservice Development Tutorial](../../../tutorials/microservice/index.md) that guides you through: + +* Creating the initial microservice solution +* Adding new microservices (Catalog and Ordering services) +* Building CRUD functionality +* Implementing HTTP API calls between services +* Using distributed events for asynchronous communication + +## See Also + +* [Get Started: Microservice Solution](../../../get-started/microservice.md) +* [Microservice Solution Template](../../../solution-templates/microservice/index.md) +* [Microservice Development Tutorial](../../../tutorials/microservice/index.md) +* [ABP Studio Overview](../../../studio/overview.md) diff --git a/docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md b/docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md index b44572838a..6edcee78f1 100644 --- a/docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md +++ b/docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md @@ -1,3 +1,10 @@ +```json +//[doc-seo] +{ + "Description": "Explore how to integrate AI services into your ABP Framework applications using the Microsoft.Extensions.AI library for seamless functionality." +} +``` + # Microsoft.Extensions.AI [Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai) is a library that provides a unified API for integrating AI services. It is a part of the Microsoft AI Extensions Library. It is used to integrate AI services into your application. This documentation is about the usage of this library with ABP Framework. Make sure you have read the [Artificial Intelligence](./index.md) documentation before reading this documentation. @@ -92,7 +99,7 @@ Example of resolving a typed chat client accessor: public class MyService { private readonly IChatClientAccessor _chatClientAccessor; -} + public async Task GetResponseAsync(string prompt) { var chatClient = _chatClientAccessor.ChatClient; @@ -174,4 +181,4 @@ public class MyProjectModule : AbpModule - [Usage of Agent Framework](./microsoft-agent-framework.md) - [Usage of Semantic Kernel](./microsoft-semantic-kernel.md) -- [AI Samples for .NET](https://learn.microsoft.com/en-us/samples/dotnet/ai-samples/ai-samples/) \ No newline at end of file +- [AI Samples for .NET](https://learn.microsoft.com/en-us/samples/dotnet/ai-samples/ai-samples/) diff --git a/docs/en/framework/ui/angular/checkbox-component.md b/docs/en/framework/ui/angular/checkbox-component.md index 2e5cc44ad3..fa3824027d 100644 --- a/docs/en/framework/ui/angular/checkbox-component.md +++ b/docs/en/framework/ui/angular/checkbox-component.md @@ -25,26 +25,21 @@ The ABP Checkbox Component is a reusable form input component for the checkbox t # Usage -The ABP Checkbox component is a part of the `ThemeSharedModule` module. If you've imported that module into your module, there's no need to import it again. If not, then first import it as shown below: +The ABP Checkbox component (`AbpCheckboxComponent`) is a standalone component. You can import it directly in your component: ```ts -// my-feature.module.ts - -import { ThemeSharedModule } from "@abp/ng.theme.shared"; -import { CheckboxDemoComponent } from "./CheckboxDemoComponent.component"; - -@NgModule({ - imports: [ - ThemeSharedModule, - // ... - ], - declarations: [CheckboxDemoComponent], - // ... +import { Component } from "@angular/core"; +import { AbpCheckboxComponent } from "@abp/ng.theme.shared"; + +@Component({ + selector: 'app-checkbox-demo', + imports: [AbpCheckboxComponent], + templateUrl: './checkbox-demo.component.html', }) -export class MyFeatureModule {} +export class CheckboxDemoComponent {} ``` -Then, the `abp-checkbox` component can be used. See the example below: +Then, the `abp-checkbox` component can be used in your template. See the example below: ```html
diff --git a/docs/en/framework/ui/angular/form-input-component.md b/docs/en/framework/ui/angular/form-input-component.md index aaf3b6f5be..4f860fe36d 100644 --- a/docs/en/framework/ui/angular/form-input-component.md +++ b/docs/en/framework/ui/angular/form-input-component.md @@ -22,23 +22,21 @@ The ABP FormInput Component is a reusable form input component for the text type # Usage -The ABP FormInput component is a part of the `ThemeSharedModule` module. If you've imported that module into your module, there's no need to import it again. If not, then first import it as shown below: +The ABP FormInput component (`AbpFormInputComponent`) is a standalone component. You can import it directly in your component: ```ts -import { ThemeSharedModule } from "@abp/ng.theme.shared"; -import { FormInputDemoComponent } from "./FomrInputDemoComponent.component"; - -@NgModule({ - imports: [ - ThemeSharedModule, - // ... - ], - declarations: [FormInputDemoComponent], +import { Component } from "@angular/core"; +import { AbpFormInputComponent } from "@abp/ng.theme.shared"; + +@Component({ + selector: 'app-form-input-demo', + imports: [AbpFormInputComponent], + templateUrl: './form-input-demo.component.html', }) -export class MyFeatureModule {} +export class FormInputDemoComponent {} ``` -Then, the `abp-form-input` component can be used. See the example below: +Then, the `abp-form-input` component can be used in your template. See the example below: ```html
diff --git a/docs/en/framework/ui/angular/form-validation.md b/docs/en/framework/ui/angular/form-validation.md index b4ed31842d..8047767279 100644 --- a/docs/en/framework/ui/angular/form-validation.md +++ b/docs/en/framework/ui/angular/form-validation.md @@ -303,7 +303,6 @@ import { NgxValidateCoreModule } from '@ngx-validate/core'; @Component({ selector: 'app-nested-form', templateUrl: './nested-form.component.html', - standalone: true, imports: [NgxValidateCoreModule], }) export class NestedFormComponent implements OnInit { diff --git a/docs/en/framework/ui/angular/how-replaceable-components-work-with-extensions.md b/docs/en/framework/ui/angular/how-replaceable-components-work-with-extensions.md index 5d70510e51..6db3c95fda 100644 --- a/docs/en/framework/ui/angular/how-replaceable-components-work-with-extensions.md +++ b/docs/en/framework/ui/angular/how-replaceable-components-work-with-extensions.md @@ -9,38 +9,45 @@ Additional UI extensibility points ([Entity action extensions](../angular/entity-action-extensions.md), [data table column extensions](../angular/data-table-column-extensions.md), [page toolbar extensions](../angular/page-toolbar-extensions.md) and others) are used in ABP pages to allow to control entity actions, table columns and page toolbar of a page. If you replace a page, you need to apply some configurations to be able to work extension components in your component. Let's see how to do this by replacing the roles page. -Create a new module called `MyRolesModule`: - -```bash -yarn ng generate module my-roles --module app -``` - Create a new component called `MyRolesComponent`: ```bash -yarn ng generate component my-roles/my-roles --flat --export +yarn ng generate component my-roles/my-roles --flat ``` Open the generated `src/app/my-roles/my-roles.component.ts` file and replace its content with the following: ```js import { Component, Injector, inject, OnInit } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { finalize } from 'rxjs/operators'; -import { ListService, PagedAndSortedResultRequestDto, PagedResultDto } from '@abp/ng.core'; +import { ListService, PagedAndSortedResultRequestDto, PagedResultDto, LocalizationPipe } from '@abp/ng.core'; import { eIdentityComponents, RolesComponent } from '@abp/ng.identity'; import { IdentityRoleDto, IdentityRoleService } from '@abp/ng.identity/proxy'; -import { ePermissionManagementComponents } from '@abp/ng.permission-management'; -import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; +import { ePermissionManagementComponents, PermissionManagementComponent } from '@abp/ng.permission-management'; +import { Confirmation, ConfirmationService, ModalComponent, ButtonComponent } from '@abp/ng.theme.shared'; import { EXTENSIONS_IDENTIFIER, FormPropData, - generateFormFromProps + generateFormFromProps, + PageToolbarComponent, + ExtensibleTableComponent, + ExtensibleFormComponent } from '@abp/ng.components/extensible'; @Component({ selector: 'app-my-roles', + imports: [ + ReactiveFormsModule, + LocalizationPipe, + ModalComponent, + ButtonComponent, + PageToolbarComponent, + ExtensibleTableComponent, + ExtensibleFormComponent, + PermissionManagementComponent + ], templateUrl: './my-roles.component.html', providers: [ ListService, @@ -236,25 +243,12 @@ Open the generated `src/app/my-role/my-role.component.html` file and replace its We have added the `abp-page-toolbar`, `abp-extensible-table`, and `abp-extensible-form` extension components to template of the `MyRolesComponent`. -You should import the required modules for the `MyRolesComponent` to `MyRolesModule`. Open the `src/my-roles/my-roles.module.ts` file and replace the content with the following: - -```js -import { ExtensibleModule } from '@abp/ng.components/extensible'; -import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; -import { MyRolesComponent } from './my-roles.component'; -import { PermissionManagementModule } from '@abp/ng.permission-management'; - -@NgModule({ - declarations: [MyRolesComponent], - imports: [SharedModule, ExtensibleModule, PermissionManagementModule], - exports: [MyRolesComponent], -}) -export class MyRolesModule {} -``` - -- `ExtensionsModule` imported to be able to use the extension components in your component. -- `PermissionManagementModule` imported to be able to use the `abp-permission-*management` in your component. +Since we are using standalone components, all required imports are already defined in the component's `imports` array: +- `PageToolbarComponent`, `ExtensibleTableComponent`, `ExtensibleFormComponent` - Extension components +- `PermissionManagementComponent` - Permission management component +- `ModalComponent`, `ButtonComponent` - Theme shared components +- `LocalizationPipe` - For localization +- `ReactiveFormsModule` - For form handling As the last step, it is needs to be replaced the `RolesComponent` with the `MyRolesComponent`. Open the `app.component.ts` and modify its content as shown below: diff --git a/docs/en/framework/ui/angular/localization.md b/docs/en/framework/ui/angular/localization.md index 9f3122529c..d50484c83a 100644 --- a/docs/en/framework/ui/angular/localization.md +++ b/docs/en/framework/ui/angular/localization.md @@ -12,7 +12,7 @@ Before exploring _the localization pipe_ and _the localization service_, you sho The localization key format consists of two sections which are **Resource Name** and **Key**. `ResourceName::Key` -> If you do not specify the resource name, the `defaultResourceName` which is declared in `environment.ts` will be considered as default. +> If you do not specify the resource name, the `defaultResourceName` will be used. The value is first retrieved from the backend API response. If the backend does not provide a `defaultResourceName`, the value declared in `environment.ts` will be used as a fallback. ```ts const environment = { diff --git a/docs/en/framework/ui/angular/quick-start.md b/docs/en/framework/ui/angular/quick-start.md index 0e48f0190e..2417b187b1 100644 --- a/docs/en/framework/ui/angular/quick-start.md +++ b/docs/en/framework/ui/angular/quick-start.md @@ -1,13 +1,13 @@ ```json //[doc-seo] { - "Description": "Learn how to set up your development environment for ABP Angular 17.3.x with this quick start guide, ensuring a smooth development experience." + "Description": "Learn how to set up your development environment for ABP Angular 21.x with this quick start guide, ensuring a smooth development experience." } ``` # ABP Angular Quick Start -**In this version ABP uses Angular [20.0.x](https://github.com/angular/angular/tree/20.0.x) version. You don't have to install Angular CLI globally** +**In this version ABP uses Angular [21.0.x](https://github.com/angular/angular/tree/21.0.x) version. You don't have to install Angular CLI globally** ## How to Prepare Development Environment @@ -18,13 +18,13 @@ Please follow the steps below to prepare your development environment for Angula 3. **[Optional] Install VS Code:** [VS Code](https://code.visualstudio.com/) is a free, open-source IDE which works seamlessly with TypeScript. Although you can use any IDE including Visual Studio or Rider, VS Code will most likely deliver the best developer experience when it comes to Angular projects. ABP project templates even contain plugin recommendations for VS Code users, which VS Code will ask you to install when you open the Angular project folder. Here is a list of recommended extensions: - [Angular Language Service](https://marketplace.visualstudio.com/items?itemName=angular.ng-template) - [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - - [TSLint](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-typescript-tslint-plugin) + - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - [Visual Studio IntelliCode](https://marketplace.visualstudio.com/items?itemName=visualstudioexptteam.vscodeintellicode) - [Path Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense) - [npm Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.npm-intellisense) - [Angular 10 Snippets - TypeScript, Html, Angular Material, ngRx, RxJS & Flex Layout](https://marketplace.visualstudio.com/items?itemName=Mikael.Angular-BeastCode) - [JavaScript (ES6) code snippets](https://marketplace.visualstudio.com/items?itemName=xabikos.JavaScriptSnippets) - - [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) + - [JavaScript Debugger](https://marketplace.visualstudio.com/items?itemName=ms-vscode.js-debug) (built-in, usually pre-installed) - [Git History](https://marketplace.visualstudio.com/items?itemName=donjayamanne.githistory) - [indent-rainbow](https://marketplace.visualstudio.com/items?itemName=oderwat.indent-rainbow) diff --git a/docs/en/framework/ui/angular/ssr-configuration.md b/docs/en/framework/ui/angular/ssr-configuration.md index e058beb861..4d889377fd 100644 --- a/docs/en/framework/ui/angular/ssr-configuration.md +++ b/docs/en/framework/ui/angular/ssr-configuration.md @@ -239,7 +239,7 @@ The schematic installs `openid-client` to handle authentication on the server si ## 5. Render Modes & Hybrid Rendering -Angular 20 provides different rendering modes that you can configure per route in the `app.routes.server.ts` file to optimize performance and SEO. +Angular 21 provides different rendering modes that you can configure per route in the `app.routes.server.ts` file to optimize performance and SEO. ### 5.1. Available Render Modes @@ -352,13 +352,17 @@ currentTime = new Date(); // ✅ Good - use TransferState for consistent data import { TransferState, makeStateKey } from '@angular/core'; -const TIME_KEY = makeStateKey('time'); +TIME_KEY = makeStateKey('time'); +transferState = inject(TransferState); +time: string; -constructor(private transferState: TransferState) { +constructor() { if (isPlatformServer(this.platformId)) { - this.transferState.set(TIME_KEY, new Date().toISOString()); + this.time = new Date().toISOString(); + this.transferState.set(this.TIME_KEY, this.time); } else { - this.time = this.transferState.get(TIME_KEY, new Date().toISOString()); + const timeFromCache = this.transferState.get(this.TIME_KEY, new Date().toISOString()); + this.time = timeFromCache; } } ``` diff --git a/docs/en/framework/ui/angular/theming.md b/docs/en/framework/ui/angular/theming.md index 1351a02f33..bad673716a 100644 --- a/docs/en/framework/ui/angular/theming.md +++ b/docs/en/framework/ui/angular/theming.md @@ -229,13 +229,14 @@ All of the options are shown below. You can choose either of them. ````ts import { eUserMenuItems } from '@abp/ng.theme.basic'; -import { UserMenuService } from '@abp/ng.theme.shared'; +import { UserMenuService, UserMenu } from '@abp/ng.theme.shared'; +import { LocalizationPipe, INJECTOR_PIPE_DATA_TOKEN } from '@abp/ng.core'; import { Component, inject } from '@angular/core'; import { Router } from '@angular/router'; -// make sure that you import this component in a NgModule @Component({ selector: 'abp-current-user-test', + imports: [LocalizationPipe], template: ` @if (data.textTemplate.icon){ diff --git a/docs/en/framework/ui/mvc-razor-pages/modals.md b/docs/en/framework/ui/mvc-razor-pages/modals.md index d37575cc26..6ad7ad421b 100644 --- a/docs/en/framework/ui/mvc-razor-pages/modals.md +++ b/docs/en/framework/ui/mvc-razor-pages/modals.md @@ -207,7 +207,7 @@ namespace MyProject.Web.Pages.Products public class ProductCreateModalModel : AbpPageModel { [BindProperty] - public PoductCreationDto Product { get; set; } + public ProductCreationDto Product { get; set; } public async Task OnGetAsync() { @@ -227,9 +227,9 @@ namespace MyProject.Web.Pages.Products * This is a simple `PageModal` class. The `[BindProperty]` make the form binding to the model when you post (submit) the form; The standard ASP.NET Core system. * `OnPostAsync` returns `NoContent` (this method is defined by the base `AbpPageModel` class). Because we don't need to a return value in the client side, after the form post operation. -**PoductCreationDto:** +**ProductCreationDto:** -`ProductCreateModalModel` uses a `PoductCreationDto` class defined as shown below: +`ProductCreateModalModel` uses a `ProductCreationDto` class defined as shown below: ````csharp using System; @@ -238,7 +238,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; namespace MyProject.Web.Pages.Products { - public class PoductCreationDto + public class ProductCreationDto { [Required] [StringLength(128)] diff --git a/docs/en/framework/ui/mvc-razor-pages/tag-helpers/index.md b/docs/en/framework/ui/mvc-razor-pages/tag-helpers/index.md index d986d65b43..e0be666b2c 100644 --- a/docs/en/framework/ui/mvc-razor-pages/tag-helpers/index.md +++ b/docs/en/framework/ui/mvc-razor-pages/tag-helpers/index.md @@ -7,7 +7,7 @@ # ABP Tag Helpers -ABP defines a set of **tag helper components** to simply the user interface development for ASP.NET Core (MVC / Razor Pages) applications. +ABP defines a set of **tag helper components** to simplify the user interface development for ASP.NET Core (MVC / Razor Pages) applications. ## Bootstrap Component Wrappers @@ -45,7 +45,7 @@ Here, the list of components those are wrapped by the ABP: ## Form Elements -**Abp Tag Helpers** add new features to standard **Asp.Net Core MVC input & select Tag Helpers** and wrap them with **Bootstrap** form controls. See [Form Elements documentation](form-elements.md) . +**Abp Tag Helpers** add new features to standard **ASP.NET Core MVC input & select Tag Helpers** and wrap them with **Bootstrap** form controls. See [Form Elements documentation](form-elements.md) . ## Dynamic Forms diff --git a/docs/en/framework/ui/mvc-razor-pages/theming.md b/docs/en/framework/ui/mvc-razor-pages/theming.md index ff73b3ad1b..138b52e3d2 100644 --- a/docs/en/framework/ui/mvc-razor-pages/theming.md +++ b/docs/en/framework/ui/mvc-razor-pages/theming.md @@ -437,7 +437,7 @@ In this way, applications or modules can have selectors based on the current lay ### RTL -To support Right-To-Left languages, the Layout should check the current culture and add `dir="rtl"` to the `html` tag and `rtl` CSS class the the `body` tag. +To support Right-To-Left languages, the Layout should check the current culture and add `dir="rtl"` to the `html` tag and `rtl` CSS class to the `body` tag. You can check `CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft` to understand if the current language is a RTL language. diff --git a/docs/en/framework/ui/mvc-razor-pages/widgets.md b/docs/en/framework/ui/mvc-razor-pages/widgets.md index ac9901733b..7e894027f6 100644 --- a/docs/en/framework/ui/mvc-razor-pages/widgets.md +++ b/docs/en/framework/ui/mvc-razor-pages/widgets.md @@ -42,7 +42,7 @@ namespace DashboardDemo.Web.Pages.Components.MySimpleWidget Inheriting from `AbpViewComponent` is not required. You could inherit from ASP.NET Core's standard `ViewComponent`. `AbpViewComponent` only defines some base useful properties. -You can inject a service and use in the `Invoke` method to get some data from the service. You may need to make Invoke method async, like `public async Task InvokeAsync()`. See [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components) document fore all different usages. +You can inject a service and use in the `Invoke` method to get some data from the service. You may need to make Invoke method async, like `public async Task InvokeAsync()`. See [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components) document for all different usages. **Default.cshtml**: diff --git a/docs/en/framework/ui/react-native/index.md b/docs/en/framework/ui/react-native/index.md index 6496b0a640..862d836ac4 100644 --- a/docs/en/framework/ui/react-native/index.md +++ b/docs/en/framework/ui/react-native/index.md @@ -12,11 +12,11 @@ } ```` -# Getting Started with the React Native +# Getting Started with React Native -> React Native mobile option is *available for* ***Team*** *or higher licenses* +> The React Native mobile option is *available for* ***Team*** *or higher licenses* -ABP platform provide basic [React Native](https://reactnative.dev/) startup template to develop mobile applications **integrated to your ABP based backends**. +The ABP platform provides a basic [React Native](https://reactnative.dev/) startup template to develop mobile applications **integrated with your ABP-based backends**. ![React Native gif](../../../images/react-native-introduction.gif) @@ -24,12 +24,14 @@ ABP platform provide basic [React Native](https://reactnative.dev/) startup temp Please follow the steps below to prepare your development environment for React Native. -1. **Install Node.js:** Please visit [Node.js downloads page](https://nodejs.org/en/download/) and download proper Node.js v20.11+ installer for your OS. An alternative is to install [NVM](https://github.com/nvm-sh/nvm) and use it to have multiple versions of Node.js in your operating system. -2. **[Optional] Install Yarn:** You may install Yarn v1 (not v2) following the instructions on [the installation page](https://classic.yarnpkg.com/en/docs/install). Yarn v1 delivers an arguably better developer experience compared to npm v6 and below. You may skip this step and work with npm, which is built-in in Node.js, instead. -3. **[Optional] Install VS Code:** [VS Code](https://code.visualstudio.com/) is a free, open-source IDE which works seamlessly with TypeScript. Although you can use any IDE including Visual Studio or Rider, VS Code will most likely deliver the best developer experience when it comes to React Native projects. -4. **Install an Emulator/Simulator:** React Native applications need an Android emulator or an iOS simulator to run on your OS. If you do not have Android Studio installed and configured on your system, we recommend [setting up android emulator without android studio](setting-up-android-emulator.md). +1. **Install Node.js:** Visit the [Node.js downloads page](https://nodejs.org/en/download/) and download the appropriate Node.js v20.11+ installer for your operating system. Alternatively, you can install [NVM](https://github.com/nvm-sh/nvm) to manage multiple versions of Node.js on your system. +2. **[Optional] Install Yarn:** You can install Yarn v1 (not v2) by following the instructions on [the installation page](https://classic.yarnpkg.com/en/docs/install). Yarn v1 provides a better developer experience compared to npm v6 and below. You can skip this step and use npm, which is built into Node.js. +3. **[Optional] Install VS Code:** [VS Code](https://code.visualstudio.com/) is a free, open-source IDE that works seamlessly with TypeScript. While you can use any IDE, including Visual Studio or Rider, VS Code typically provides the best developer experience for React Native projects. +4. **[Optional] Install an Emulator/Simulator:** If you want to test on Android emulators or iOS simulators (instead of using the Web View method), you'll need to install one of the following: + - **Android Studio & Emulator:** Install [Android Studio](https://developer.android.com/studio) and set up an Android Virtual Device (AVD) through the AVD Manager. You can follow the [Android Studio Emulator guide](https://docs.expo.dev/workflow/android-studio-emulator/) on expo.io documentation. + - **Xcode & iOS Simulator:** On macOS, install [Xcode](https://developer.apple.com/xcode/) from the App Store, which includes the iOS Simulator. You can follow the [iOS Simulator guide](https://docs.expo.dev/workflow/ios-simulator/) on expo.io documentation. -If you prefer the other way, you can check the [Android Studio Emulator](https://docs.expo.dev/workflow/android-studio-emulator/) or [iOS Simulator](https://docs.expo.dev/workflow/ios-simulator/) on expo.io documentation to learn how to set up an emulator. + > **Note:** The Web View method (recommended for quick testing) doesn't require an emulator or simulator. If you prefer a CLI-based approach for Android, you can check the [setting up android emulator without android studio](setting-up-android-emulator.md) guide as an alternative. ## How to Start a New React Native Project @@ -37,34 +39,143 @@ You have multiple options to initiate a new React Native project that works with ### 1. Using ABP Studio -ABP Studio application is the most convenient and flexible way to initiate a React Native application based on ABP framework. You can follow the [tool documentation](../../../studio) and select the option below: +ABP Studio is the most convenient and flexible way to create a React Native application based on the ABP framework. Follow the [tool documentation](../../../studio) and select the option below: ![React Native option](../../../images/react-native-option.png) ### 2. Using ABP CLI -ABP CLI is another way of creating an ABP solution with a React Native application. Simply [install the ABP CLI](../../../cli) and run the following command in your terminal: +The ABP CLI is another way to create an ABP solution with a React Native application. [Install the ABP CLI](../../../cli) and run the following command in your terminal: ```shell abp new MyCompanyName.MyProjectName -csf -u -m react-native ``` -> To see further options in the CLI, please visit the [CLI manual](../../../cli). +> For more options, visit the [CLI manual](../../../cli). -This command will prepare a solution with an **Angular** or an **MVC** (depends on your choice), a **.NET Core**, and a **React Native** project in it. +This command creates a solution containing an **Angular** or **MVC** project (depending on your choice), a **.NET Core** project, and a **React Native** project. -## How to Configure & Run the Backend +## Running the React Native Application + +> **Recommended:** For faster development and testing, we recommend using the **Web View** option first, as it requires fewer backend modifications. The backend configuration described in the next section is only needed if you want to test on Android emulators or iOS simulators. + +Before running the React Native application, install the dependencies by running `yarn install` or `npm install` in the `react-native` directory. + + +### Web View (Recommended - Quickest Method) + +The quickest way to test the application is by using the web view. While testing on a physical device is also supported, we recommend using [local HTTPS development](https://docs.expo.dev/guides/local-https-development/) as it requires fewer backend modifications. + +Follow these steps to set up the web view: + +1. Navigate to the `react-native` directory and start the application by running: + ```bash + yarn web + ``` + +2. Generate SSL certificates by running the following command in a separate directory: + ```bash + mkcert localhost + ``` + +3. Set up the local proxy by running: + ```bash + yarn create:local-proxy + ``` + The default port is `443`. To use a different port, specify the `SOURCE_PORT` environment variable: + ```bash + SOURCE_PORT=8443 yarn create:local-proxy + ``` + +4. If you changed the port in the previous step, update the `apiUrl` in `Environment.ts` accordingly. + +5. Update the mobile application settings in the database and re-run the migrations. If you specified a custom port, ensure the port is updated in the configuration as well: + ```json + "OpenIddict": { + "Applications": { + "MyApplication_Mobile": { + "ClientId": "MyApplication_Mobile", + "RootUrl": "https://localhost" + } + } + } + ``` + +### Running on Emulator/Simulator + +If you prefer to test on an Android emulator or iOS simulator, you'll need to configure the backend as described in the section below. Follow these steps: + +1. Make sure the [database migration is complete](../../../get-started?UI=NG&DB=EF&Tiered=No#create-the-database) and the [API is up and running](../../../get-started?UI=NG&DB=EF&Tiered=No#run-the-application). +2. Open `react-native` folder and run `yarn install` or `npm install` if you have not already. +3. Open the `Environment.ts` file in the `react-native` folder and replace the `localhost` address in the `apiUrl` and `issuer` properties with your local IP address as shown below: + +{{ if Architecture == "Monolith" }} + +![react native monolith environment local IP](../../../images/react-native-monolith-environment-local-ip.png) + +{{ else if Architecture == "Tiered" }} + +![react native tiered environment local IP](../../../images/react-native-tiered-environment-local-ip.png) + +> Make sure that `issuer` matches the running address of the `.AuthServer` project, `apiUrl` matches the running address of the `.HttpApi.Host` or `.Web` project. + +{{ else }} + +![react native microservice environment local IP](../../../images/react-native-environment-local-ip.png) + +> Make sure that `issuer` matches the running address of the `.AuthServer` project, `apiUrl` matches the running address of the `.AuthServer` project. + +{{ end }} + +1. Run `yarn start` or `npm start`. Wait for the Expo CLI to print the options. + +> The React Native application was generated with [Expo](https://expo.io/). Expo is a set of tools built around React Native to help you quickly start an app, and it includes many features. + +![expo-cli-options](../../../images/rn-options.png) + +In the image above, you can start the application on an Android emulator, an iOS simulator, or a physical phone by scanning the QR code with the [Expo Client](https://expo.io/tools#client) or by choosing the corresponding option. + +### Expo + +![React Native login screen on iPhone 16](../../../images/rn-login-iphone.png) + +### Android Studio + +1. Start the emulator in **Android Studio** before running the `yarn start` or `npm start` command. +2. Press **a** to open in Android Studio. + +![React Native login screen on Android Device](../../../images/rn-login-android-studio.png) + +Enter **admin** as the username and **1q2w3E\*** as the password to log in to the application. + +The application is up and running. You can continue to develop your application based on this startup template. + +## How to Configure & Run the Backend (Required for Emulator/Simulator Testing) > React Native application does not trust the auto-generated .NET HTTPS certificate. You should use **HTTP** during the development. -A React Native application running on an Android emulator or a physical phone **can not connect to the backend** on `localhost`. To fix this problem, it is necessary to run the backend application using the `Kestrel` configuration. +To disable the HTTPS-only settings of OpenIddict, open the {{ if Architecture == "Monolith" }}`MyProjectNameHttpApiHostModule`{{ else if Architecture == "Tiered" }}`MyProjectNameAuthServerModule`{{ end }} project and add the following code block to the `PreConfigureServices` method: + +```csharp +#if DEBUG + PreConfigure(options => + { + options.UseAspNetCore() + .DisableTransportSecurityRequirement(); + }); +#endif +``` + +> **Important:** Before running the backend application, make sure you have completed the [database migration](../../../get-started?UI=NG&DB=EF&Tiered=No#create-the-database) if you are starting with a fresh database. The backend application requires the database to be properly initialized. + +A React Native application running on an Android emulator or a physical phone **cannot connect to the backend** on `localhost`. To resolve this, you need to run the backend application using the `Kestrel` configuration. {{ if Architecture == "Monolith" }} ![React Native monolith host project configuration](../../../images/react-native-monolith-be-config.png) -- Open the `appsettings.json` file in the `.DbMigrator` folder. Replace the `localhost` address on the `RootUrl` property with your local IP address. Then, run the database migrator. -- Open the `appsettings.Development.json` file in the `.HttpApi.Host` folder. Add this configuration to accept global requests just to test the react native application on the development environment. +- Open the `appsettings.json` file in the `.DbMigrator` folder. Replace the `localhost` address in the `RootUrl` property with your local IP address. Then, run the database migrator. +- Open the `appsettings.Development.json` file in the `.HttpApi.Host` folder. Add this configuration to accept global requests for testing the React Native application in the development environment. ```json { @@ -82,8 +193,8 @@ A React Native application running on an Android emulator or a physical phone ** ![React Native tiered project configuration](../../../images/react-native-tiered-be-config.png) -- Open the `appsettings.json` file in the `.DbMigrator` folder. Replace the `localhost` address on the `RootUrl` property with your local IP address. Then, run the database migrator. -- Open the `appsettings.Development.json` file in the `.AuthServer` folder. Add this configuration to accept global requests just to test the react native application on the development environment. +- Open the `appsettings.json` file in the `.DbMigrator` folder. Replace the `localhost` address in the `RootUrl` property with your local IP address. Then, run the database migrator. +- Open the `appsettings.Development.json` file in the `.AuthServer` folder. Add this configuration to accept global requests for testing the React Native application in the development environment. ```json { @@ -97,7 +208,7 @@ A React Native application running on an Android emulator or a physical phone ** } ``` -- Open the `appsettings.Development.json` file in the `.HttpApi.Host` folder. Add this configuration to accept global requests again. In addition, you will need to introduce the authentication server as mentioned above. +- Open the `appsettings.Development.json` file in the `.HttpApi.Host` folder. Add this configuration to accept global requests. Additionally, you need to configure the authentication server as mentioned above. ```json { @@ -121,7 +232,7 @@ A React Native application running on an Android emulator or a physical phone ** ![React Native microservice project configuration](../../../images/react-native-microservice-be-config.png) -- Open the `appsettings.Development.json` file in the `.AuthServer` folder. Add this configuration to accept global requests just to test the react native application on the development environment. +- Open the `appsettings.Development.json` file in the `.AuthServer` folder. Add this configuration to accept global requests for testing the React Native application in the development environment. ```json { @@ -138,7 +249,7 @@ A React Native application running on an Android emulator or a physical phone ** } ``` -- Open the `appsettings.Development.json` file in the `.AdministrationService` folder. Add this configuration to accept global requests just to test the react native application on the development environment. You should also provide the authentication server configuration. In addition, you need to apply the same process for all the services you would use in the react native application. +- Open the `appsettings.Development.json` file in the `.AdministrationService` folder. Add this configuration to accept global requests for testing the React Native application in the development environment. You should also provide the authentication server configuration. Additionally, you need to apply the same process for all services you will use in the React Native application. ```json { @@ -161,7 +272,7 @@ A React Native application running on an Android emulator or a physical phone ** } ``` -- Update the `appsettings.json` file in the `.IdentityService` folder. Replace the `localhost` configuration with your local IP address for the react native application +- Update the `appsettings.json` file in the `.IdentityService` folder. Replace the `localhost` configuration with your local IP address for the React Native application. ```json { @@ -182,7 +293,7 @@ A React Native application running on an Android emulator or a physical phone ** } ``` -- Lastly, update the mobile gateway configurations as following: +- Finally, update the mobile gateway configurations as follows: ```json //gateways/mobile/MyMicroserviceProject.MobileGateway/Properties/launchSettings.json @@ -259,71 +370,4 @@ A React Native application running on an Android emulator or a physical phone ** {{ end }} -Run the backend application as described in the [getting started document](../../../get-started). - -> You should turn off the "Https Restriction" if you're using OpenIddict as a central identity management solution. Because the IOS Simulator doesn't support self-signed certificates and OpenIddict is set to only work with HTTPS by default. - -## How to disable the Https-only settings of OpenIddict - -Open the {{ if Architecture == "Monolith" }}`MyProjectNameHttpApiHostModule`{{ else if Architecture == "Tiered" }}`MyProjectNameAuthServerModule`{{ end }} project and copy-paste the below code-block to the `PreConfigureServices` method: - -```csharp -#if DEBUG - PreConfigure(options => - { - options.UseAspNetCore() - .DisableTransportSecurityRequirement(); - }); -#endif -``` - -## How to Configure & Run the React Native Application - -1. Make sure the [database migration is complete](../../../get-started?UI=NG&DB=EF&Tiered=No#create-the-database) and the [API is up and running](../../../get-started?UI=NG&DB=EF&Tiered=No#run-the-application). -2. Open `react-native` folder and run `yarn` or `npm install` if you have not already. -3. Open the `Environment.ts` in the `react-native` folder and replace the `localhost` address on the `apiUrl` and `issuer` properties with your local IP address as shown below: - -{{ if Architecture == "Monolith" }} - -![react native monolith environment local IP](../../../images/react-native-monolith-environment-local-ip.png) - -{{ else if Architecture == "Tiered" }} - -![react native tiered environment local IP](../../../images/react-native-tiered-environment-local-ip.png) - -> Make sure that `issuer` matches the running address of the `.AuthServer` project, `apiUrl` matches the running address of the `.HttpApi.Host` or `.Web` project. - -{{ else }} - -![react native microservice environment local IP](../../../images/react-native-microservice-environment-local-ip.png) - -> Make sure that `issuer` matches the running address of the `.AuthServer` project, `apiUrl` matches the running address of the `.AuthServer` project. - -{{ end }} - -1. Run `yarn start` or `npm start`. Wait for the Expo CLI to print the opitons. - -> The React Native application was generated with [Expo](https://expo.io/). Expo is a set of tools built around React Native to help you quickly start an app and, while it has many features. - -![expo-cli-options](../../../images/rn-options.png) - -In the above image, you can start the application with an Android emulator, an iOS simulator or a physical phone by scanning the QR code with the [Expo Client](https://expo.io/tools#client) or choosing the option. - -### Expo - -![React Native login screen on iPhone 16](../../../images/rn-login-iphone.png) - -### Android Studio - -1. Start the emulator in **Android Studio** before running the `yarn start` or `npm start` command. -2. Press **a** to open in Android Studio. - -![React Native login screen on Android Device](../../../images/rn-login-android-studio.png) - -Enter **admin** as the username and **1q2w3E\*** as the password to login to the application. - -The application is up and running. You can continue to develop your application based on this startup template. - -## See Also - -- [React Native project structure](../../../solution-templates/application-module#react-native) +Run the backend application(s) as described in the [getting started document](../../../get-started). diff --git a/docs/en/framework/ui/react-native/setting-up-android-emulator.md b/docs/en/framework/ui/react-native/setting-up-android-emulator.md index e5d14de32a..495197a0da 100644 --- a/docs/en/framework/ui/react-native/setting-up-android-emulator.md +++ b/docs/en/framework/ui/react-native/setting-up-android-emulator.md @@ -1,58 +1,121 @@ -```json -//[doc-seo] -{ - "Description": "Learn how to set up an Android emulator without Android Studio using command line tools on Windows, macOS, and Linux." -} -``` - +```json +//[doc-seo] +{ + "Description": "Learn how to set up an Android emulator without Android Studio using command line tools on Windows, macOS, and Linux." +} +``` + # Setting Up Android Emulator Without Android Studio (Windows, macOS, Linux) -This guide explains how to install and run an Android emulator **without Android Studio**, using only **Command Line Tools**. +This guide walks you through installing and running an Android emulator **without Android Studio**, using only the **Android Command Line Tools**. --- ## 1. Download Required Tools -Go to: [https://developer.android.com/studio#command-tools](https://developer.android.com/studio#command-tools) -Download the "Command line tools only" package for your OS: +Visit the [Android Command Line Tools download page](https://developer.android.com/studio#command-line-tools-only) and download the "Command line tools only" package for your operating system: - **Windows:** `commandlinetools-win-*.zip` - **macOS:** `commandlinetools-mac-*.zip` - **Linux:** `commandlinetools-linux-*.zip` +> **Alternative for Windows:** If you prefer using Windows Package Manager, you can install Android Studio (which includes the command-line tools) using: +> ```powershell +> winget install --id=Google.AndroidStudio -e +> ``` +> However, this guide focuses on installing only the command-line tools without the full IDE. + --- -## 2. Create the Required Directory Structure +## 2. Create Directory Structure and Extract Files + +The Android SDK tools require a specific directory structure. Follow the steps below for your operating system. ### Windows: -``` -C:\Android\ -└── cmdline-tools\ - └── latest\ - └── [extract all files from the zip here] -``` + +1. **Create the directory structure:** + ``` + C:\Android\ + └── cmdline-tools\ + └── latest\ + ``` + +2. **Extract the downloaded zip file.** The archive contains a `cmdline-tools` folder with `bin`, `lib`, and other files. + +3. **Move all contents** from the extracted `cmdline-tools` folder into `C:\Android\cmdline-tools\latest\` + + Your final directory structure should look like this: + ``` + C:\Android\ + └── cmdline-tools\ + └── latest\ + ├── bin\ + ├── lib\ + └── [other files] + ``` ### macOS / Linux: -``` -~/Android/ -└── cmdline-tools/ - └── latest/ - └── [extract all files from the zip here] -``` -> You need to create the `latest` folder manually. +1. **Create the directory structure:** + ```bash + mkdir -p ~/Android/cmdline-tools + ``` + +2. **Extract the downloaded zip file:** + ```bash + cd ~/Downloads + unzip commandlinetools-*.zip + ``` + +3. **Move the extracted folder to the correct location:** + ```bash + mv cmdline-tools ~/Android/cmdline-tools/latest + ``` + + Your final directory structure should look like this: + ``` + ~/Android/ + └── cmdline-tools/ + └── latest/ + ├── bin/ + ├── lib/ + └── [other files] + ``` + +> **Important:** The `latest` folder must be created manually (Windows) or by renaming the extracted folder (macOS/Linux). The Android SDK tools require this exact directory structure to function properly. --- -## 3. Set Environment Variables +## 3. Configure Environment Variables + +Set up environment variables so your system can locate the Android SDK tools. + +### Windows (PowerShell - permanent): + +Run these commands in PowerShell to set environment variables permanently: + +```powershell +[System.Environment]::SetEnvironmentVariable('ANDROID_HOME', 'C:\Android', 'User') +$currentPath = [System.Environment]::GetEnvironmentVariable('Path', 'User') +[System.Environment]::SetEnvironmentVariable('Path', "$currentPath;C:\Android\cmdline-tools\latest\bin;C:\Android\platform-tools;C:\Android\emulator", 'User') +``` + +> **Note:** You may need to restart your terminal or PowerShell session for the changes to take effect. + +### Windows (CMD - temporary for current session): + +If you only need the environment variables for the current session, use these commands: -### Windows (temporary for CMD session): ```cmd -set PATH=C:\Android\cmdline-tools\latest\bin;C:\Android\platform-tools;C:\Android\emulator;%PATH% +set ANDROID_HOME=C:\Android +set PATH=%PATH%;C:\Android\cmdline-tools\latest\bin;C:\Android\platform-tools;C:\Android\emulator ``` +> **Note:** These settings will be lost when you close the command prompt window. + ### macOS / Linux: -Add the following to your `.bashrc`, `.zshrc`, or `.bash_profile` file: + +Add the following environment variables to your shell configuration file (`.bashrc`, `.zshrc`, or `.bash_profile`): ```bash export ANDROID_HOME=$HOME/Android @@ -61,62 +124,164 @@ export PATH=$PATH:$ANDROID_HOME/platform-tools export PATH=$PATH:$ANDROID_HOME/emulator ``` -> Apply the changes: +After saving the file, reload your shell configuration: + ```bash source ~/.zshrc # or ~/.bashrc if you're using bash ``` +**Verify Environment Variables:** + +After reloading, verify the variables are set correctly: + +```bash +echo $ANDROID_HOME +which sdkmanager +``` + --- -## 4. Install SDK Components +## 4. Accept Android SDK Licenses (macOS/Linux) -Install platform tools, emulator, and a system image: +**macOS / Linux users only:** Before installing SDK components, you must accept the Android SDK licenses: ```bash -sdkmanager --sdk_root=$ANDROID_HOME "platform-tools" "platforms;android-34" "system-images;android-34;google_apis;x86_64" "emulator" +yes | sdkmanager --licenses ``` -> On Windows, replace `$ANDROID_HOME` with `--sdk_root=C:\Android`. +This command automatically accepts all licenses. Without this step, the installation will fail. + +> **Note:** Windows users will be prompted to accept licenses during the installation in the next step. --- -## 5. Create an AVD (Android Virtual Device) +## 5. Install SDK Components + +Use `sdkmanager` to install the required Android SDK components: platform tools, an Android platform, a system image, and the emulator. + +### Windows: + +```cmd +sdkmanager --sdk_root=C:\Android "platform-tools" "platforms;android-35" "system-images;android-35;google_apis;x86_64" "emulator" +``` + +### macOS / Linux: + +**For Apple Silicon Macs (M1/M2/M3/M4):** +```bash +sdkmanager "platform-tools" "platforms;android-35" "system-images;android-35;google_apis;arm64-v8a" "emulator" +``` + +**For Intel-based Macs and Linux:** +```bash +sdkmanager "platform-tools" "platforms;android-35" "system-images;android-35;google_apis;x86_64" "emulator" +``` + +> **Note:** +> - This command installs Android 15 (API level 35), which is the current stable version required by Google Play as of 2025. +> - To see all available versions, run `sdkmanager --list` and replace `android-35` with your preferred API level if needed. +> - Use `arm64-v8a` for Apple Silicon Macs (M1/M2/M3/M4) and `x86_64` for Intel-based Macs, Windows, and most Linux systems. The architecture must match your system's processor. + +--- + +## 6. Create an Android Virtual Device (AVD) + +An AVD is a device configuration that defines the characteristics of an Android device you want to simulate. + +### List Available Device Profiles (Optional): + +Before creating an AVD, you can view all available device profiles (e.g., Pixel, Nexus) to choose from: -### List available devices: ```bash avdmanager list devices ``` -### Create your AVD: +### Create Your AVD: + +Create a new AVD with the following command: + +**Windows:** +```cmd +avdmanager create avd -n myEmu -k "system-images;android-35;google_apis;x86_64" --device "pixel_8" +``` + +**macOS / Linux (Apple Silicon):** ```bash -avdmanager create avd -n myEmu -k "system-images;android-34;google_apis;x86_64" --device "pixel" +avdmanager create avd -n myEmu -k "system-images;android-35;google_apis;arm64-v8a" --device "pixel_8" ``` +**macOS / Linux (Intel-based):** +```bash +avdmanager create avd -n myEmu -k "system-images;android-35;google_apis;x86_64" --device "pixel_8" +``` + +When prompted "Do you wish to create a custom hardware profile?" type `no` and press Enter. + +> **Note:** +> - Replace `myEmu` with your preferred AVD name +> - Replace `pixel_8` with a device profile from the list above if you want a different device configuration +> - The `-k` parameter specifies the system image you installed in the previous step. Make sure it matches the architecture you installed (x86_64 for Windows and Intel-based systems, arm64-v8a for Apple Silicon Macs) + --- -## 6. Start the Emulator +## 7. Start the Emulator + +Launch your emulator using the AVD name you created: ```bash emulator -avd myEmu ``` -The emulator window should open +Replace `myEmu` with the name you used when creating your AVD. The emulator window should open and boot up the Android system. + +### Common Startup Issues (macOS/Linux): + +**If you get "command not found":** +```bash +$ANDROID_HOME/emulator/emulator -avd myEmu +``` + +**For better performance on Apple Silicon:** +```bash +emulator -avd myEmu -gpu host +``` + +**For headless mode (no GUI window):** +```bash +emulator -avd myEmu -no-window +``` --- -## Extra Tools and Commands +## 8. Additional Tools and Commands + +### List All AVDs: + +View all created virtual devices: + +```bash +avdmanager list avd +``` + +### List Connected Devices: + +Use ADB (Android Debug Bridge) to view all connected Android devices and running emulators: -### List connected devices with ADB: ```bash adb devices ``` +This is useful for verifying that your emulator is running and properly connected. + ### Install an APK: + +Install an Android application package (APK) to your emulator or connected device: + ```bash adb install myApp.apk ``` ---- +Replace `myApp.apk` with the path to your APK file. ## How to Enable Fast Refresh in React Native @@ -146,17 +311,66 @@ Focus the emulator window and press: --- -## Troubleshooting +## 9. Troubleshooting + +Common issues and their solutions: -| Problem | Explanation | -|--------|-------------| -| `sdkmanager not found` | Make sure `PATH` includes the `latest/bin` directory | -| `x86_64 system image not found` | Make sure you've downloaded it using `sdkmanager` | -| `emulator not found` | Add the `emulator` directory to `PATH` | -| `setx` truncates path (Windows) | Use GUI to update environment variables manually | +| Problem | Solution | +|--------|----------| +| `sdkmanager not found` | **Windows:** Verify that your `PATH` environment variable includes `C:\Android\cmdline-tools\latest\bin`. **macOS/Linux:** Verify `PATH` includes `$ANDROID_HOME/cmdline-tools/latest/bin`. Run `source ~/.zshrc` or `source ~/.bashrc` to reload environment variables | +| `Warning: Could not create settings` | **macOS/Linux:** Run `yes \| sdkmanager --licenses` to accept all Android SDK licenses. **Windows:** Accept licenses when prompted during installation | +| System image not found | Ensure you've installed the system image using `sdkmanager` in step 5. The architecture must match: `x86_64` for Windows and Intel-based systems, `arm64-v8a` for Apple Silicon Macs. Run `sdkmanager --list` to verify available images, then run the installation command again if needed | +| `emulator not found` | **Windows:** Add the `emulator` directory to your `PATH`: `C:\Android\emulator`. **macOS/Linux:** Use full path: `$ANDROID_HOME/emulator/emulator -avd myEmu` or verify `$ANDROID_HOME/emulator` is in your PATH | +| `setx` truncates path (Windows) | If `setx` truncates your PATH variable, use the PowerShell method from step 3, or update environment variables manually through the Windows GUI (System Properties → Environment Variables) | +| Emulator won't start | **Windows:** Ensure hardware acceleration is enabled. Enable Hyper-V or use Intel HAXM. **macOS (Apple Silicon):** Try `emulator -avd myEmu -gpu host`. **Linux:** Ensure KVM is enabled: `sudo apt install qemu-kvm` and add user to kvm group | +| Permission denied errors (macOS/Linux) | Run `chmod +x $ANDROID_HOME/cmdline-tools/latest/bin/*` to make tools executable | +| Emulator extremely slow | **Windows:** Enable Hyper-V or Intel HAXM. **Linux:** Install and enable KVM (see below). **Apple Silicon:** Ensure you're using `arm64-v8a` system images for optimal performance | + +### Enable Hardware Acceleration (Linux): + +For better performance on Linux, install and enable KVM: + +```bash +sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils +sudo adduser $USER kvm +``` + +Log out and log back in for group changes to take effect. + +--- + +## 10. Summary + +You've successfully set up an Android emulator without installing Android Studio, using only the command-line tools. This emulator can be used for React Native development or any other mobile development framework that requires Android emulation. + +The emulator is now ready to use. You can start it anytime with `emulator -avd myEmu` (using your AVD name) and begin developing and testing your Android applications. + +### Quick Reference Commands: + +**Start the emulator:** +```bash +emulator -avd myEmu +``` + +**List running devices:** +```bash +adb devices +``` + +**Install an app:** +```bash +adb install myApp.apk +``` + +**List all AVDs:** +```bash +avdmanager list avd +``` --- -## Summary +## Additional Notes -You can now run an Android emulator without installing Android Studio, entirely through the command line. This emulator can be used for React Native or any mobile development framework. +- **Android API Updates:** As of January 2026, Android 15 (API 35) is the current requirement for Google Play Store submissions. This guide uses API 35, but you can install older versions if needed for legacy app testing. +- **Storage Location:** All Android SDK files are stored in `C:\Android\` (Windows) or `~/Android/` (macOS/Linux) and can consume several GB of disk space. +- **Updating SDK Components:** Run `sdkmanager --update` periodically to keep your SDK tools up to date. \ No newline at end of file diff --git a/docs/en/get-started/layered-web-application.md b/docs/en/get-started/layered-web-application.md index b2bd05454a..f4a383ff0b 100644 --- a/docs/en/get-started/layered-web-application.md +++ b/docs/en/get-started/layered-web-application.md @@ -142,11 +142,11 @@ In this step, you can choose which languages your application will support. * Click Add Custom Language if you want to add a language that is not listed. -You can change these settings later if needed. Thenk click the *Next* button for the *Additional Options* page: +You can change these settings later if needed. Then click the *Next* button for the *Additional Options* page: ![abp-studio-new-solution-dialog-additional-options](images/abp-studio-new-solution-dialog-additional-options_dark.png) -If you uncheck the *Kubernetes Configuration* option, the solution will not include the Kubernetes configuration files, such as Helm charts and other Kubernetes-related files. You can also specify *Social Logins*; if you uncheck this option, the solution will not be configured for social login. Lastly, you can specify the *Include Tests* option to include or exclude the test projects from the solution. +If you uncheck the *Kubernetes Configuration* option, the solution will not include the Kubernetes configuration files, which includes the Helm charts and other Kubernetes-related files. You can also specify *Social Logins*; if you uncheck this option, the solution will not be configured for social login. Lastly, you can specify the *Include Tests* option to include or exclude the test projects from the solution. On the next screen, you can configure the modularity options for your solution: diff --git a/docs/en/get-started/microservice.md b/docs/en/get-started/microservice.md index ee7e31bf85..56f275c258 100644 --- a/docs/en/get-started/microservice.md +++ b/docs/en/get-started/microservice.md @@ -118,7 +118,7 @@ Click the Next button to see *Additional Options* selection: ![abp-studio-new-solution-dialog-additional-options](images/abp-studio-new-solution-dialog-additional-options-microservice.png) -If you unchecked the *Kubernetes Configuration* option, the solution will not include the Kubernetes configuration files which include the Helm charts and other Kubernetes related files. You can also specify *Social Logins*; if you uncheck this option, the solution will not be configured for social login. Lastly, you can specify the *Include Tests* option to include the test projects in the solution. +If you unchecked the *Kubernetes Configuration* option, the solution will not include the Kubernetes configuration files which includes the Helm charts and other Kubernetes-related files. You can also specify *Social Logins*; if you uncheck this option, the solution will not be configured for social login. Lastly, you can specify the *Include Tests* option to include the test projects in the solution. Click the Next button to see *Additional Services* screen: @@ -297,7 +297,7 @@ Clicking the *Connect* button will start a process that establishes the VPN conn ![abp-studio-microservice-kubernetes-services](images/abp-studio-microservice-kubernetes-services.png) -Now, you can access all the services inside the Kubernetes cluster, including the services those are not exposed out of the cluster. You can use the service name as DNS. For example, you can directly visit `http://cloudcrm-local-identity` in your Browser. You can also right-click to a service or application and select the Browse command to open it's UI in the built-in browser of ABP Studio: +Now, you can access all the services inside the Kubernetes cluster, including the services those are not exposed out of the cluster. You can use the service name as DNS. For example, you can directly visit `http://cloudcrm-local-identity` in your Browser. You can also right-click to a service or application and select the Browse command to open its UI in the built-in browser of ABP Studio: ![abp-studio-microservice-kubernetes-services-browse](images/abp-studio-microservice-kubernetes-services-browse.png) diff --git a/docs/en/get-started/pre-requirements.md b/docs/en/get-started/pre-requirements.md index 8aad4818ab..e17b9cd227 100644 --- a/docs/en/get-started/pre-requirements.md +++ b/docs/en/get-started/pre-requirements.md @@ -36,7 +36,7 @@ Visual Studio Code is a **free and cross-platform** lightweight code editor that ## .NET SDK -ABP is based on NET, so you need to install the .NET SDK. You can download the .NET SDK from the [.NET official website](https://dotnet.microsoft.com/en-us/download/dotnet/9.0). +ABP is based on .NET, so you need to install the .NET SDK. You can download the .NET SDK from the [.NET official website](https://dotnet.microsoft.com/en-us/download/dotnet/9.0). > Installing Visual Studio or JetBrains Rider may automatically install the .NET SDK. @@ -56,7 +56,7 @@ dotnet tool update --global dotnet-ef ## Node.js -ABP projects include some frontend resource packages, so you need to install Node.js/NPM manage these resource packages. You can download Node.js from the [official Node.js website](https://nodejs.org/). We recommend installing version v20.11+. +ABP projects include some frontend resource packages, so you need to install Node.js/NPM to manage these resource packages. You can download Node.js from the [official Node.js website](https://nodejs.org/). We recommend installing version v20.11+. ## Yarn (Required Only for Angular Projects) @@ -89,7 +89,7 @@ ABP startup solution templates and tools use some PowerShell scripts (`*.ps1`) t * [Install PowerShell on macOS](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-macos) * [Install PowerShell on Linux](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-linux) -## MicroService Solution +## Microservice Solution The following tools are only required to develop ABP's [microservice solution](../solution-templates/microservice/index.md) diff --git a/docs/en/get-started/single-layer-web-application.md b/docs/en/get-started/single-layer-web-application.md index 9ee87fd439..ef657722b0 100644 --- a/docs/en/get-started/single-layer-web-application.md +++ b/docs/en/get-started/single-layer-web-application.md @@ -66,7 +66,7 @@ Once your configuration is done, click the *Next* button to navigate to the *UI Here, you see all the possible UI options supported by that startup solution template. Pick the **{{ UI_Value }}**. -Notice that; Once you select a UI type, some additional options will be available under the UI Framework list. You can further configure the options or leave them as default and click the Next button for the *Database Provider* selection screen: +Notice that: Once you select a UI type, some additional options will be available under the UI Framework list. You can further configure the options or leave them as default and click the Next button for the *Database Provider* selection screen: {{ if DB == "EF" }} ![abp-studio-new-solution-dialog-database-provider](images/abp-studio-no-layers-new-solution-dialog-database-provider-efcore_dark.png) @@ -110,7 +110,7 @@ In this step, you can choose which languages your application will support. * Click Add Custom Language if you want to add a language that is not listed. -You can change these settings later if needed. Thenk click the *Next* button for the *Additional Options* page: +You can change these settings later if needed. Then click the *Next* button for the *Additional Options* page: ![abp-studio-no-layers-new-solution-additional-options](images/abp-studio-no-layers-new-solution-additional-options_dark.png) diff --git a/docs/en/modules/payment.md b/docs/en/modules/payment.md index 29654a2d42..487e0dc64d 100644 --- a/docs/en/modules/payment.md +++ b/docs/en/modules/payment.md @@ -15,9 +15,9 @@ Payment module implements payment gateway integration of an application. It prov See [the module description page](https://abp.io/modules/Volo.Payment) for an overview of the module features. -## How to install +## How to Install -Payment module is not installed in [the startup templates](../solution-templates/layered-web-application). So, it needs to be installed manually. There are two ways of installing a module into your application. +The Payment module is not installed in [the startup templates](../solution-templates/layered-web-application). So, it needs to be installed manually. There are two ways of installing a module into your application. ### Using ABP CLI @@ -30,7 +30,7 @@ abp add-module Volo.Payment ### Manual Installation -If you modified your solution structure, adding module using ABP CLI might not work for you. In such cases, payment module can be added to a solution manually. +If you modified your solution structure, adding a module using ABP CLI might not work for you. In such cases, the Payment module can be added to a solution manually. In order to do that, add packages listed below to matching project on your solution. For example, ```Volo.Payment.Application``` package to your **{ProjectName}.Application.csproj** like below; @@ -47,7 +47,9 @@ After adding the package reference, open the module class of the project (eg: `{ )] ``` -> If you are using Blazor Web App, you need to add the `Volo.Payment.Admin.Blazor.WebAssembly` package to the **{ProjectName}.Blazor.Client.csproj** project and ad the `Volo.Payment.Admin.Blazor.Server` package to the **{ProjectName}.Blazor.csproj** project. +> If you are using Blazor Web App, you need to add the `Volo.Payment.Admin.Blazor.WebAssembly` package to the **{ProjectName}.Blazor.Client.csproj** project and add the `Volo.Payment.Admin.Blazor.Server` package to the **{ProjectName}.Blazor.csproj** project. + +> For Blazor UI public pages (payment gateway selection, pre-payment, and post-payment pages), see the [Blazor UI](#blazor-ui) section below for detailed installation and configuration instructions. ### Supported Gateway Packages @@ -55,7 +57,8 @@ In order to use a Payment Gateway, you need to add related NuGet packages to you After adding packages of a payment gateway to your application, you also need to configure global payment module options and options for the payment modules you have added. See the Options section below. -### Creating a custom payment gateway +### Creating Custom Payment Gateways + If you require a different payment gateway than existing ones, you can create a custom payment gateway by your own. 2 steps are required to create a custom payment gateway. First is creating a payment gateway object that implements `IPaymentGateway`. This interface exposes core payment operations without any UI. Second step is creating UI for the payment gateway. This UI is used to redirect user to payment gateway and validate payment. Follow the [instructions here](payment-custom-gateway) to create a custom payment gateway. @@ -66,27 +69,291 @@ This module follows the [module development best practices guide](../framework/a You can visit [Payment module package list page](https://abp.io/packages?moduleName=Volo.Payment) to see list of packages related with this module. -## User interface +## User Interface -### Public Pages +The Payment module provides both **public pages** (for payment processing) and **admin pages** (for managing payment plans and requests). The UI is available for **MVC/Razor Pages**, **Blazor**, and **Angular** applications. See the UI-specific sections below for installation and configuration details. -#### Payment gateway selection +### MVC / Razor Pages UI -This page allows selecting a payment gateway. If there is one payment gateway configured for final application, this page will be skipped. +For MVC/Razor Pages applications, the `abp add-module Volo.Payment` command automatically adds the required packages (`Volo.Payment.Web` and gateway-specific Web packages) and the necessary `DependsOn` statements to your module. The only thing you need to do is configure `PaymentWebOptions` as explained in the [PaymentWebOptions](#paymentweboptions) section. -![payment-gateway-selection](../images/payment-gateway-selection.png) +### Blazor UI -#### PayU prepayment page +For Blazor applications, the `abp add-module Volo.Payment` command automatically adds the required packages (`Volo.Payment.Blazor.Server` or `Volo.Payment.Blazor.WebAssembly` and gateway-specific Blazor packages) and the necessary `DependsOn` statements to your module. The only thing you need to do is configure `PaymentBlazorOptions` as explained below. -This page is used to send Name, Surname and Email Address of user to PayU. +#### Installation -![payment-payu-prepayment-page](../images/payment-payu-prepayment-page.png) +> **Note:** If you used the `abp add-module Volo.Payment` command to install the Payment module, the following packages and module dependencies are automatically added to your project. You can skip to the [Gateway-Specific Blazor Packages](#gateway-specific-blazor-packages) section. The information below is provided for reference or manual installation scenarios. + +To use the Payment module's public pages in a Blazor application, you need to install the core Blazor packages and the gateway-specific Blazor packages for each payment gateway you want to support. + +##### Core Blazor Packages + +For **Blazor Server** applications, add the following package to your **{ProjectName}.Blazor.Server.csproj** (or **{ProjectName}.Blazor.csproj** for Blazor Web App): + +```json + +``` + +For **Blazor WebAssembly** applications, add the following package to your **{ProjectName}.Blazor.csproj** (or **{ProjectName}.Blazor.Client.csproj** for Blazor Web App): + +```json + +``` + +#### Gateway-Specific Blazor Packages + +Each payment gateway requires its own Blazor package. Add the packages for the gateways you want to support: + +**Stripe:** +- Blazor Server: `Volo.Payment.Stripe.Blazor.Server` +- Blazor WebAssembly: `Volo.Payment.Stripe.Blazor.WebAssembly` + +**PayPal:** +- Blazor Server: `Volo.Payment.PayPal.Blazor.Server` +- Blazor WebAssembly: `Volo.Payment.PayPal.Blazor.WebAssembly` + +**PayU:** +- Blazor Server: `Volo.Payment.PayU.Blazor.Server` +- Blazor WebAssembly: `Volo.Payment.PayU.Blazor.WebAssembly` + +**Iyzico:** +- Blazor Server: `Volo.Payment.Iyzico.Blazor.Server` +- Blazor WebAssembly: `Volo.Payment.Iyzico.Blazor.WebAssembly` +- HttpApi (required for callbacks): `Volo.Payment.Iyzico.HttpApi` + +> **Important:** Iyzico requires the `Volo.Payment.Iyzico.HttpApi` package because Blazor cannot directly handle POST requests from external payment gateways. This package provides an API endpoint to receive the POST callback from Iyzico and redirect to the Blazor post-payment page. + +**Alipay:** +- Blazor Server: `Volo.Payment.Alipay.Blazor.Server` +- Blazor WebAssembly: `Volo.Payment.Alipay.Blazor.WebAssembly` + +**TwoCheckout:** +- Blazor Server: `Volo.Payment.TwoCheckout.Blazor.Server` +- Blazor WebAssembly: `Volo.Payment.TwoCheckout.Blazor.WebAssembly` + +##### Module Dependencies + +After adding the package references, add the module dependencies to your Blazor module class. For example, for a Blazor Server application with Stripe and PayPal: + +```csharp +[DependsOn( + // ... other dependencies + typeof(AbpPaymentBlazorServerModule), + typeof(AbpPaymentStripeBlazorServerModule), + typeof(AbpPaymentPayPalBlazorServerModule) +)] +public class YourBlazorModule : AbpModule +{ + // ... +} +``` + +For Blazor WebAssembly: + +```csharp +[DependsOn( + // ... other dependencies + typeof(AbpPaymentBlazorWebAssemblyModule), + typeof(AbpPaymentStripeBlazorWebAssemblyModule), + typeof(AbpPaymentPayPalBlazorWebAssemblyModule) +)] +public class YourBlazorModule : AbpModule +{ + // ... +} +``` + +#### Configuration + +Configure `PaymentBlazorOptions` in your Blazor module's `ConfigureServices` method: + +```csharp +Configure(options => +{ + options.RootUrl = configuration["App:SelfUrl"]; + options.CallbackUrl = configuration["App:SelfUrl"] + "/PaymentSucceed"; + options.GatewaySelectionCheckoutButtonStyle = "btn btn-primary"; // Optional CSS class +}); +``` + +You can also configure these options in your `appsettings.json` file: + +```json +{ + "Payment": { + "Blazor": { + "RootUrl": "https://localhost:44300", + "CallbackUrl": "https://localhost:44300/PaymentSucceed", + "GatewaySelectionCheckoutButtonStyle": "btn btn-primary" + } + } +} +``` + +##### Gateway-Specific Blazor Options + +Each payment gateway has its own Blazor options for customizing the UI. These options can be configured in `appsettings.json`: + +```json +{ + "Payment": { + "Blazor": { + "Payu": { + "PrePaymentCheckoutButtonStyle": "btn btn-success", + "Recommended": true, + "ExtraInfos": ["Fast checkout", "Secure payment"] + }, + "TwoCheckout": { + "Recommended": false, + "ExtraInfos": ["International payments"] + }, + "PayPal": { + "Recommended": true, + "ExtraInfos": ["Pay with PayPal balance", "Buyer protection"] + }, + "Stripe": { + "Recommended": true, + "ExtraInfos": ["Credit/Debit cards", "Apple Pay", "Google Pay"] + }, + "Iyzico": { + "PrePaymentCheckoutButtonStyle": "btn btn-primary", + "Recommended": false, + "ExtraInfos": ["Turkish payment gateway"] + }, + "Alipay": { + "PrePaymentCheckoutButtonStyle": "btn btn-info", + "Recommended": false, + "ExtraInfos": ["Chinese payment gateway", "CNY only"] + } + } + } +} +``` -### Admin Pages +#### Creating Payments in Blazor + +To initiate a payment in a Blazor component, inject `IPaymentRequestAppService` and `NavigationManager`, create a payment request, and navigate to the gateway selection page: + +```csharp +@page "/" +@using Microsoft.AspNetCore.Components +@using Volo.Payment +@using Volo.Payment.Requests +@inject IPaymentRequestAppService PaymentRequestAppService +@inject NavigationManager NavigationManager + + + +@code { + private async Task StartPaymentAsync() + { + var paymentRequest = await PaymentRequestAppService.CreateAsync( + new PaymentRequestCreateDto + { + Currency = "USD", + Products = new List + { + new PaymentRequestProductCreateDto + { + Code = "Product_01", + Name = "LEGO Super Mario", + Count = 2, + UnitPrice = 60, + TotalPrice = 120 + } + } + }); + + NavigationManager.NavigateTo($"/Payment/GatewaySelection?paymentRequestId={paymentRequest.Id}"); + } +} +``` + +##### Passing Extra Parameters + +Some payment gateways require additional parameters. You can pass these as extra properties when creating the payment request: + +```csharp +var paymentRequest = await PaymentRequestAppService.CreateAsync( + new PaymentRequestCreateDto + { + Currency = "USD", + Products = new List + { + new PaymentRequestProductCreateDto + { + Code = "Product_01", + Name = "LEGO Super Mario", + Count = 1, + UnitPrice = 60, + TotalPrice = 60 + } + }, + ExtraProperties = new ExtraPropertyDictionary + { + // For Iyzico - Customer information + { "Name", "John" }, + { "Surname", "Doe" }, + { "Email", "john.doe@example.com" }, + { "Address", "123 Main St" }, + { "City", "Istanbul" }, + { "Country", "Turkey" }, + { "ZipCode", "34000" }, + + // For PayU - Customer information + { "BuyerName", "John" }, + { "BuyerSurname", "Doe" }, + { "BuyerEmail", "john.doe@example.com" } + } + }); +``` + +#### Handling the Callback (Optional) + +When a user completes a payment on the external payment gateway, the following flow occurs: + +1. The user is redirected to the **PostPayment page** (handled internally by the payment module) +2. The PostPayment page validates the payment with the gateway and updates the payment request status to **Completed** +3. If a `CallbackUrl` is configured in `PaymentBlazorOptions`, the user is then redirected to that URL with the `paymentRequestId` as a query parameter + +Create a page to handle this callback and perform any application-specific actions: + +```csharp +@page "/PaymentSucceed" +@using Microsoft.AspNetCore.WebUtilities + +

Payment Successful!

+

Thank you for your purchase.

+

Payment Request ID: @PaymentRequestId

+ +@code { + [Parameter] + [SupplyParameterFromQuery] + public Guid? PaymentRequestId { get; set; } + + protected override async Task OnInitializedAsync() + { + if (PaymentRequestId.HasValue) + { + // The payment is already completed at this point. + // Perform application-specific actions here: + // e.g., activate subscription, send confirmation email, + // update order status, grant access to purchased content, etc. + } + } +} +``` + +> **Note:** By the time the user reaches your callback page, the payment request status has already been set to **Completed** by the PostPayment page. Your callback page is for performing additional application-specific logic. It is also your responsibility to handle if a payment request is used more than once. If you have already delivered your product for a given `PaymentRequestId`, you should not deliver it again when the callback URL is visited a second time. ### Angular UI -#### Installation +For Angular applications, you need to read and apply the steps explained in the following sections: + +#### Configurations In order to configure the application to use the payment module, you first need to import `PaymentAdminConfigModule` from `@volo/abp.ng.payment/admin/config` to the root configuration. `PaymentAdminConfigModule` has a static `forRoot` method which you should call for a proper configuration: @@ -120,15 +387,35 @@ const APP_ROUTES: Routes = [ ]; ``` -#### Payment plans page +### Pages + +#### Public Pages + +##### Payment Gateway Selection + +This page allows selecting a payment gateway. If there is only one payment gateway configured for the application, this page will be skipped. + +![payment-gateway-selection](../images/payment-gateway-selection.png) + +##### PrePayment Page + +Some payment gateways require additional information before redirecting to the payment gateway. For example, PayU and Iyzico require customer information (Name, Surname, Email Address, etc.) before processing the payment. + +![payment-payu-prepayment-page](../images/payment-payu-prepayment-page.png) + +#### Admin Pages + +##### Payment Plans Page + Payment plans for subscriptions can be managed on this page. You can connect external subscriptions for each gateway to a plan. ![payment plans](../images/payment-plans.png) ![payment plans gateway plans](../images/payment-plans-gateway-plans.png) -#### Payment request list -This page lists all the payment request operations in application. +##### Payment Request List + +This page lists all the payment request operations in the application. ![payment request list](../images/payment-request-list.png) @@ -171,7 +458,22 @@ Configure(options => * ```PrePaymentUrl```: URL of the page before redirecting user to payment gateway for payment. * ```PostPaymentUrl```: URL of the page when user redirected back from payment gateway to your website. This page is used to validate the payment mostly. * ```Order```: Order of payment gateway for gateway selection page. - * ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page. + * ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. + * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. + +### PaymentBlazorOptions + +```PaymentBlazorOptions``` is used to configure Blazor application related configurations. This is the Blazor equivalent of `PaymentWebOptions`. + +* ```CallbackUrl```: Final callback URL for internal payment gateway modules to return. User will be redirected to this URL on your website after a successful payment. +* ```RootUrl```: Root URL of your Blazor application. +* ```GatewaySelectionCheckoutButtonStyle```: CSS style to add to the Checkout button on the gateway selection page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. +* ```PaymentGatewayBlazorConfigurationDictionary```: Used to store Blazor related payment gateway configuration. + * ```Name```: Name of payment gateway. + * ```PrePaymentUrl```: URL of the Blazor page before redirecting user to payment gateway for payment. + * ```PostPaymentUrl```: URL of the Blazor page when user is redirected back from payment gateway to your website. + * ```Order```: Order of payment gateway for gateway selection page. + * ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### PayuOptions @@ -193,9 +495,17 @@ Configure(options => ```PayuWebOptions``` is used to configure PayU payment gateway web options. -* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page. +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. -* ```PrePaymentCheckoutButtonStyle```: Css style to add Checkout button on PayU prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. +* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the PayU prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. + +### PayuBlazorOptions + +```PayuBlazorOptions``` is used to configure PayU payment gateway Blazor options. + +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the PayU prepayment page. ### TwoCheckoutOptions @@ -210,7 +520,14 @@ Configure(options => ```TwoCheckoutWebOptions``` is used to configure TwoCheckout payment gateway web options. -* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page. +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. + +### TwoCheckoutBlazorOptions + +```TwoCheckoutBlazorOptions``` is used to configure TwoCheckout payment gateway Blazor options. + +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### StripeOptions @@ -228,7 +545,14 @@ Configure(options => ```StripeWebOptions``` is used to configure Stripe payment gateway web options. -* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page. +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. + +### StripeBlazorOptions + +```StripeBlazorOptions``` is used to configure Stripe payment gateway Blazor options. + +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### PayPalOptions @@ -245,7 +569,14 @@ Configure(options => ```PayPalWebOptions``` is used to configure PayPal payment gateway web options. -* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page. +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. + +### PayPalBlazorOptions + +```PayPalBlazorOptions``` is used to configure PayPal payment gateway Blazor options. + +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### IyzicoOptions @@ -263,9 +594,17 @@ Configure(options => ```IyzicoWebOptions``` is used to configure Iyzico payment gateway web options. -* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page. +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Iyzico prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. + +### IyzicoBlazorOptions + +```IyzicoBlazorOptions``` is used to configure Iyzico payment gateway Blazor options. + +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. -* ```PrePaymentCheckoutButtonStyle```: CSS style to add Checkout button on Iyzico prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. +* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Iyzico prepayment page. ### AlipayOptions @@ -285,9 +624,17 @@ Configure(options => #### AlipayWebOptions -* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page. +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Alipay prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. + +#### AlipayBlazorOptions + +```AlipayBlazorOptions``` is used to configure Alipay payment gateway Blazor options. + +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. -* ```PrePaymentCheckoutButtonStyle```: CSS style to add Checkout button on Iyzico prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. +* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Alipay prepayment page. > You can check the [Alipay document](https://opendocs.alipay.com/open/02np97) for more details. @@ -541,7 +888,7 @@ PrePayment page asks users for extra information if requested by the external pa PostPayment page is responsible for validation of the response of the external payment gateway. When a user completes the payment, user is redirected to PostPayment page for that payment gateway and PostPayment page validates the status of the payment. If the payment is succeeded, status of the payment request is updated and user is redirected to main application. -Note: It is main application's responsibility to handle if a payment request is used more than one time. For example if PostPayment page generates a URL like https://mywebsite.com/PaymentSucceed?PaymentRequestId={PaymentRequestId}, this URL can be visited more than onec manually by end users. If you already delivered your product for a given PaymentRequestId, you shouldn't deliver it when this URL is visited second time. +Note: It is the main application's responsibility to handle if a payment request is used more than once. For example, if the PostPayment page generates a URL like https://mywebsite.com/PaymentSucceed?PaymentRequestId={PaymentRequestId}, this URL can be visited more than once manually by end users. If you have already delivered your product for a given PaymentRequestId, you shouldn't deliver it when this URL is visited a second time. ### Creating One-Time Payment @@ -563,7 +910,7 @@ public class IndexModel: PageModel { var paymentRequest = await _paymentRequestAppService.CreateAsync(new PaymentRequestCreateDto() { - Currency= "USD", + Currency = "USD", Products = new List() { new PaymentRequestProductCreateDto @@ -582,7 +929,7 @@ public class IndexModel: PageModel } ``` -If the payment is successful, payment module will return to configured ```PaymentWebOptions.CallbackUrl```. The main application can take necessary actions for a successful payment (Activating a user account, triggering a shipment start process etc...). +If the payment is successful, payment module will return to the configured ```PaymentWebOptions.CallbackUrl```. The main application can take necessary actions for a successful payment (activating a user account, triggering a shipment start process, etc.). ## Subscriptions diff --git a/docs/en/release-info/index.md b/docs/en/release-info/index.md index e3d3aa56cd..b7da966e43 100644 --- a/docs/en/release-info/index.md +++ b/docs/en/release-info/index.md @@ -1,16 +1,135 @@ ```json //[doc-seo] { - "Description": "Explore the latest ABP Framework release information, including notes, migration guides, and upgrading tips to enhance your development experience." + "Description": "Understand ABP Platform's versioning, release schedule, LTS support policy, and how we handle breaking changes to ensure smooth upgrades for your applications." } ``` -# Release Information +# Versioning & Releases -* [Release Notes](./release-notes.md) -* [Migration Guides](./migration-guides/index.md) -* [Road Map](./road-map.md) -* [Upgrading](./upgrading.md) -* [Preview Releases](./previews.md) -* [Nightly Releases](./nightly-builds.md) -* [Official Packages](https://abp.io/packages) \ No newline at end of file +ABP Platform follows a predictable release cycle aligned with .NET releases. This document explains our versioning strategy, release schedule, support policy, and how we handle breaking changes. + +## Our Commitment + +As a framework you build upon, ABP must be both reliable and evolving — stable enough to trust for long-term projects, yet continuously improving with new features and the latest .NET advancements. To achieve this balance, we commit to: + +* **Predictable releases**: Major versions annually, aligned with .NET releases +* **Long-term support**: Every major version receives 2 years of support +* **Smooth upgrades**: Comprehensive migration guides and tooling for version updates +* **Transparent communication**: Clear documentation of breaking changes and deprecations + +## ABP Versioning + +ABP version numbers indicate the level of changes that are introduced by the release. This use of [semantic versioning](https://semver.org/) helps you understand the potential impact of updating to a new version. + +ABP version numbers have three parts: `major.minor.patch`. For example, version 10.1.2 indicates major version 10, minor version 1, and patch level 2. + +The version number is incremented based on the level of change included in the release. + +| Level of change | Details | +| --- | --- | +| **Major release** | Contains significant new features. Aligned with the new major .NET release. Some developer assistance is expected. You should check the [migration guide](migration-guides/index.md) and possibly refactor code to adapt to new APIs. | +| **Minor release** | Contains new features and improvements. Minor releases are generally backward-compatible; minimal developer assistance is expected, but you can optionally modify your applications to begin using new APIs and features. | +| **Patch release** | Low risk, bug fix and security patch release. No developer assistance is expected. | + +> **Note:** ABP version is aligned with .NET version. For example, ABP 10.x runs on .NET 10, ABP 9.x runs on .NET 9. + +### Preview Releases + +We provide preview releases for each major and minor release so you can try new features before the stable release: + +| Pre-release type | Details | +| --- | --- | +| **Release Candidate (RC)** | A release that is feature complete and in final testing. RC releases are indicated by a release tag appended with the `-rc` identifier, such as `10.1.0-rc.1`. | +| **Nightly builds** | The latest development builds published every weekday night. Nightly builds allow you to try the previous day's development. | + +See the [Preview Releases](previews.md) and [Nightly Builds](nightly-builds.md) documents for more information. + +## Release Frequency + +We work toward a regular schedule of releases, so that you can plan and coordinate your updates with the continuing evolution of ABP and the .NET platform. + +> **Note:** Dates are offered as general guidance and are subject to change. + +In general, expect the following release cycle: + +* **A major release once a year**, typically in November, following the new major .NET release +* **2-4 minor releases** for each major version, released every ~3 months after the major release +* **Patch releases** as needed, typically every 2-4 weeks for the latest minor version + +This cadence of releases gives eager developers access to new features as soon as they are fully developed and tested, while maintaining the stability and reliability of the platform for production users. + +### Release Schedule + +| Version | Status | Released | Active Ends | LTS Ends | +| --- | --- | --- | --- | --- | +| ^10.0.0 | Active | 2025-11 | 2026-11 | 2027-11 | +| ^9.0.0 | LTS | 2024-11 | 2025-11 | 2026-11 | + + +See the [Release Notes](release-notes.md) for detailed information about each release. + +## Support Policy and Schedule (LTS) + +ABP Platform follows a **Long-Term Support (LTS)** policy to ensure your applications remain secure and stable over time. + +### Support Window + +Every major version has a **2-year lifecycle** with two distinct phases: + +| Support Stage | Duration | Details | +| --- | --- | --- | +| **Active** | ~1 year | The version is under active development. Regularly-scheduled updates and patches are released. New features and improvements are added in minor versions. | +| **Long-Term Support (LTS)** | ~1 year | Only critical fixes and security patches are released. No new features are added. | + +This means we actively develop a major version for about 1 year (until the next major .NET release), then provide LTS support for another year. + +### LTS Fixes + +As a general rule, a fix is considered for an LTS version if it resolves one of: + +* A newly identified security vulnerability +* A critical bug that significantly impacts production applications +* A regression caused by a 3rd party change, such as a new browser version or dependency update + +## Deprecation Policy + +When the ABP team intends to remove an API or feature, it will be marked as *deprecated*. This occurs when an API is obsolete, superseded by another API, or otherwise discontinued. Deprecated APIs remain available through their deprecated phase, which lasts a minimum of one major version (approximately one year). + +To help ensure that you have sufficient time and a clear path to update, this is our deprecation policy: + +| Deprecation Stage | Details | +| --- | --- | +| **Announcement** | We announce deprecated APIs and features in the [release notes](release-notes.md) and [migration guides](migration-guides/index.md). Deprecated APIs are typically marked with `[Obsolete]` attribute in the code, which enables IDEs to provide warnings if your project depends on them. We also announce a recommended update path. | +| **Deprecation period** | When an API or feature is deprecated, it is still present in at least the next major release. After that, deprecated APIs and features are candidates for removal. A deprecation can be announced in any release, but the removal of a deprecated API or feature happens only in major releases. | +| **NuGet/NPM dependencies** | We typically make dependency updates that require changes to your applications in major releases. In minor releases, we may update dependencies by expanding the supported versions, but we try not to require projects to update these dependencies until the next major version. | + +## Breaking Changes Policy + +Breaking changes require you to do work because the state after the change is not backward compatible with the state before it. Examples of breaking changes include the removal of public APIs, changes to method signatures, changing the timing of events, or updating to a new version of a dependency that includes breaking changes itself. + +### How We Handle Breaking Changes + +To support you in case of breaking changes: + +* We follow our [deprecation policy](#deprecation-policy) before we remove a public API +* We provide detailed [migration guides](migration-guides/index.md) when a version includes breaking changes + +### Update Path + +We recommend updating one major version at a time for a smoother upgrade experience. For example, to update from version 8.x to version 10.x: + +1. Update from version 8.x to version 9.x +2. Update from version 9.x to version 10.x + +See the [upgrading](upgrading.md) document for detailed instructions on how to upgrade your solutions. + +## Related Documents + +* [Release Notes](release-notes.md) - Detailed release notes for each version +* [Migration Guides](migration-guides/index.md) - Step-by-step guides for upgrading between versions +* [Road Map](road-map.md) - Upcoming features and planned releases +* [Upgrading](upgrading.md) - How to upgrade your ABP-based solutions +* [Preview Releases](previews.md) - Information about preview/RC releases +* [Nightly Builds](nightly-builds.md) - How to use nightly builds +* [Official Packages](https://abp.io/packages) - Browse all ABP packages diff --git a/docs/en/release-info/migration-guides/abp-10-1.md b/docs/en/release-info/migration-guides/abp-10-1.md new file mode 100644 index 0000000000..dce45aff64 --- /dev/null +++ b/docs/en/release-info/migration-guides/abp-10-1.md @@ -0,0 +1,52 @@ +```json +//[doc-seo] +{ + "Description": "Upgrade your ABP solutions from v10.0 to v10.1 with this comprehensive migration guide, ensuring compatibility and new features with ABP v10.1." +} +``` + +# ABP Version 10.1 Migration Guide + +This document is a guide for upgrading ABP v10.0 solutions to ABP v10.1. There are some changes in this version that may affect your applications. Please read them carefully and apply the necessary changes to your application. + +## Open-Source (Framework) + +### Add New EF Core Migrations for Password History/User Passkey Entities + +In this version, we added password history/ user passkeys support to the [Identity PRO Module](../../modules/identity-pro.md) to enhance security compliance. A new `IdentityUserPasswordHistory` entity has been added to store previous password hashes, preventing users from reusing recent passwords. Additionally, we have introduced an `IdentityUserPasskey `entity to support passkey-based authentication. + +**You need to create a new EF Core migration and apply it to your database** after upgrading to ABP 10.1. + +> See [#23894](https://github.com/abpframework/abp/pull/23894) for more details. + +## PRO + +> Please check the **Open-Source (Framework)** section before reading this section. The listed topics might affect your application and you might need to take care of them. + +If you are a paid-license owner and using the ABP's paid version, then please follow the following sections to get informed about the breaking changes and apply the necessary ones: + +### AI Management Module: `Workspace` Entity Base Class Changed + +In this version, the `Workspace` entity in the [AI Management Module](../../modules/ai-management/index.md) has been changed from `FullAuditedAggregateRoot` to `AuditedAggregateRoot` as the base class. + +**This change removes support for soft deletion and related auditing features. If you are using the AI Management module, you need to create a new EF Core migration and apply it to your database.** + +> **Important:** If you have soft-deleted Workspaces in your database, they will become visible after this update. You may need to create a migration script to clean up already deleted records before applying the migration. + +### CMS Kit Pro Module: Dynamic FAQ Group Management + +In this version, the FAQ group system in the [CMS Kit Pro Module](../../modules/cms-kit-pro/faq.md) has been redesigned to support dynamic group management. FAQ groups are now **first-class entities** stored in the database, replacing the previous static configuration approach. + +**Key Changes:** + +- A new `FaqGroup` entity has been introduced with unique names for FAQ groups +- The `FaqSection` entity now uses `GroupId` (Guid) instead of `GroupName` (string) _(GroupName is deprecated and will be removed soon. Use GroupId instead.)_ +- Static FAQ group configuration (`FaqOptions.SetGroups`) has been removed + +**Migration Steps:** + +1. **Remove static group configuration** from your code (e.g., `Configure(options => { options.SetGroups([...]); })`) +2. **Create a new EF Core migration and apply it to your database** +3. **Run the one-time data migration seeder** to migrate existing FAQ sections to the new group entity model + +> **Note:** If you have existing FAQ data, you may need to create a data seeder to migrate your existing group associations to the new entity-based model. diff --git a/docs/en/release-info/migration-guides/index.md b/docs/en/release-info/migration-guides/index.md index f0d8a78b84..61bb095434 100644 --- a/docs/en/release-info/migration-guides/index.md +++ b/docs/en/release-info/migration-guides/index.md @@ -9,6 +9,7 @@ The following documents explain how to migrate your existing ABP applications. We write migration documents only if you need to take an action while upgrading your solution. Otherwise, you can easily upgrade your solution using the [abp update command](../upgrading.md). +- [10.0 to 10.1](abp-10-1.md) - [9.x to 10.0](abp-10-0.md) - [9.2 to 9.3](abp-9-3.md) - [9.x to 9.2](abp-9-2.md) diff --git a/docs/en/release-info/release-notes.md b/docs/en/release-info/release-notes.md index bf335fa7a2..08d0bcde31 100644 --- a/docs/en/release-info/release-notes.md +++ b/docs/en/release-info/release-notes.md @@ -14,8 +14,23 @@ Also see the following notes about ABP releases: * [ABP Studio release notes](../studio/release-notes.md) * [Change logs for ABP pro packages](https://abp.io/pro-releases) +## 10.1 (2026-01-06) + +> This is currently a RC (release-candidate) and you can see the detailed **[blog post / announcement](https://abp.io/community/announcements/announcing-abp-10-1-release-candidate-cyqui19d)** for the v10.1 release. + +* Resource-Based Authorization +* Introducing the [TickerQ Background Worker Provider](../framework/infrastructure/background-workers/tickerq.md) +* Angular UI: Version Upgrade to **v21** +* [File Management Module](../modules/file-management.md): Public File Sharing Support +* [Payment Module](../modules/payment.md): Public Page Implementation for Blazor & Angular UIs +* [AI Management Module](../modules/ai-management/index.md) for Blazor & Angular UIs +* [Identity PRO Module](../modules/identity-pro.md): Password History Support +* [Account PRO Module](../modules/account-pro.md): Introducing WebAuthn Passkeys + ## 10.0 (2025-11-18) +> **Note**: ABP has upgraded to .NET 10.0, so if you plan to use ABP 10.0, you’ll need to migrate your solutions to .NET 10.0. You can refer to the [Migrate from ASP.NET Core 9.0 to 10.0](https://learn.microsoft.com/en-us/aspnet/core/migration/90-to-100) documentation for guidance. However, ABP’s NuGet packages are compatible with both .NET 9 and .NET 10, allowing developers to continue using .NET 9 while still enjoying the latest features and improvements of the ABP Framework without upgrading their SDK. + See the detailed **[blog post / announcement](https://abp.io/community/announcements/abp.io-platform-10.0-final-has-been-released-spknn925)** for the v10.0 release. * Upgraded to .NET 10.0 diff --git a/docs/en/release-info/road-map.md b/docs/en/release-info/road-map.md index 0df94ec157..70a9b392c4 100644 --- a/docs/en/release-info/road-map.md +++ b/docs/en/release-info/road-map.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Explore the ABP Platform Road Map for insights on upcoming features, release schedules, and improvements in version 9.1, launching January 2025." + "Description": "Explore the ABP Platform Road Map for insights on upcoming features, release schedules, and improvements in version 10.1, launching January 2026." } ``` @@ -11,33 +11,35 @@ This document provides a road map, release schedule, and planned features for th ## Next Versions -### v10.1 +### v10.2 -The next version will be 10.1 and planned to release the stable 10.1 version in January 2026. We will be mostly working on the following topics: +The next version will be 10.2 and planned to release the stable 10.2 version in April 2026. We will be mostly working on the following topics: * Framework - * OpenTelemetry Protocol Support for 3rd-party Integrations - * Resource Based Authorization Integration + * Resource-Based Authorization Improvements + * Handle datetime/timezon in `AbpExtensibleDataGrid` Component * Upgrading 3rd-party Dependencies * Enhancements in the Core Points * ABP Suite - * Define Navigation Properties Without Target String Property Dependency + * Creating enums on-the-fly (without needing to create manually on the code side) + * Improvements on the generated codes for nullability * Improvements on Master-Detail Page Desing (making it more compact) * Improvements One-To-Many Scenarios * File Upload Modal Enhancements * ABP Studio * Allow to Directly Create New Solutions with ABP's RC (Release Candidate) Versions + * Integrate AI Management Module with all solution templates and UIs * Automate More Details on New Service Creation for a Microservice Solution * Allow to Download ABP Samples from ABP Studio - * Task Panel Enhancements (and Documentation) + * Task Panel Documentation * Support Multiple Concurrent Kubernetes Deployment/Integration Scenarios * Improve the Module Installation Experience / Installation Guides * Application Modules - * Payment Module: Public Page Implementation (for Blazor & Angular UIs) - * AI Management Module: UI Implementation for Blazor & Angular UIs + * AI Management: MCP & RAG Supports + * File Management: Using Resource-Based Permission (on file-sharing and more...) * CMS Kit: Enhancements for Some Features (Rating, Dynamic Widgets, FAQ and more...) * UI/UX Improvements on Existing Application Modules diff --git a/docs/en/release-info/upgrading.md b/docs/en/release-info/upgrading.md index 88ce1249d8..974d7ef2d5 100644 --- a/docs/en/release-info/upgrading.md +++ b/docs/en/release-info/upgrading.md @@ -21,6 +21,8 @@ Run this command in the terminal while you are in the root folder of your soluti > If your solution has the Angular UI, you probably have `aspnet-core` and `angular` folders in the solution. Run this command in the parent folder of these two folders. +You can also specify a target version with `--version` parameter. See the [ABP CLI update command](../cli/index.md#update) for all available options. + ### Database Migrations > Warning: Be careful if you are migrating your database since you may have data loss in some cases. Carefully check the generated migration code before executing it. It is suggested to take a backup of your current database. diff --git a/docs/en/studio/concepts.md b/docs/en/studio/concepts.md index 1e6afb4fab..1530f3a029 100644 --- a/docs/en/studio/concepts.md +++ b/docs/en/studio/concepts.md @@ -43,6 +43,45 @@ An ABP Studio module is a sub-solution that contains zero, one or multiple packa An ABP Studio Package typically matches to a .NET project (`csproj`). +### Metadata + +Metadata is a collection of key-value pairs that provide additional information for various ABP Studio features. Metadata follows a hierarchical structure where values defined at lower levels override those at higher levels: + +**Hierarchy (from highest to lowest priority):** +1. **Helm Chart Metadata** - Defined in chart properties (Kubernetes context only) +2. **Kubernetes Profile Metadata** / **Run Profile Metadata** - Defined in profile settings (context-dependent) +3. **Solution Metadata** - Defined via *Solution Explorer* → right-click solution → *Manage Metadata* +4. **Global Metadata** - Defined via *Tools* → *Global Metadata* + +**Common Metadata Keys:** + +| Key | Description | Used By | +|-----|-------------|---------| +| `k8ssuffix` | Appends a suffix to Kubernetes namespace (e.g., for multi-developer scenarios) | Kubernetes integration | +| `dotnetEnvironment` | Specifies the .NET environment (e.g., `Development`, `Staging`) | Helm chart installation | +| `projectPath` | Path to the project for Docker image building | Docker image build | +| `imageName` | Docker image name | Docker image build | +| `projectType` | Project type (`dotnet` or `angular`) | Docker image build | + +> Metadata defined in *Global Metadata* is available for all solutions but will not be shared with team members. Metadata defined at *Solution* or *Profile* level will be shared through solution files. + +### Secrets + +Secrets are key-value pairs designed for storing sensitive information such as passwords, API keys, and connection strings. Unlike metadata, secrets are stored in the local file system and are not included in solution files for security reasons. + +**Hierarchy (from highest to lowest priority):** +1. **Kubernetes Profile Secrets** / **Run Profile Secrets** - Defined in profile settings (context-dependent) +2. **Solution Secrets** - Defined via *Solution Explorer* → right-click solution → *Manage Secrets* +3. **Global Secrets** - Defined via *Tools* → *Global Secrets* + +**Common Secret Keys:** + +| Key | Description | Used By | +|-----|-------------|---------| +| `wireGuardPassword` | Password for WireGuard VPN connection to Kubernetes cluster | Kubernetes integration | + +> Secrets are stored locally and are not shared with team members by default. Each developer needs to configure their own secrets. + ## ABP Studio vs .NET Terms Some ABP Studio terms may seem conflict with .NET and Visual Studio. To make them even more clear, you can use the following table. diff --git a/docs/en/studio/images/overview/ai-assistant.png b/docs/en/studio/images/overview/ai-assistant.png new file mode 100644 index 0000000000..156c354680 Binary files /dev/null and b/docs/en/studio/images/overview/ai-assistant.png differ diff --git a/docs/en/studio/kubernetes.md b/docs/en/studio/kubernetes.md index ae23989f6c..758a7cf55c 100644 --- a/docs/en/studio/kubernetes.md +++ b/docs/en/studio/kubernetes.md @@ -100,7 +100,7 @@ It is the root of all subcharts. When you add a new main chart to the root, it i - `Install Chart(s)`: Installs the selected chart to the current profile. - `Uninstall Chart(s)`: Uninstalls the selected chart from the current profile. - `Properties`: It opens the *Chart Properties* window. You can see the chart information in the *Chart Info* tab. In the *Metadata* tab, you can add metadata for the selected main chart. It overrides the metadata in the profile. In the *Kubernetes Services* tab, you can relate a Kubernetes service with the main chart; however, since the main chart usually doesn't create kubernetes service, we can leave it empty. -- `Refrest Sub Charts`: Refreshes the subcharts of the selected main chart. +- `Refresh Sub Charts`: Refreshes the subcharts of the selected main chart. - `Open With`: You can open the selected chart with *Visual Studio Code* or *File Explorer*. - `Remove`: Removes the selected main chart from the solution. @@ -137,6 +137,11 @@ While connected, changing the current profile is not possible. Existing applicat ![connected](./images/kubernetes/connected.png) +When connected, you can right-click on a Kubernetes service to see the following context menu options: + +- `Browse`: Opens the [browser](./monitoring-applications.md#browse) and navigates to the Kubernetes service URL. This option is only visible if the service has a related Helm chart with matching *Kubernetes Services* regex pattern. +- `Enable Interception` / `Disable Interception`: Enables or disables traffic interception for the selected service. See the [Intercept a Service](#intercept-a-service) section for more details. + When you are connecting to a Kubernetes cluster, it automatically installs the WireGuard VPN to the Kubernetes cluster for a safe connection. You can specify the *wireGuardPassword* in the *Kubernetes Profile* -> *Secrets* tab or at a higher level such as *Solution Secrets* or *Global Secrets*. If you don't provide a password, it generates a random password and stores it in the *Kubernetes Profile* -> *Secrets*. However, if you try to connect to a cluster that already installed WireGuard VPN, then you should give the same password; otherwise, it won't connect. To see the random password, you can click the *eye* icon in the *Kubernetes Profile* -> *Secrets* tab. ![wireGuardPassword](./images/kubernetes/wireGuardPassword.png) diff --git a/docs/en/studio/monitoring-applications.md b/docs/en/studio/monitoring-applications.md index 121a3aebac..083a0d384f 100644 --- a/docs/en/studio/monitoring-applications.md +++ b/docs/en/studio/monitoring-applications.md @@ -66,82 +66,223 @@ In this tab, you can view comprehensive overall information. You have the option ![overall](./images/monitoring-applications/overall.png) -In the data grid, details for each application are displayed. It's possible to sort rows by columns. When selecting a row, you can right-click to access the context menu, offering various actions. This menu allows for opening related tabs that are filtered by the selected application. +In the data grid, details for each application are displayed. It's possible to sort rows by columns. - `Name`: The name of the application. -- `State`: The state of the application. It can take on several values such as *Scheduled*, *Starting*, *Started*, *Stopping* and *Stopped*. In the event of an application crash during its starting, the state is mark as *Scheduled*, we can cancel the starting process at that stage. -- `Health` : The health state of the application. Clicking on the icon shows the latest health check response. Displays `N/A` if the application is not running or health check is not configured for the application. -- `Instances`: Indicates the count of running instances for the application. This value is particularly helpful when scaling the application within a Kubernetes, providing visibility into the number of currently active instances. +- `State`: The state of the application. It can take on several values such as *Scheduled*, *Starting*, *Started*, *Stopping* and *Stopped*. The *Scheduled* state indicates the application is waiting for an automatic restart (e.g., after a crash or when watch mode detects changes). You can cancel the scheduled restart at this stage. +- `Health`: The health state of the application. The icon indicates the current health status: *Healthy* (green), *Unhealthy* (red), *Degraded* (yellow), or *Unknown* (gray). Clicking on the icon shows the latest health check response in JSON format. Displays `N/A` if the application is not running or health check is not configured for the application. +- `Instances`: Indicates the count of running instances for the application. This value is particularly helpful when scaling the application within a Kubernetes cluster, providing visibility into the number of currently active pods. - `Uptime`: The time elapsed since the application started. - `Requests`: The number of HTTP requests received by the application. -- `Events (R/S)`: The number of [Distributed Event](../framework/infrastructure/event-bus/distributed) received or sent by the application. +- `Events (R/S)`: The number of [Distributed Events](../framework/infrastructure/event-bus/distributed) received or sent by the application. - `Exceptions`: The number of exceptions thrown by the application. - `Actions`: The actions that can be performed on the application. You can start and stop the application. -> For the events system, you can exclusively view the [Distributed Events](../framework/infrastructure/event-bus/distributed). Generally, the [Local Events](../framework/infrastructure/event-bus/distributed) is not included. +### Context Menu Actions + +When selecting a row, you can right-click to access the context menu with the following actions: + +| Action | Description | +|--------|-------------| +| **Start / Stop** | Start or stop the selected application. | +| **Restart** | Restart the application (available when the application is running). | +| **Build** | Build options including *Build*, *Graph Build*, *Restore*, and *Clean* (available for C# applications when stopped). | +| **Browse** | Open the application in the [Browse](#browse) tab (available when a Launch URL is configured). | +| **Health Status** | Submenu with *Browse Health UI* and *Show Latest Health Check Response* options. | +| **Requests** | Open the [HTTP Requests](#http-requests) tab filtered by this application. | +| **Exceptions** | Open the [Exceptions](#exceptions) tab filtered by this application. | +| **Logs** | Open the [Logs](#logs) tab with this application selected. | +| **Copy Url** | Copy the application's URL to the clipboard. | +| **Properties** | Open the application properties dialog. | + +> For the events system, you can exclusively view the [Distributed Events](../framework/infrastructure/event-bus/distributed). Generally, the [Local Events](../framework/infrastructure/event-bus/local) are not included. ## Browse -ABP Studio includes a browser tool that allows access to websites and running applications. You can open new tabs to browse different websites or view active applications. It's a convenient utility to access websites and applications without leaving ABP Studio. Clicking the *Browse* tab displays the running applications and an *Open new tab* button. +ABP Studio includes a built-in browser that allows access to websites and running applications. You can open new tabs to browse different websites or view active applications. It's a convenient utility to access websites and applications without leaving ABP Studio. Clicking the *Browse* tab displays the running applications and an *Open new tab* button. ![browse](./images/monitoring-applications/browse.png) -You can open the *Browse* tabs as many times as you want. It's possible to open the same application in several tabs simultaneously. To open an application, navigate through *Solution Runner* -> *C# or CLI Application* -> *Browse*. This option is only visible when there is a [Launch URL](./running-applications.md#properties). Additionally, you can access any URL by entering it into the address bar. +You can open the *Browse* tabs as many times as you want. It's possible to open the same application in several tabs simultaneously. To open an application, you can: + +- Double-click on an application in the *Solution Runner* tree. +- Right-click on an application and select *Browse* from the context menu. +- Click on a running application in the application list shown in the *Browse* tab. + +These options are only available when the application has a [Launch URL](./running-applications.md#properties) configured. Additionally, you can access any URL by entering it into the address bar. ![browse-2](./images/monitoring-applications/browse-2.png) -When you click the *Dev Tools* button it opens the [Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools) for the selected tab. +### Browser Toolbar + +The browser toolbar provides the following controls: + +| Control | Description | +|---------|-------------| +| **Back / Forward** | Navigate through your browsing history within the tab. | +| **Refresh** | Reload the current page. | +| **Address Bar** | Enter any URL to navigate directly. The address bar shows the current URL and allows you to navigate to any website. | +| **Dev Tools** | Opens the [Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools) for the selected tab, allowing you to inspect elements, debug JavaScript, and analyze network requests. | +| **Clear Cookies** | Clears all cookies for the currently selected tab, useful for testing authentication flows or resetting session state. | +| **Open in External Browser** | Opens the current URL in your system's default web browser. | ![dev-tools](./images/monitoring-applications/dev-tools.png) +### Default Credentials Notification + +When browsing certain applications (such as AppHost dashboards), ABP Studio displays a notification bar with default credentials. This helps you quickly log in during development. You can dismiss this notification permanently by clicking the *Don't show again* option. + ## HTTP Requests Within this tab, you can view all *HTTP Requests* received by your C# applications. You have the option to filter requests based on URLs by using the search textbox or by selecting a particular application from the combobox. The *Clear Requests* button removes all received requests. Moreover, you have the ability to sort requests by columns. ![http-requests](./images/monitoring-applications/http-requests.png) -Clicking on a row enables you to view the details of each HTTP request; `URL`, `Method`, `Status Code`, `Timestamp`, `Headers (Request, Response)`, `Request (Payload)` and `Response`. +### Request List Columns + +The request list displays the following information: + +| Column | Description | +|--------|-------------| +| **Timestamp** | When the request was received (displayed as HH:mm:ss). | +| **Application** | The name of the application that received the request. | +| **Path** | The request URL path and query string. | +| **Method** | The HTTP method (GET, POST, PUT, DELETE, etc.). | +| **Status Code** | The HTTP response status code. | +| **Duration** | The time taken to process the request in milliseconds. | +| **Request Body Size** | The size of the request payload. | +| **Response Body Size** | The size of the response payload. | + +### Request Details + +Clicking on a row enables you to view the details of each HTTP request: + +- **URL**: The full request URL. +- **Method**: The HTTP method used. +- **Status Code**: The response status code. +- **Duration**: Request processing time in milliseconds. +- **Timestamp**: When the request was received. +- **Headers (Request)**: All request headers sent by the client. +- **Headers (Response)**: All response headers returned by the server. +- **Request (Payload)**: The request body with size information. +- **Response**: The response body with size information. ![http-requests-details](./images/monitoring-applications/http-requests-details.png) -You can format the JSON content by clicking the *Format* button. +### Formatting JSON Content + +Both request and response payloads have a *Format* button that formats JSON content for better readability. This is available when the content type is `application/json`. ![http-requests-details-json](./images/monitoring-applications/http-requests-details-json.png) -Furthermore, by clicking the gear icon in the *HTTP Requests* tab, you can access the *Solution Runner HTTP Requests Options* window. Within the *Ignored URLs* tab, you have the ability to exclude particular URLs by applying a regex pattern. Excluded URLs won't be visible in the *HTTP Requests* tab. By default, the metrics URL is already ignored. You can add or remove items as needed. +### Quick URL Filter + +When viewing request details, you can right-click and select *Filter Selected URL* to quickly apply the current URL as a filter. This is useful when you want to see all requests to a specific endpoint. + +### Configuring Ignored URLs + +By clicking the gear icon in the *HTTP Requests* tab, you can access the *Solution Runner HTTP Requests Options* window. Within the *Ignored URLs* tab, you have the ability to exclude particular URLs by applying a regex pattern. Excluded URLs won't be visible in the *HTTP Requests* tab. By default, the metrics URL is already ignored. You can add or remove items as needed. ![http-requests-options](./images/monitoring-applications/http-requests-options.png) -> After adding a new URL, it will only affect subsequent requests. +> After adding a new URL pattern, it will only affect subsequent requests. Existing requests will remain visible. ## Events -In this tab, you can view all [Distributed Events](../framework/infrastructure/event-bus/distributed) sent or received by your C# applications. You can filter them by [Event Name](../framework/infrastructure/event-bus/distributed#event-name) using the search textbox or by selecting a specific application. Additionally, you can choose the *Direction* (Received/Send) and *Source* (Direct/Inbox/Outbox) of events. The *Clear Events* button removes all events. +In this tab, you can view all [Distributed Events](../framework/infrastructure/event-bus/distributed) sent or received by your C# applications. You can filter them by [Event Name](../framework/infrastructure/event-bus/distributed#event-name) using the search textbox or by selecting a specific application. Additionally, you can choose the *Direction* (Received/Sent) and *Source* (Direct/Inbox/Outbox) of events. The *Clear Events* button removes all events. ![events](./images/monitoring-applications/events.png) -> In the *Direction* section, there are two options: *Received*, indicating events received by the application, and *Sent*, indicating events sent by the application. Within the *Source* section, three options are available, and their significance comes into play when utilizing the [Inbox/Outbox pattern](../framework/infrastructure/event-bus/distributed#outbox-inbox-for-transactional-events). *Inbox* refers to events received by the application, *Outbox* refers to events sent by the application and *Direct* signifies events sent or received by the application without involving Inbox/Outbox pattern. +### Direction and Source Filters + +| Filter | Options | Description | +|--------|---------|-------------| +| **Direction** | *Received*, *Sent* | Filter by whether events were received or sent by the application. | +| **Source** | *Direct*, *Inbox*, *Outbox* | Filter by event source. Relevant when using the [Inbox/Outbox pattern](../framework/infrastructure/event-bus/distributed#outbox-inbox-for-transactional-events). | -Clicking on a row enables you to view the details of each event; `Application`, `Event Name`, `Direction`, `Source`, `Timestamp` and `Event Data`. +- **Direct**: Events sent or received without using the Inbox/Outbox pattern. +- **Inbox**: Events received through the transactional inbox. +- **Outbox**: Events sent through the transactional outbox. + +### Event Details + +Clicking on a row enables you to view the details of each event: + +- **Application**: The application that sent or received the event. +- **Event Name**: The full event type name. +- **Direction**: Whether the event was received or sent. +- **Source**: The event source (Direct, Inbox, or Outbox). +- **Timestamp**: When the event was processed. +- **Event Data**: The event payload in JSON format. ![event-details](./images/monitoring-applications/event-details.png) +### Formatting Event Data + +The *Event Data* section includes a *Format* button that formats the JSON content for better readability. This makes it easier to inspect complex event payloads. + +> ABP Studio automatically decodes Base64-encoded event data. If your event bus uses Base64 encoding for message transport, the data will be displayed in its decoded, readable form. + ## Exceptions -This tab displays all exceptions by your C# applications. You can apply filters using the search textbox based on *Message*, *Source*, *ExceptionType*, and *StackTrace* or by choosing a specific application. Additionally, you have the option to select the [Log Level](../framework/fundamentals/exception-handling.md#log-level) for adding a filter. To clear all exceptions, use the *Clear Exceptions* button. +This tab displays all exceptions thrown by your C# applications. You can apply filters using the search textbox based on *Message*, *Source*, *ExceptionType*, and *StackTrace* or by choosing a specific application. Additionally, you have the option to select the [Log Level](../framework/fundamentals/exception-handling.md#log-level) for filtering. To clear all exceptions, use the *Clear Exceptions* button. ![exceptions](./images/monitoring-applications/exceptions.png) -Click on a row to inspect the details of each exception; `Application`, `Exception Type`, `Source`, `Timestamp`, `Level`, `Message` and `StackTrace`. +### Exception List + +The exception list shows a summary of each exception: + +- **Exception Type**: The short exception type name (e.g., `NullReferenceException` instead of `System.NullReferenceException`). +- **Message**: A truncated version of the exception message. +- **Timestamp**: When the exception occurred. +- **Level**: The log level (Error, Warning, etc.). + +### Exception Details + +Click on a row to inspect the full details of each exception: + +- **Application**: The application where the exception occurred. +- **Exception Type**: The full exception type name including namespace. +- **Source**: The source file or component where the exception originated. +- **Timestamp**: When the exception was thrown. +- **Level**: The log level associated with this exception. +- **Message**: The complete exception message. +- **StackTrace**: The full stack trace showing the call hierarchy. ![exception-details](./images/monitoring-applications/exception-details.png) +### Inner Exceptions + +If an exception contains inner exceptions, they are displayed hierarchically in the details panel. This allows you to trace the root cause of wrapped exceptions, which is common in scenarios like database errors wrapped in application-level exceptions. + ## Logs -The *Logs* tab allows you to view all logs for both CLI and C# applications. To access logs, simply select an application. You can also apply filters using the search textbox by log text or by selecting a specific *Log Level*. When you select a *Log Level* it shows selected log level and higher log levels. For example, if you select *Warning* it shows *Warning*, *Error* and *Critical* logs. To clear selected application logs, use the *Clear Logs* button. If *Auto Scroll* is checked, the display automatically scrolls when new logs are received. +The *Logs* tab allows you to view all logs for both CLI and C# applications. To access logs, simply select an application from the dropdown. You can also apply filters using the search textbox by log text or by selecting a specific *Log Level*. ![logs](./images/monitoring-applications/logs.png) +### Log Level Filtering + +When you select a *Log Level*, it shows the selected level and all higher severity levels: + +| Selected Level | Shows | +|---------------|-------| +| **Trace** | Trace, Debug, Information, Warning, Error, Critical | +| **Debug** | Debug, Information, Warning, Error, Critical | +| **Information** | Information, Warning, Error, Critical | +| **Warning** | Warning, Error, Critical | +| **Error** | Error, Critical | +| **Critical** | Critical only | +| **None** | All logs (no filtering) | + +### Log Display Features + +- **Color Coding**: Log entries are color-coded by level for quick visual identification. Errors and critical logs stand out with distinct colors. +- **Auto Scroll**: When enabled, the display automatically scrolls to show new logs as they arrive. This is useful for real-time monitoring. +- **Clear Logs**: Clears the logs for the currently selected application only. Other applications' logs remain intact. +- **Text Filter**: Search within log messages using the search textbox. The filter is applied in real-time with a slight delay for performance. + ## Tools The *Tools* tab provides quick access to web-based management interfaces for infrastructure services like Grafana, RabbitMQ, pgAdmin, and Redis Commander. Each tool opens in a dedicated browser tab within ABP Studio, eliminating the need to switch between external browser windows. diff --git a/docs/en/studio/overview.md b/docs/en/studio/overview.md index 3f5835c998..228f10afce 100644 --- a/docs/en/studio/overview.md +++ b/docs/en/studio/overview.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Explore ABP Studio's key features like Solution Explorer and Kubernetes Integration to optimize your ABP application development and management." + "Description": "Explore ABP Studio's key features like Solution Explorer, Kubernetes Integration, and AI Assistant to optimize your ABP application development and management." } ``` @@ -19,7 +19,7 @@ ## Introduction -ABP Studio, a comprehensive desktop application, offers a wide range of features and functionalities tailored to streamline the development and management of ABP-based applications. This article provides an overview of the key components of ABP Studio, including the Solution Explorer, Solution Runner, Kubernetes Integration, Application Monitoring Area, Background Tasks, Notifications and Logs. Understanding these components is essential for efficiently utilizing ABP Studio to its full potential. +ABP Studio, a comprehensive desktop application, offers a wide range of features and functionalities tailored to streamline the development and management of ABP-based applications. This article provides an overview of the key components of ABP Studio, including the Solution Explorer, Solution Runner, Kubernetes Integration, AI Assistant, Application Monitoring Area, Background Tasks, Notifications and Logs. Understanding these components is essential for efficiently utilizing ABP Studio to its full potential. ![overview-intro](./images/overview/overview-intro.png) @@ -69,7 +69,7 @@ The Welcome Screen is the initial user interface users encounter upon starting A ## Sidebar -Located on the left side of the interface, the Left Area is a panel that provides quick access to various functionalities like [Solution Explorer](./solution-explorer.md), [Solution Runner](./running-applications.md), and Kubernetes Integration. Now let's examine each item. +Located on the left side of the interface, the Left Area is a panel that provides quick access to various functionalities like [Solution Explorer](./solution-explorer.md), [Solution Runner](./running-applications.md), Kubernetes Integration, and AI Assistant. Now let's examine each item. ### Solution Explorer @@ -99,6 +99,21 @@ This pane is dedicated to managing Kubernetes services. It simplifies the proces ![kubernetes-integration-kubernetes-pane](./images/overview/kubernetes-integration-kubernetes.png) +### AI Assistant + +The AI Assistant is an integrated chat interface within ABP Studio that provides intelligent assistance for ABP-related questions. You can access it from the left sidebar by clicking the AI icon. + +![ai-assistant](./images/overview/ai-assistant.png) + +Key features of the AI Assistant include: + +- **Chat Interface**: Ask anything about ABP Framework, and get instant, context-aware responses to help with your development workflow. +- **File Attachments**: Attach files to your conversations to get more specific assistance with your code or configuration. +- **Recent Chats**: Access your conversation history to continue previous discussions or reference past solutions. +- **Credits System**: The AI Assistant uses a credits-based system, with usage tracked and displayed in the interface. + +> **Note**: Review the Privacy Notice available in the AI Assistant panel to understand how your data is handled. + ## Application Monitoring Area The [Application Monitoring Area](./monitoring-applications.md) is a functional component within ABP Studio that displays real-time operational status and metrics for applications. It indicates the active state of applications, the count of running instances, and so on. The user interface is organized into several tabs: Overall, Browse, HTTP Requests, Events, Exceptions, and Logs. Now let's examine the **Overall** tab. diff --git a/docs/en/studio/release-notes.md b/docs/en/studio/release-notes.md index d2eba88e0f..13e206127e 100644 --- a/docs/en/studio/release-notes.md +++ b/docs/en/studio/release-notes.md @@ -9,7 +9,14 @@ This document contains **brief release notes** for each ABP Studio release. Release notes only include **major features** and **visible enhancements**. Therefore, they don't include all the development done in the related version. -## 2.1.3 (2025-12-15) Latest +## 2.1.4 (2025-12-30) Latest + +* Fixed books sample for blazor-webapp tiered solution. +* Fixed K8s cluster deployment issues for microservices. +* Fixed docker build problem on microservice template. +* Showed logs of the executed tasks. + +## 2.1.3 (2025-12-15) * Updated `createCommand` and CLI help for multi-tenancy. * Fixed `BookController` templating problem. diff --git a/docs/en/studio/running-applications.md b/docs/en/studio/running-applications.md index c922c1b501..adec07ad66 100644 --- a/docs/en/studio/running-applications.md +++ b/docs/en/studio/running-applications.md @@ -77,7 +77,9 @@ We can use common [dotnet](https://learn.microsoft.com/en-us/dotnet/core/tools) ### Add -We can add 3 different item type to *Profile* for defining the tree structure. Those options are `C# Application`, `CLI Application` and `Folder`. +We can add 4 different item types to *Profile* for defining the tree structure. Those options are `C# Application`, `CLI Application`, `Docker Container`, and `Folder`. + +> Note: The `Docker Container` option is only available when right-clicking the profile root. When right-clicking a folder, only `C# Application`, `CLI Application`, and `Folder` options are available. ![profile-root-context-menu-add](images/solution-runner/profile-root-context-menu-add.png) @@ -181,13 +183,15 @@ You can see the context menu by right-clicking *Folder*. It will start/stop all ## C# Application -The .NET icon indicates that the application is a C# project. After we [add](#c-application) the C# applications to the root of the tree or folder, we can go to any C# application and right-click to view the context menu; `Start`, `Build`, `Browse`, `Requests`, `Exceptions`, `Logs`, `Copy URL`, `Properties`, `Remove`. +The .NET icon indicates that the application is a C# project. After we [add](#c-application) the C# applications to the root of the tree or folder, we can go to any C# application and right-click to view the context menu; `Start`, `Stop`, `Restart`, `Build`, `Browse`, `Health Status`, `Requests`, `Exceptions`, `Logs`, `Copy URL`, `Properties`, `Remove`, and `Open with`. ![csharp-application-context-menu](images/solution-runner/csharp-application-context-menu.png) -### Start +### Start / Stop / Restart -Starts the selected application. Once it is started, *Stop* and *Restart* options will be available. +- **Start**: Starts the selected application. +- **Stop**: Stops the running application. +- **Restart**: Restarts the running application. This option is only available when the application is started. > When you start the C# application, you should see a *chain* icon next to the application name, that means the started application connected to ABP Studio. C# applications can connect to ABP Studio even when running from outside the ABP Studio environment, for example debugging with Visual Studio. If the application is run from outside the ABP Studio environment, it will display *(external)* information next to the chain icon. @@ -199,14 +203,17 @@ It's the [similar](#build) options like root of the tree options. The only diffe ### Monitoring -When the C# application is connected to ABP Studio, it starts sending telemetry information to see in one place. We can easily click these options to see the detail; `Browse`, `Requests`, `Exceptions` and `Logs`. +When the C# application is connected to ABP Studio, it starts sending telemetry information to see in one place. We can easily click these options to see the detail; `Browse`, `Health Status`, `Requests`, `Exceptions`, `Events` and `Logs`. ![csharp-application-context-menu-monitor](images/solution-runner/csharp-application-context-menu-monitor.png) - `Browse`: ABP Studio includes a browser tool for accessing websites and running applications. You can click this option to view the application in the ABP Studio browser. However, this option is only accessible if the application is started. -- Health Status : If Health Check endpoints are defined, it allows you to browse Health UI and see the latest health check response. +- `Health Status`: A submenu that provides health monitoring options when Health Check endpoints are defined: + - **Browse Health UI**: Opens the Health UI page of the application in the built-in browser. + - **Show Latest Health Check Response**: Displays the latest health check response in JSON format. - `Requests`: It opens the *HTTP Requests* tab with adding the selected application filter. You can view all *HTTP Requests* received by your applications. - `Exceptions`: We can display all exceptions on this tab. It opens the *Exceptions* tab with selected application. +- `Events`: Opens the *Events* tab filtered by this application. You can view all [Distributed Events](../framework/infrastructure/event-bus/distributed) sent or received by this application. - `Logs`: Clicking this option opens the *Logs* tab with adding the selected application filter. ### Properties @@ -215,16 +222,31 @@ We can open the *Application Properties* window to change *Launch url*, *Health ![solutioın-runner-properties](images/solution-runner/solutioın-runner-properties.png) -- **Health check endpoint**: Endpoint for controlling the health status of the application periodically. If the application doesn't have a endpoint for health check, you can enter `/` to use the home page of the application as health check endpoint. +- **Launch URL**: The URL used when browsing the application. This is the address where the application is accessible. +- **Kubernetes service**: A regex pattern to match Kubernetes service names. When connected to a Kubernetes cluster, ABP Studio uses this pattern to find the corresponding Kubernetes service and uses its URL instead of the Launch URL. This applies to *Browse*, *Copy URL*, and *Health UI* features. For example, if your Helm chart creates a service named `bookstore-identity-service`, you can use `.*-identity-service` or `bookstore-identity.*` as the pattern. For [microservice](../get-started/microservice.md) templates, this is pre-configured. +- **Health check endpoint**: Endpoint for controlling the health status of the application periodically. If the application doesn't have an endpoint for health check, you can enter `/` to use the home page of the application as health check endpoint. - **Health UI endpoint**: Endpoint of the Health UI page of the application. -- **Skip build before starting**: When enabled, application is started without build and it makes starting faster. This is useful when you are working on a single application out of multiple, so you don't need to build others everytime they start. -- **Watch changes while running**: When enabled, you should see an *eye* icon next to the application name. +- **Skip build before starting**: When enabled, the application is started without building, making startup faster. This is useful when you are working on a single application out of multiple, so you don't need to build others every time they start. +- **Watch changes while running**: When enabled, changes in your code are watched and dotnet hot-reloads the application or restarts it if needed. You should see an *eye* icon next to the application name when this is enabled. +- **Open browser on start**: When enabled, the application is automatically opened in the Browse tab when it starts. +- **Auto refresh browser on restart**: When enabled, browser tabs showing this application are automatically refreshed when the application restarts. +- **Runnable**: Controls whether the application can be started from Solution Runner. When disabled, the application will not be included in batch start operations. ![csharp-application-context-menu-run-connection](images/solution-runner/csharp-application-context-menu-run-connection.png) +### Open with + +The *Open with* submenu provides options to open the application project in external tools: + +- **Visual Studio**: Opens the project in Visual Studio (available if installed). +- **Visual Studio Code**: Opens the project folder in Visual Studio Code (available if installed). +- **JetBrains Rider**: Opens the project in JetBrains Rider (available if installed). +- **Terminal**: Opens a terminal window in the project directory. +- **Explorer / Finder**: Opens the project folder in the system file explorer. + ### Miscellaneous -- We can copy the selected application *Browse* URL with *Copy URL*. It copies the *Browse* URL instead *Launch URL* since we could connected to *Kubernetes* service. +- We can copy the selected application *Browse* URL with *Copy URL*. It copies the *Browse* URL instead of *Launch URL* since we could be connected to a *Kubernetes* service. - You can change the target framework by right-click the selected application and change the *Target Framework* option. This option visible if the project has multiple target framework such as MAUI applications. - To remove an application from the tree, open the context menu by right-clicking the application and selecting *Remove*. diff --git a/docs/en/studio/solution-explorer.md b/docs/en/studio/solution-explorer.md index 21dde505e5..edbac3b471 100644 --- a/docs/en/studio/solution-explorer.md +++ b/docs/en/studio/solution-explorer.md @@ -33,6 +33,9 @@ It is the main solution that you can open with ABP Studio, an ABP solution can c - `New Folder`: Creates a new folder within the ABP Solution, allowing you to organize your modules. - `New Module`: Allows you to create a new module. - `Existing Module`: You can add existing module to your solution. +- `Analyze`: Analyzes all modules and packages in the solution to extract information like aggregate roots, application services, permissions, etc. + - `Analyze`: Analyzes all modules and packages using existing build outputs. + - `Build & Analyze`: Builds the solution first, then analyzes all modules and packages. - `Rename`: Renames the solution. - `Manage Secrets`: You can edit your solution secrets. - `Manage Metadata`: You can edit your solution metadata. @@ -52,6 +55,9 @@ It is the main solution that you can open with ABP Studio, an ABP solution can c - `Clean`: Cleans the output of the previous build for modules. - `Restore`: Restores the dependencies for modules. - `Open With` + - `Visual Studio`: Opens the solution in Visual Studio. This option is only available if you have Visual Studio installed. + - `Visual Studio Code`: Opens the solution in Visual Studio Code. This option is only available if you have Visual Studio Code installed. + - `JetBrains Rider`: Opens the solution in JetBrains Rider. This option is only available if you have JetBrains Rider installed. - `Terminal`: Opens the terminal in the solution directory. - `Explorer`: Opens the file explorer in the solution directory. - `Solution Configuration`: You can see the project creation options in this menu. It opens the *Solution Configuration* window. @@ -73,6 +79,9 @@ You can click the *OK* button to add the folder to the solution. When you right- - `New Folder`: Creates a new nested folder within the selected folder, allowing you to organize your modules. - `New Module`: Allows you to create a new module to selected folder. - `Existing Module`: You can add existing module to your selected folder. +- `Analyze`: Analyzes all modules and packages in the selected folder. + - `Analyze`: Analyzes all modules and packages using existing build outputs. + - `Build & Analyze`: Builds first, then analyzes all modules and packages in the folder. - `Rename`: Renames the selected folder. - `Delete`: Deletes the selected folder and all child items from the solution. - `ABP CLI` @@ -102,6 +111,9 @@ A [module](./concepts.md#module) is a sub-solution that can contains zero, one o - `New Package`: Creates a new package within the selected module. - `Existing Package`: You can add existing package to your selected module. - `Folder`: Creates a new folder within the selected module, allowing you to organize your packages. +- `Analyze`: Analyzes all packages in the selected module. + - `Analyze`: Analyzes all packages using existing build outputs. + - `Build & Analyze`: Builds first, then analyzes all packages in the module. - `Import Module`: This option allows you to import an existing module from *Solution*, *Local*, or *NuGet* into your selected module. - `Rename`: Renames the selected module. - `Remove`: Removes the selected module and all child items from the solution. @@ -126,6 +138,7 @@ A [module](./concepts.md#module) is a sub-solution that can contains zero, one o - `JetBrains Rider`: Opens the module in JetBrains Rider. This option is only available if you have JetBrains Rider installed. - `Terminal`: Opens the terminal in the module directory. - `Explorer`: Opens the file explorer in the module directory. +- `Open Readme`: Opens the README file in the module if available. This option is only visible if the module has a README file. - `Upgrade to Pro`: This will be visible only when you purchased a license but still using the modules came with open-source (free) license. For more details, check out [Migrating from Open Source Templates](../guides/migrating-from-open-source.md) document. This is not shown in the screenshot above. ### Adding a New Empty Module @@ -238,10 +251,18 @@ A [package](./concepts.md#package) is a project that can be added to a module, a ![package-context-menu](./images/solution-explorer/package-context-menu.png) -- `Add Package Reference`: This option allows you to add a package reference to the selected package. +- `Add`: This menu is only visible for Host projects (except Blazor WebAssembly). + - `NPM Package`: Adds an NPM package reference to the selected package. +- `Add Package Reference`: This option allows you to add a package reference to the selected package. This option is only visible for non-Host projects. +- `Analyze`: Analyzes the selected package to extract information like aggregate roots, application services, permissions, etc. This menu is only visible for analyzable package types. + - `Analyze`: Analyzes the package using existing build outputs. + - `Build & Analyze`: Builds first, then analyzes the package. - `Reload`: Reloads the selected package. - `Remove`: Removes the selected package from the module. - `ABP CLI` + - `Generate Proxy`: Generates client-side proxy code for the selected package. + - `Javascript`: Generates JavaScript proxy code. + - `C#`: Generates C# proxy code. - `Install Libs`: Install NPM Packages for UI projects in your selected package. - `Upgrade ABP Packages`: Update all the ABP related NuGet and NPM packages in your selected package. - `Switch to`: It switches your selected package to the specified version of the ABP. @@ -264,6 +285,7 @@ A [package](./concepts.md#package) is a project that can be added to a module, a - `JetBrains Rider`: Opens the package in JetBrains Rider. This option is only available if you have JetBrains Rider installed. - `Terminal`: Opens the terminal in the package directory. - `Explorer`: Opens the file explorer in the package directory. +- `Open Readme`: Opens the README file in the package if available. This option is only visible if the package has a README file. ### Adding a New Package diff --git a/docs/en/studio/working-with-suite.md b/docs/en/studio/working-with-suite.md index b436b7b0f8..2dc3a4bcc0 100644 --- a/docs/en/studio/working-with-suite.md +++ b/docs/en/studio/working-with-suite.md @@ -39,7 +39,10 @@ If There are more than one module which is openable via Suite, Studio will ask y ### Opening from context menu -Alternatively, you can right click to a module in [Solution Explorer](solution-explorer.md) and click `ABP Suite` to open it with Suite. Suite will automatically open `Crud Page Generation` screen with the selected module. +You can right-click on the solution root or a module in [Solution Explorer](solution-explorer.md) and click `ABP Suite` to open it with Suite. + +- **Solution root**: If you right-click on the solution root and select `ABP Suite`, Studio will ask you to pick a module if there are multiple modules. Suite will then open with the selected module. +- **Module**: If you right-click on a specific module and select `ABP Suite`, Suite will automatically open `Crud Page Generation` screen with that module. ![suite-context-menu](./images/suite/suite-context-menu.png) @@ -51,10 +54,14 @@ Standard application solutions (`App` & `App-nolayers`) generated by Studio are You can generate code on the services of the microservice solution. UI code generation is not supported at the moment. It is on the roadmap. -## Managing Installed Version +## Managing Installed Version You can update or downgrade the version of `ABP Suite` by `Change Version` button in `ABP Suite` menu. +### Automatic Version Matching + +When you open ABP Suite for a solution, Studio checks if the installed Suite version matches the solution's ABP version. If they differ, Studio will prompt you to update or downgrade Suite to match your solution's version. This ensures compatibility between Suite and your project. + ![suite-change-version-button-main-page](./images/suite/suite-change-version-button-main-page.png) ![suite-change-version-window](./images/suite/suite-change-version-window.png) diff --git a/docs/en/testing/integration-tests.md b/docs/en/testing/integration-tests.md index c87d270747..d28ed5ca5b 100644 --- a/docs/en/testing/integration-tests.md +++ b/docs/en/testing/integration-tests.md @@ -34,13 +34,13 @@ The startup template is configured to use **in-memory SQLite** database for the Using in-memory SQLite database has two main advantages; * It is faster compared to an external DBMS. -* It create a **new fresh database** for each test case, so tests doesn't affect each other. +* It creates a **new fresh database** for each test case, so tests don't affect each other. > **Tip**: Do not use EF Core's In-Memory database for advanced integration tests. It is not a real DBMS and has many differences in details. For example, it doesn't support transaction and rollback scenarios, so you can't truly test the failing scenarios. On the other hand, In-Memory SQLite is a real DBMS and supports the fundamental SQL database features. ## The Seed Data -Writing tests against an empty database is not practical. In most cases, you need to some initial data in the database. For example, if you write a test class that query, update and delete the products, it would be helpful to have a few products in the database before executing the test case. +Writing tests against an empty database is not practical. In most cases, you need some initial data in the database. For example, if you write a test class that queries, updates and deletes the products, it would be helpful to have a few products in the database before executing the test case. ABP's [Data Seeding](../framework/infrastructure/data-seeding.md) system is a powerful way to seed the initial data. The application startup template has a *YourProject*TestDataSeedContributor class in the `.TestBase` project. You can fill it to have an initial data that you can use for each test method. @@ -401,7 +401,7 @@ public class EfCoreIssueAppService_Tests : IssueAppService_Tests By deriving from the related abstract classes, now we can see the all tests in the test explorers and run them. +> By deriving from the related abstract classes, now we can see all the tests in the test explorers and run them. ![unitest-efcore-mongodb](../images/unitest-efcore-mongodb.png) diff --git a/docs/en/testing/overall.md b/docs/en/testing/overall.md index 154c169d5b..15e9abf725 100644 --- a/docs/en/testing/overall.md +++ b/docs/en/testing/overall.md @@ -69,7 +69,7 @@ The startup solution has the following libraries already installed; * [NSubstitute](https://nsubstitute.github.io/) as the mocking library. * [Shouldly](https://github.com/shouldly/shouldly) as the assertion library. -While you are free to replace them with your favorite tools, this document and examples will be base on these tooling. +While you are free to replace them with your favorite tools, this document and examples will be based on these tooling. ## The Test Explorer diff --git a/docs/en/testing/unit-tests.md b/docs/en/testing/unit-tests.md index cfc63a4848..2e06a44efb 100644 --- a/docs/en/testing/unit-tests.md +++ b/docs/en/testing/unit-tests.md @@ -73,7 +73,7 @@ namespace MyProject.Issues Notice that the `IsClosed` and `CloseDate` properties have private setters to force some business rules by using the `Open()` and `Close()` methods: -* Whenever you close an issue, the `CloseDate` should be set to the [current time](../framework/infrastructure/virtual-file-system.md). +* Whenever you close an issue, the `CloseDate` should be set to the current time. * An issue can not be re-opened if it is locked. And if it is re-opened, the `CloseDate` should be set to `null`. Since the `Issue` entity is a part of the Domain Layer, we should test it in the `Domain.Tests` project. Create an `Issue_Tests` class inside the `Domain.Tests` project: @@ -160,7 +160,7 @@ public void Should_Not_Allow_To_ReOpen_A_Locked_Issue() `Assert.Throws` checks if the executed code throws a matching exception. -> See the [xUnit](https://xunit.net/#documentation) & [Shoudly](https://docs.shouldly.org/) documentation to learn more about these libraries. +> See the [xUnit](https://xunit.net/#documentation) & [Shouldly](https://docs.shouldly.org/) documentation to learn more about these libraries. ## Classes With Dependencies diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs index fc7d372b37..dc51022956 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs @@ -31,6 +31,8 @@ public class PageLayout : IScopedDependency, INotifyPropertyChanged } } + public bool ShowToolbar { get; set; } = true; + public virtual ObservableCollection BreadcrumbItems { get; } = new(); public virtual ObservableCollection ToolbarItems { get; } = new(); diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/PageToolbars/SimplePageToolbarContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/PageToolbars/SimplePageToolbarContributor.cs index 647538c121..6e47281a70 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/PageToolbars/SimplePageToolbarContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/PageToolbars/SimplePageToolbarContributor.cs @@ -16,6 +16,8 @@ public class SimplePageToolbarContributor : IPageToolbarContributor public string? RequiredPolicyName { get; } + private bool? _shouldAddComponent; + public SimplePageToolbarContributor( Type componentType, Dictionary? arguments = null, @@ -38,15 +40,19 @@ public class SimplePageToolbarContributor : IPageToolbarContributor protected virtual async Task ShouldAddComponentAsync(PageToolbarContributionContext context) { - if (RequiredPolicyName != null) + if (_shouldAddComponent.HasValue) + { + return _shouldAddComponent.Value; + } + + if (RequiredPolicyName == null) { - var authorizationService = context.ServiceProvider.GetRequiredService(); - if (!await authorizationService.IsGrantedAsync(RequiredPolicyName)) - { - return false; - } + _shouldAddComponent = true; + return _shouldAddComponent.Value; } - return true; + var authorizationService = context.ServiceProvider.GetRequiredService(); + _shouldAddComponent = await authorizationService.IsGrantedAsync(RequiredPolicyName); + return _shouldAddComponent.Value; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs index 1e5abce611..533a5b9187 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs @@ -11,6 +11,8 @@ public class ContentLayout public string? MenuItemName { get; set; } + public bool ShowToolbar { get; set; } = true; + public ContentLayout() { BreadCrumb = new BreadCrumb(); @@ -23,11 +25,6 @@ public class ContentLayout return true; } - if (BreadCrumb.ShowCurrent && !Title.IsNullOrEmpty()) - { - return true; - } - - return false; + return BreadCrumb.ShowCurrent || BreadCrumb.ShowHome; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpNoContentApiDescriptionProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpNoContentApiDescriptionProvider.cs new file mode 100644 index 0000000000..a0c9570fc6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpNoContentApiDescriptionProvider.cs @@ -0,0 +1,48 @@ +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Reflection; + +namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; + +public class AbpNoContentApiDescriptionProvider : IApiDescriptionProvider, ITransientDependency +{ + public virtual void OnProvidersExecuted(ApiDescriptionProviderContext context) + { + } + + /// + /// The order -999 ensures that this provider is executed right after the + /// Microsoft.AspNetCore.Mvc.ApiExplorer.DefaultApiDescriptionProvider. + /// + public int Order => -999; + + public virtual void OnProvidersExecuting(ApiDescriptionProviderContext context) + { + foreach (var result in context.Results.Where(x => x.IsRemoteService())) + { + var actionProducesResponseTypeAttributes = + ReflectionHelper.GetAttributesOfMemberOrDeclaringType( + result.ActionDescriptor.GetMethodInfo()); + if (actionProducesResponseTypeAttributes.Any(x => x.StatusCode == (int) HttpStatusCode.NoContent)) + { + continue; + } + + var returnType = result.ActionDescriptor.GetReturnType(); + if (returnType == typeof(Task) || returnType == typeof(void)) + { + result.SupportedResponseTypes.Add(new ApiResponseType + { + // If the return type is Task, then we should treat it as a void return type since we can't infer anything without additional metadata or requiring unreferenced code. + Type = typeof(void), + StatusCode = (int) HttpStatusCode.NoContent + }); + } + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs index 7d6955f08b..85cc987b61 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs @@ -61,11 +61,13 @@ public static class CookieAuthenticationOptionsExtensions return; } + var clientId = principalContext.Properties.GetString("client_id"); + var clientSecret = principalContext.Properties.GetString("client_secret"); var response = await openIdConnectOptions.Backchannel.IntrospectTokenAsync(new TokenIntrospectionRequest { Address = introspectionEndpoint, - ClientId = openIdConnectOptions.ClientId!, - ClientSecret = openIdConnectOptions.ClientSecret, + ClientId = clientId ?? openIdConnectOptions.ClientId!, + ClientSecret = clientSecret ?? openIdConnectOptions.ClientSecret, Token = accessToken }); diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj index ddf556047a..6d685652ca 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj @@ -22,8 +22,7 @@ - - + diff --git a/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimeZoneHelper.cs b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimeZoneHelper.cs index 6101585878..23446069a9 100644 --- a/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimeZoneHelper.cs +++ b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimeZoneHelper.cs @@ -7,14 +7,43 @@ namespace Volo.Abp.Timing; public static class TimeZoneHelper { + /// + /// Returns timezone list ordered by display name, enriched with UTC offset, filtering out invalid ids. + /// public static List GetTimezones(List timezones) { return timezones .OrderBy(x => x.Name) - .Select(x => new NameValue( $"{x.Name} ({GetTimezoneOffset(TZConvert.GetTimeZoneInfo(x.Name))})", x.Name)) + .Select(TryCreateNameValueWithOffset) + .OfType() .ToList(); } + /// + /// Builds a with the original timezone ID in Value and a display name that includes + /// the UTC offset in the Name property; returns null if the id is not found. + /// + public static NameValue? TryCreateNameValueWithOffset(NameValue timeZone) + { + try + { + var timeZoneInfo = TZConvert.GetTimeZoneInfo(timeZone.Name); + var name = $"{timeZone.Name} ({GetTimezoneOffset(timeZoneInfo)})"; + return new NameValue(name, timeZone.Name); + } + catch (Exception) + { + // Invalid or unknown timezone IDs are expected here (e.g. from user input or + // external sources). We intentionally swallow this exception and return null + // so callers (like GetTimezones) can filter out invalid entries. + } + + return null; + } + + /// + /// Formats the base UTC offset as "+hh:mm" or "-hh:mm" for display purposes. + /// public static string GetTimezoneOffset(TimeZoneInfo timeZoneInfo) { if (timeZoneInfo.BaseUtcOffset < TimeSpan.Zero) diff --git a/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimeZoneHelper_Tests.cs b/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimeZoneHelper_Tests.cs new file mode 100644 index 0000000000..54d8d37361 --- /dev/null +++ b/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimeZoneHelper_Tests.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Shouldly; +using TimeZoneConverter; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Timing; + +public class TimeZoneHelper_Tests : AbpIntegratedTest +{ + [Fact] + public void GetTimezones_Should_Filter_Invalid_Timezones() + { + var validTimeZoneId = "UTC"; + var invalidTimeZoneId = "Invalid/Zone"; + + var timezones = new List + { + new(invalidTimeZoneId, invalidTimeZoneId), + new(validTimeZoneId, validTimeZoneId) + }; + + var result = TimeZoneHelper.GetTimezones(timezones); + + result.Count.ShouldBe(1); + + var expectedTimeZoneInfo = TZConvert.GetTimeZoneInfo(validTimeZoneId); + var expectedName = $"{validTimeZoneId} ({TimeZoneHelper.GetTimezoneOffset(expectedTimeZoneInfo)})"; + + result[0].Name.ShouldBe(expectedName); + result[0].Value.ShouldBe(validTimeZoneId); + } + + [Fact] + public void TryCreateNameValueWithOffset_Should_Return_Null_For_Invalid_Timezone() + { + TimeZoneHelper.TryCreateNameValueWithOffset(new NameValue("Invalid/Zone", "Invalid/Zone")).ShouldBeNull(); + } +} diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json index 841cd18775..07309d4922 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json @@ -34,14 +34,14 @@ "DisplayName:PhoneNumber": "Telefon numarası", "PersonalSettings": "Kişisel ayarlar", "PersonalSettingsSaved": "Kişisel ayarlar kaydedildi", - "PersonalSettingsChangedConfirmationModalTitle": "Personal info changed", - "PersonalSettingsChangedConfirmationModalDescription": "If you want to apply these changes, you have to login. Do you want to log out?", + "PersonalSettingsChangedConfirmationModalTitle": "Kişisel bilgiler değişti", + "PersonalSettingsChangedConfirmationModalDescription": "Değişikliklerin yansıtılması için tekrar giriş yapmanız gerekmektedir. Şimdi oturumu kapatmak istiyor musunuz?", "PasswordChanged": "Şifre değiştirildi", "NewPasswordConfirmFailed": "Lütfen yeni şifreyi onaylayın.", "NewPasswordSameAsOld": "Yeni şifre eski şifre ile aynı olamaz.", - "Manage": "Manage", + "Manage": "Yönet", "MyAccount": "Hesabım", - "DisplayName:Abp.Account.IsSelfRegistrationEnabled": "self-registration etkin mi ?", + "DisplayName:Abp.Account.IsSelfRegistrationEnabled": "Kendi kendine kayıt etkin mi?", "Description:Abp.Account.IsSelfRegistrationEnabled": "Bir kullanıcının hesabı kendisi tarafından kaydedip kaydedememesidir.", "DisplayName:Abp.Account.EnableLocalLogin": "Yerel bir hesapla kimlik doğrulaması", "Description:Abp.Account.EnableLocalLogin": "Sunucunun, kullanıcıların yerel bir hesapla kimlik doğrulamasına izin verip vermeyeceğini belirtir.", diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDynamicInitializer.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDynamicInitializer.cs index 32ed4cb646..d96b7b3800 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDynamicInitializer.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDynamicInitializer.cs @@ -1,7 +1,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -19,33 +18,19 @@ public class FeatureDynamicInitializer : ITransientDependency public ILogger Logger { get; set; } protected IServiceProvider ServiceProvider { get; } - protected IOptions Options { get; } - [CanBeNull] - protected IHostApplicationLifetime ApplicationLifetime { get; } - protected ICancellationTokenProvider CancellationTokenProvider { get; } - protected IDynamicFeatureDefinitionStore DynamicFeatureDefinitionStore { get; } - protected IStaticFeatureSaver StaticFeatureSaver { get; } - - public FeatureDynamicInitializer( - IServiceProvider serviceProvider, - IOptions options, - ICancellationTokenProvider cancellationTokenProvider, - IDynamicFeatureDefinitionStore dynamicFeatureDefinitionStore, - IStaticFeatureSaver staticFeatureSaver) + + public FeatureDynamicInitializer(IServiceProvider serviceProvider) { Logger = NullLogger.Instance; ServiceProvider = serviceProvider; - Options = options; - ApplicationLifetime = ServiceProvider.GetService(); - CancellationTokenProvider = cancellationTokenProvider; - DynamicFeatureDefinitionStore = dynamicFeatureDefinitionStore; - StaticFeatureSaver = staticFeatureSaver; } public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default) { - var options = Options.Value; + var options = ServiceProvider + .GetRequiredService>() + .Value; if (!options.SaveStaticFeaturesToDatabase && !options.IsDynamicFeatureStoreEnabled) { @@ -54,11 +39,12 @@ public class FeatureDynamicInitializer : ITransientDependency if (runInBackground) { + var applicationLifetime = ServiceProvider.GetService(); Task.Run(async () => { - if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) + if (cancellationToken == default && applicationLifetime?.ApplicationStopping != null) { - cancellationToken = ApplicationLifetime.ApplicationStopping; + cancellationToken = applicationLifetime.ApplicationStopping; } await ExecuteInitializationAsync(options, cancellationToken); }, cancellationToken); @@ -68,20 +54,23 @@ public class FeatureDynamicInitializer : ITransientDependency return ExecuteInitializationAsync(options, cancellationToken); } - protected virtual async Task ExecuteInitializationAsync(FeatureManagementOptions options, CancellationToken cancellationToken) + protected virtual async Task ExecuteInitializationAsync( + FeatureManagementOptions options, + CancellationToken cancellationToken) { try { - using (CancellationTokenProvider.Use(cancellationToken)) + var cancellationTokenProvider = ServiceProvider.GetRequiredService(); + using (cancellationTokenProvider.Use(cancellationToken)) { - if (CancellationTokenProvider.Token.IsCancellationRequested) + if (cancellationTokenProvider.Token.IsCancellationRequested) { return; } await SaveStaticFeaturesToDatabaseAsync(options, cancellationToken); - if (CancellationTokenProvider.Token.IsCancellationRequested) + if (cancellationTokenProvider.Token.IsCancellationRequested) { return; } @@ -104,6 +93,8 @@ public class FeatureDynamicInitializer : ITransientDependency return; } + var staticFeatureSaver = ServiceProvider.GetRequiredService(); + await Policy .Handle() .WaitAndRetryAsync( @@ -118,7 +109,7 @@ public class FeatureDynamicInitializer : ITransientDependency { try { - await StaticFeatureSaver.SaveAsync(); + await staticFeatureSaver.SaveAsync(); } catch (Exception ex) { @@ -128,17 +119,20 @@ public class FeatureDynamicInitializer : ITransientDependency }, cancellationToken); } - protected virtual async Task PreCacheDynamicFeaturesAsync(FeatureManagementOptions options) + protected virtual async Task PreCacheDynamicFeaturesAsync( + FeatureManagementOptions options) { if (!options.IsDynamicFeatureStoreEnabled) { return; } + var dynamicFeatureDefinitionStore = ServiceProvider.GetRequiredService(); + try { // Pre-cache features, so first request doesn't wait - await DynamicFeatureDefinitionStore.GetGroupsAsync(); + await dynamicFeatureDefinitionStore.GetGroupsAsync(); } catch (Exception ex) { diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/AbpIdentityResultExtensions.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/AbpIdentityResultExtensions.cs index e851cbc6cc..0c1c13c08c 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/AbpIdentityResultExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/AbpIdentityResultExtensions.cs @@ -1,58 +1,10 @@ using System; -using System.Linq; -using System.Collections.Generic; -using System.Globalization; -using System.Resources; -using Microsoft.Extensions.Localization; -using Volo.Abp; using Volo.Abp.Identity; -using Volo.Abp.Text.Formatting; namespace Microsoft.AspNetCore.Identity; public static class AbpIdentityResultExtensions { - private static readonly Dictionary IdentityStrings = new Dictionary(); - - static AbpIdentityResultExtensions() - { - var identityResourceManager = new ResourceManager("Microsoft.Extensions.Identity.Core.Resources", typeof(UserManager<>).Assembly); - var resourceSet = identityResourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, false); - if (resourceSet == null) - { - throw new AbpException("Can't get the ResourceSet of Identity."); - } - - var iterator = resourceSet.GetEnumerator(); - while (true) - { - if (!iterator.MoveNext()) - { - break; - } - - var key = iterator.Key?.ToString(); - var value = iterator.Value?.ToString(); - if (key != null && value != null) - { - IdentityStrings.Add(key, value); - } - } - - if (IdentityStrings.ContainsKey("InvalidUserName")) - { - // The default text of `InvalidUserName` is `Username '{0}' is invalid, can only contain letters or digits.` - IdentityStrings["InvalidUserName"] = "Username '{0}' is invalid."; - } - - IdentityStrings["PasswordInHistory"] = "Passwords must not match your last {0} passwords."; - - if (!IdentityStrings.Any()) - { - throw new AbpException("ResourceSet values of Identity is empty."); - } - } - public static void CheckErrors(this IdentityResult identityResult) { if (identityResult.Succeeded) @@ -68,71 +20,6 @@ public static class AbpIdentityResultExtensions throw new AbpIdentityResultException(identityResult); } - public static string[] GetValuesFromErrorMessage(this IdentityResult identityResult, IStringLocalizer localizer) - { - if (identityResult.Succeeded) - { - throw new ArgumentException( - "identityResult.Succeeded should be false in order to get values from error."); - } - - if (identityResult.Errors == null) - { - throw new ArgumentException("identityResult.Errors should not be null."); - } - - var error = identityResult.Errors.First(); - var englishString = IdentityStrings.GetOrDefault(error.Code); - - if (englishString == null) - { - return Array.Empty(); - } - - if (FormattedStringValueExtracter.IsMatch(error.Description, englishString, out var values)) - { - return values; - } - - return Array.Empty(); - } - - public static string LocalizeErrors(this IdentityResult identityResult, IStringLocalizer localizer) - { - if (identityResult.Succeeded) - { - throw new ArgumentException("identityResult.Succeeded should be false in order to localize errors."); - } - - if (identityResult.Errors == null) - { - throw new ArgumentException("identityResult.Errors should not be null."); - } - - return identityResult.Errors.Select(err => LocalizeErrorMessage(err, localizer)).JoinAsString(", "); - } - - public static string LocalizeErrorMessage(this IdentityError error, IStringLocalizer localizer) - { - var key = $"Volo.Abp.Identity:{error.Code}"; - - var localizedString = localizer[key]; - - if (!localizedString.ResourceNotFound) - { - var englishString = IdentityStrings.GetOrDefault(error.Code); - if (englishString != null) - { - if (FormattedStringValueExtracter.IsMatch(error.Description, englishString, out var values)) - { - return string.Format(localizedString.Value, values.Cast().ToArray()); - } - } - } - - return localizer["Volo.Abp.Identity:DefaultError"]; - } - public static string GetResultAsString(this SignInResult signInResult) { if (signInResult.Succeeded) diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityErrorDescriber.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityErrorDescriber.cs index 3dea11faea..64ee9e58d7 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityErrorDescriber.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityErrorDescriber.cs @@ -1,10 +1,8 @@ -using JetBrains.Annotations; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity.Localization; -using Volo.Abp.Localization; namespace Volo.Abp.Identity; @@ -19,15 +17,202 @@ public class AbpIdentityErrorDescriber : IdentityErrorDescriber Localizer = localizer; } - public override IdentityError InvalidUserName([CanBeNull] string userName) + public override IdentityError DefaultError() { - using (CultureHelper.Use("en")) + return new IdentityError { - return new IdentityError - { - Code = nameof(InvalidUserName), - Description = Localizer["Volo.Abp.Identity:InvalidUserName", userName ?? ""] - }; - } + Code = nameof(DefaultError), + Description = Localizer["Volo.Abp.Identity:DefaultError"] + }; + } + + public override IdentityError ConcurrencyFailure() + { + return new IdentityError + { + Code = nameof(ConcurrencyFailure), + Description = Localizer["Volo.Abp.Identity:ConcurrencyFailure"] + }; + } + + public override IdentityError PasswordMismatch() + { + return new IdentityError + { + Code = nameof(PasswordMismatch), + Description = Localizer["Volo.Abp.Identity:PasswordMismatch"] + }; + } + + public override IdentityError InvalidToken() + { + return new IdentityError + { + Code = nameof(InvalidToken), + Description = Localizer["Volo.Abp.Identity:InvalidToken"] + }; + } + + public override IdentityError RecoveryCodeRedemptionFailed() + { + return new IdentityError + { + Code = nameof(RecoveryCodeRedemptionFailed), + Description = Localizer["Volo.Abp.Identity:RecoveryCodeRedemptionFailed"] + }; + } + + public override IdentityError LoginAlreadyAssociated() + { + return new IdentityError + { + Code = nameof(LoginAlreadyAssociated), + Description = Localizer["Volo.Abp.Identity:LoginAlreadyAssociated"] + }; + } + + public override IdentityError InvalidUserName(string? userName) + { + return new IdentityError + { + Code = nameof(InvalidUserName), + Description = Localizer["Volo.Abp.Identity:InvalidUserName", userName ?? ""] + }; + } + + public override IdentityError InvalidEmail(string? email) + { + return new IdentityError + { + Code = nameof(InvalidEmail), + Description = Localizer["Volo.Abp.Identity:InvalidEmail", email ?? ""] + }; + } + + public override IdentityError DuplicateUserName(string userName) + { + return new IdentityError + { + Code = nameof(DuplicateUserName), + Description = Localizer["Volo.Abp.Identity:DuplicateUserName", userName] + }; + } + + public override IdentityError DuplicateEmail(string email) + { + return new IdentityError + { + Code = nameof(DuplicateEmail), + Description = Localizer["Volo.Abp.Identity:DuplicateEmail", email] + }; + } + + public override IdentityError InvalidRoleName(string? role) + { + return new IdentityError + { + Code = nameof(InvalidRoleName), + Description = Localizer["Volo.Abp.Identity:InvalidRoleName", role ?? ""] + }; + } + + public override IdentityError DuplicateRoleName(string role) + { + return new IdentityError + { + Code = nameof(DuplicateRoleName), + Description = Localizer["Volo.Abp.Identity:DuplicateRoleName", role] + }; + } + + public override IdentityError UserAlreadyHasPassword() + { + return new IdentityError + { + Code = nameof(UserAlreadyHasPassword), + Description = Localizer["Volo.Abp.Identity:UserAlreadyHasPassword"] + }; + } + + public override IdentityError UserLockoutNotEnabled() + { + return new IdentityError + { + Code = nameof(UserLockoutNotEnabled), + Description = Localizer["Volo.Abp.Identity:UserLockoutNotEnabled"] + }; + } + + public override IdentityError UserAlreadyInRole(string role) + { + return new IdentityError + { + Code = nameof(UserAlreadyInRole), + Description = Localizer["Volo.Abp.Identity:UserAlreadyInRole", role] + }; + } + + public override IdentityError UserNotInRole(string role) + { + return new IdentityError + { + Code = nameof(UserNotInRole), + Description = Localizer["Volo.Abp.Identity:UserNotInRole", role] + }; + } + + public override IdentityError PasswordTooShort(int length) + { + return new IdentityError + { + Code = nameof(PasswordTooShort), + Description = Localizer["Volo.Abp.Identity:PasswordTooShort", length] + }; + } + + public override IdentityError PasswordRequiresUniqueChars(int uniqueChars) + { + return new IdentityError + { + Code = nameof(PasswordRequiresUniqueChars), + Description = Localizer["Volo.Abp.Identity:PasswordRequiresUniqueChars", uniqueChars] + }; + } + + public override IdentityError PasswordRequiresNonAlphanumeric() + { + return new IdentityError + { + Code = nameof(PasswordRequiresNonAlphanumeric), + Description = Localizer["Volo.Abp.Identity:PasswordRequiresNonAlphanumeric"] + }; + } + + + public override IdentityError PasswordRequiresDigit() + { + return new IdentityError + { + Code = nameof(PasswordRequiresDigit), + Description = Localizer["Volo.Abp.Identity:PasswordRequiresDigit"] + }; + } + + public override IdentityError PasswordRequiresLower() + { + return new IdentityError + { + Code = nameof(PasswordRequiresLower), + Description = Localizer["Volo.Abp.Identity:PasswordRequiresLower"] + }; + } + + public override IdentityError PasswordRequiresUpper() + { + return new IdentityError + { + Code = nameof(PasswordRequiresUpper), + Description = Localizer["Volo.Abp.Identity:PasswordRequiresUpper"] + }; } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityResultException.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityResultException.cs index 03a8c6f881..a5b4156d2b 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityResultException.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityResultException.cs @@ -2,14 +2,10 @@ using System.Linq; using JetBrains.Annotations; using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Localization; -using Volo.Abp.ExceptionHandling; -using Volo.Abp.Identity.Localization; -using Volo.Abp.Localization; namespace Volo.Abp.Identity; -public class AbpIdentityResultException : BusinessException, ILocalizeErrorMessage +public class AbpIdentityResultException : BusinessException { public IdentityResult IdentityResult { get; } @@ -20,23 +16,4 @@ public class AbpIdentityResultException : BusinessException, ILocalizeErrorMessa { IdentityResult = Check.NotNull(identityResult, nameof(identityResult)); } - - public virtual string LocalizeMessage(LocalizationContext context) - { - var localizer = context.LocalizerFactory.Create(); - - SetData(localizer); - - return IdentityResult.LocalizeErrors(localizer); - } - - protected virtual void SetData(IStringLocalizer localizer) - { - var values = IdentityResult.GetValuesFromErrorMessage(localizer); - - for (var index = 0; index < values.Length; index++) - { - Data[index.ToString()] = values[index]; - } - } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityUserValidator.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityUserValidator.cs index 84d1a09c25..05c733c0c0 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityUserValidator.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityUserValidator.cs @@ -17,8 +17,6 @@ namespace Volo.Abp.Identity public virtual async Task ValidateAsync(UserManager manager, IdentityUser user) { - var describer = new IdentityErrorDescriber(); - Check.NotNull(manager, nameof(manager)); Check.NotNull(user, nameof(user)); @@ -27,36 +25,28 @@ namespace Volo.Abp.Identity var userName = await manager.GetUserNameAsync(user); if (userName == null) { - errors.Add(describer.InvalidUserName(null)); + errors.Add(manager.ErrorDescriber.InvalidUserName(null)); } else { var owner = await manager.FindByEmailAsync(userName); if (owner != null && !string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user))) { - errors.Add(new IdentityError - { - Code = "InvalidUserName", - Description = Localizer["Volo.Abp.Identity:InvalidUserName", userName] - }); + errors.Add(manager.ErrorDescriber.InvalidUserName(userName)); } } var email = await manager.GetEmailAsync(user); if (email == null) { - errors.Add(describer.InvalidEmail(null)); + errors.Add(manager.ErrorDescriber.InvalidEmail(null)); } else { var owner = await manager.FindByNameAsync(email); if (owner != null && !string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user))) { - errors.Add(new IdentityError - { - Code = "InvalidEmail", - Description = Localizer["Volo.Abp.Identity:InvalidEmail", email] - }); + errors.Add(manager.ErrorDescriber.InvalidEmail(email)); } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs index a4fc897bf4..41da1e5c22 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs @@ -553,6 +553,6 @@ public class IdentityUserManager : UserManager, IDomainService } Logger.LogError($"Could not get a valid user name for the given email address: {email}, allowed characters: {Options.User.AllowedUserNameCharacters}, tried {maxTryCount} times."); - throw new AbpIdentityResultException(IdentityResult.Failed(new IdentityErrorDescriber().InvalidUserName(userName))); + throw new AbpIdentityResultException(IdentityResult.Failed(ErrorDescriber.InvalidUserName(userName))); } } diff --git a/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs index 3272929c3e..acd50b7947 100644 --- a/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs @@ -162,7 +162,7 @@ public class IdentityUserAppService_Tests : AbpIdentityApplicationTestBase (await Assert.ThrowsAsync(async () => { await _userAppService.UpdateAsync(johnNash.Id, input); - })).Message.ShouldContain("Optimistic concurrency failure"); + })).Message.ShouldContain("Optimistic concurrency check has been failed. The entity you're working on has modified by another user. Please discard your changes and try again."); } [Fact] diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/AbpIdentityErrorDescriber_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/AbpIdentityErrorDescriber_Tests.cs new file mode 100644 index 0000000000..a7f68ec0b2 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/AbpIdentityErrorDescriber_Tests.cs @@ -0,0 +1,468 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp; +using Volo.Abp.Localization; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Identity; + +public class AbpIdentityErrorDescriber_Tests : AbpIdentityDomainTestBase +{ + [Fact] + public void Should_Localize_Error_Messages() + { + var describer = GetRequiredService(); + + using (CultureHelper.Use("en")) + { + describer.DefaultError().Description.ShouldBe("An unknown failure has occurred."); + describer.ConcurrencyFailure().Description.ShouldBe("Optimistic concurrency check has been failed. The entity you're working on has modified by another user. Please discard your changes and try again."); + describer.PasswordMismatch().Description.ShouldBe("Incorrect password."); + describer.InvalidToken().Description.ShouldBe("Invalid token."); + describer.RecoveryCodeRedemptionFailed().Description.ShouldBe("Recovery code redemption failed."); + describer.LoginAlreadyAssociated().Description.ShouldBe("A user with this login already exists."); + describer.InvalidUserName("john").Description.ShouldBe("Username 'john' is invalid."); + describer.InvalidEmail("john@abp.io").Description.ShouldBe("Email 'john@abp.io' is invalid."); + describer.DuplicateUserName("john").Description.ShouldBe("Username 'john' is already taken."); + describer.DuplicateEmail("john@abp.io").Description.ShouldBe("Email 'john@abp.io' is already taken."); + describer.InvalidRoleName("admin").Description.ShouldBe("Role name 'admin' is invalid."); + describer.DuplicateRoleName("admin").Description.ShouldBe("Role name 'admin' is already taken."); + describer.UserAlreadyHasPassword().Description.ShouldBe("User already has a password set."); + describer.UserLockoutNotEnabled().Description.ShouldBe("Lockout is not enabled for this user."); + describer.UserAlreadyInRole("admin").Description.ShouldBe("User already in role 'admin'."); + describer.UserNotInRole("admin").Description.ShouldBe("User is not in role 'admin'."); + describer.PasswordTooShort(6).Description.ShouldBe("Password length must be greater than 6 characters."); + describer.PasswordRequiresUniqueChars(3).Description.ShouldBe("Passwords must use at least 3 different characters."); + describer.PasswordRequiresNonAlphanumeric().Description.ShouldBe("Password must contain at least one non-alphanumeric character."); + describer.PasswordRequiresDigit().Description.ShouldBe("Passwords must have at least one digit ('0'-'9')."); + describer.PasswordRequiresLower().Description.ShouldBe("Passwords must have at least one lowercase ('a'-'z')."); + describer.PasswordRequiresUpper().Description.ShouldBe("Passwords must have at least one uppercase ('A'-'Z')."); + } + + using (CultureHelper.Use("tr")) + { + describer.DefaultError().Description.ShouldBe("Bilinmeyen bir hata oluştu."); + describer.ConcurrencyFailure().Description.ShouldBe("İyimser eşzamanlılık denetimi başarısız oldu. Üzerinde çalıştığınız varlık başka bir kullanıcı tarafından değiştirildi. Lütfen değişikliklerinizi geri alın ve tekrar deneyin."); + describer.PasswordMismatch().Description.ShouldBe("Hatalı şifre."); + describer.InvalidToken().Description.ShouldBe("Geçersiz token."); + describer.RecoveryCodeRedemptionFailed().Description.ShouldBe("Kurtarma kodu kullanılamadı."); + describer.LoginAlreadyAssociated().Description.ShouldBe("Bu giriş bilgilerine sahip bir kullanıcı zaten var."); + describer.InvalidUserName("john").Description.ShouldBe("'john' kullanıcı adı geçersiz."); + describer.InvalidEmail("john@abp.io").Description.ShouldBe("'john@abp.io' email adresi hatalı."); + describer.DuplicateUserName("john").Description.ShouldBe("'john' kullanıcı adı zaten alınmış."); + describer.DuplicateEmail("john@abp.io").Description.ShouldBe("'john@abp.io' email adresi zaten alınmış."); + describer.InvalidRoleName("admin").Description.ShouldBe("'admin' rol ismi geçersizdir."); + describer.DuplicateRoleName("admin").Description.ShouldBe("'admin' rol ismi zaten alınmış."); + describer.UserAlreadyHasPassword().Description.ShouldBe("Kullanıcının zaten bir şifresi var."); + describer.UserLockoutNotEnabled().Description.ShouldBe("Bu kullanıcı için hesap kilitleme etkin değil."); + describer.UserAlreadyInRole("admin").Description.ShouldBe("Kullanıcı zaten 'admin' rolünde."); + describer.UserNotInRole("admin").Description.ShouldBe("Kullanıcı 'admin' rolünde değil."); + describer.PasswordTooShort(6).Description.ShouldBe("Şifre uzunluğu 6 karakterden uzun olmalıdır."); + describer.PasswordRequiresUniqueChars(3).Description.ShouldBe("Şifre en az 3 farklı karakter içermeli."); + describer.PasswordRequiresNonAlphanumeric().Description.ShouldBe("Parola en az bir alfasayısal olmayan karakter içermelidir."); + describer.PasswordRequiresDigit().Description.ShouldBe("Şifre en az bir sayı içermeli ('0'-'9')."); + describer.PasswordRequiresLower().Description.ShouldBe("Şifre en az bir küçük harf içermeli ('a'-'z')."); + describer.PasswordRequiresUpper().Description.ShouldBe("Şifre en az bir büyük harf içermeli ('A'-'Z')."); + } + + using (CultureHelper.Use("zh-Hans")) + { + describer.DefaultError().Description.ShouldBe("发生了一个未知错误。"); + describer.ConcurrencyFailure().Description.ShouldBe("乐观并发检查失败. 你正在处理的对象已被其他用户修改. 请放弃你的更改, 然后重试。"); + describer.PasswordMismatch().Description.ShouldBe("密码错误。"); + describer.InvalidToken().Description.ShouldBe("token无效。"); + describer.RecoveryCodeRedemptionFailed().Description.ShouldBe("恢复代码兑换失败。"); + describer.LoginAlreadyAssociated().Description.ShouldBe("此登录名的用户已存在。"); + describer.InvalidUserName("john").Description.ShouldBe("用户名 'john' 无效。"); + describer.InvalidEmail("john@abp.io").Description.ShouldBe("邮箱 'john@abp.io' 无效。"); + describer.DuplicateUserName("john").Description.ShouldBe("用户名 'john' 已存在。"); + describer.DuplicateEmail("john@abp.io").Description.ShouldBe("邮箱 'john@abp.io' 已存在。"); + describer.InvalidRoleName("admin").Description.ShouldBe("角色名 'admin' 无效。"); + describer.DuplicateRoleName("admin").Description.ShouldBe("角色名 'admin' 已存在。"); + describer.UserAlreadyHasPassword().Description.ShouldBe("用户已设置密码。"); + describer.UserLockoutNotEnabled().Description.ShouldBe("该用户未启用锁定。"); + describer.UserAlreadyInRole("admin").Description.ShouldBe("用户已具有角色 'admin'。"); + describer.UserNotInRole("admin").Description.ShouldBe("用户不具有 'admin' 角色。"); + describer.PasswordTooShort(6).Description.ShouldBe("密码长度必须大于 6 字符。"); + describer.PasswordRequiresUniqueChars(3).Description.ShouldBe("密码至少包含3个唯一字符。"); + describer.PasswordRequiresNonAlphanumeric().Description.ShouldBe("密码必须至少包含一个非字母数字字符。"); + describer.PasswordRequiresDigit().Description.ShouldBe("密码至少包含一位数字 ('0'-'9')。"); + describer.PasswordRequiresLower().Description.ShouldBe("密码至少包含一位小写字母 ('a'-'z')。"); + describer.PasswordRequiresUpper().Description.ShouldBe("密码至少包含一位大写字母 ('A'-'Z')。"); + } + } + + [Fact] + public async Task Should_Localize_UserManager_Errors() + { + using (GetRequiredService().Begin()) + { + var user = new IdentityUser(Guid.NewGuid(), "um_localize_user", "um_localize_user@abp.io"); + + using (CultureHelper.Use("en")) + { + var userManager = GetRequiredService(); + var result = await userManager.CreateAsync(user, "abc", validatePassword: true); + + result.Succeeded.ShouldBeFalse(); + var message = string.Join("; ", result.Errors.Select(e => e.Description)); + message.ShouldContain("Password length must be greater than 6 characters."); + message.ShouldContain("Password must contain at least one non-alphanumeric character."); + message.ShouldContain("Passwords must have at least one digit ('0'-'9')."); + message.ShouldContain("Passwords must have at least one uppercase ('A'-'Z')."); + + var invalidUser = new IdentityUser(Guid.NewGuid(), "invalid user?", "not-an-email"); + var invalidResult = await userManager.CreateAsync(invalidUser, "Abp123!"); + + invalidResult.Succeeded.ShouldBeFalse(); + var invalidMessage = string.Join("; ", invalidResult.Errors.Select(e => e.Description)); + invalidMessage.ShouldContain("Username 'invalid user?' is invalid."); + invalidMessage.ShouldContain("Email 'not-an-email' is invalid."); + + var firstUser = new IdentityUser(Guid.NewGuid(), "dup_user_en", "dup_user_en@abp.io"); + (await userManager.CreateAsync(firstUser, "Abp123!")).Succeeded.ShouldBeTrue(); + + var duplicateUser = new IdentityUser(Guid.NewGuid(), "dup_user_en", "dup_user_en@abp.io"); + var duplicateResult = await userManager.CreateAsync(duplicateUser, "Abp123!"); + + duplicateResult.Succeeded.ShouldBeFalse(); + var duplicateMessage = string.Join("; ", duplicateResult.Errors.Select(e => e.Description)); + duplicateMessage.ShouldContain("Username 'dup_user_en' is already taken."); + duplicateMessage.ShouldContain("Email 'dup_user_en@abp.io' is already taken."); + + var persistedUser = await userManager.FindByIdAsync(firstUser.Id.ToString()); + var addPasswordResult = await userManager.AddPasswordAsync(persistedUser, "Another123!"); + + addPasswordResult.Succeeded.ShouldBeFalse(); + addPasswordResult.Errors.ShouldContain(e => e.Description == "User already has a password set."); + + var roleManager = GetRequiredService(); + var roleName = "localized_role_en"; + (await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), roleName))).Succeeded.ShouldBeTrue(); + + var roleUser = new IdentityUser(Guid.NewGuid(), "role_user_en", "role_user_en@abp.io"); + (await userManager.CreateAsync(roleUser, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + (await userManager.AddToRoleAsync(roleUser, roleName)).Succeeded.ShouldBeTrue(); + var alreadyInRoleResult = await userManager.AddToRoleAsync(roleUser, roleName); + alreadyInRoleResult.Succeeded.ShouldBeFalse(); + alreadyInRoleResult.Errors.ShouldContain(e => e.Description == $"User already in role '{roleName}'."); + + var notInRoleUser = new IdentityUser(Guid.NewGuid(), "notinrole_user_en", "notinrole_user_en@abp.io"); + (await userManager.CreateAsync(notInRoleUser, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var notInRoleResult = await userManager.RemoveFromRoleAsync(notInRoleUser, roleName); + notInRoleResult.Succeeded.ShouldBeFalse(); + notInRoleResult.Errors.ShouldContain(e => e.Description == $"User is not in role '{roleName}'."); + + var loginUser1 = new IdentityUser(Guid.NewGuid(), "login_user1_en", "login_user1_en@abp.io"); + (await userManager.CreateAsync(loginUser1, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var loginInfo = new UserLoginInfo("Google", "assoc_key_en", "Google"); + (await userManager.AddLoginAsync(loginUser1, loginInfo)).Succeeded.ShouldBeTrue(); + + var loginUser2 = new IdentityUser(Guid.NewGuid(), "login_user2_en", "login_user2_en@abp.io"); + (await userManager.CreateAsync(loginUser2, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var loginConflict = await userManager.AddLoginAsync(loginUser2, loginInfo); + loginConflict.Succeeded.ShouldBeFalse(); + loginConflict.Errors.ShouldContain(e => e.Description == "A user with this login already exists."); + + var noLowerUser = new IdentityUser(Guid.NewGuid(), "nolower_user_en", "nolower_user_en@abp.io"); + var noLowerResult = await userManager.CreateAsync(noLowerUser, "ABC123!"); + + noLowerResult.Succeeded.ShouldBeFalse(); + var noLowerMessage = string.Join("; ", noLowerResult.Errors.Select(e => e.Description)); + noLowerMessage.ShouldContain("Passwords must have at least one lowercase ('a'-'z')."); + + var options = GetRequiredService>().Value; + var originalUniqueChars = options.Password.RequiredUniqueChars; + options.Password.RequiredUniqueChars = 3; + + var uniqueUser = new IdentityUser(Guid.NewGuid(), "unique_user_en", "unique_user_en@abp.io"); + var uniqueResult = await userManager.CreateAsync(uniqueUser, "aaaaaa!"); + + uniqueResult.Succeeded.ShouldBeFalse(); + var uniqueMessage = string.Join("; ", uniqueResult.Errors.Select(e => e.Description)); + uniqueMessage.ShouldContain("Passwords must use at least 3 different characters."); + + options.Password.RequiredUniqueChars = originalUniqueChars; + + var mismatchUser = new IdentityUser(Guid.NewGuid(), "mismatch_user_en", "mismatch_user_en@abp.io"); + (await userManager.CreateAsync(mismatchUser, "Abp123!")).Succeeded.ShouldBeTrue(); + + var mismatchResult = await userManager.ChangePasswordAsync(mismatchUser, "WrongOld123!", "NewAbp123!"); + mismatchResult.Succeeded.ShouldBeFalse(); + mismatchResult.Errors.ShouldContain(e => e.Description == "Incorrect password."); + + var recoveryUser = new IdentityUser(Guid.NewGuid(), "recovery_user_en", "recovery_user_en@abp.io"); + ObjectHelper.TrySetProperty(recoveryUser, x => x.TwoFactorEnabled, () => true); + + (await userManager.CreateAsync(recoveryUser, "Abp123!")).Succeeded.ShouldBeTrue(); + var recoveryResult = await userManager.RedeemTwoFactorRecoveryCodeAsync(recoveryUser, "invalid-code"); + recoveryResult.Succeeded.ShouldBeFalse(); + recoveryResult.Errors.ShouldContain(e => e.Description == "Recovery code redemption failed."); + + var invalidRoleResult = await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), "")); + invalidRoleResult.Succeeded.ShouldBeFalse(); + invalidRoleResult.Errors.ShouldContain(e => e.Description == "Role name '' is invalid."); + + var duplicateRoleName = "duplicate_role_en"; + (await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), duplicateRoleName))).Succeeded.ShouldBeTrue(); + var duplicateRoleResult = await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), duplicateRoleName)); + duplicateRoleResult.Succeeded.ShouldBeFalse(); + duplicateRoleResult.Errors.ShouldContain(e => e.Description == $"Role name '{duplicateRoleName}' is already taken."); + } + + // Recreate a fresh user per culture to avoid state issues + user = new IdentityUser(Guid.NewGuid(), "um_localize_user_tr", "um_localize_user_tr@abp.io"); + using (CultureHelper.Use("tr")) + { + var userManager = GetRequiredService(); + var result = await userManager.CreateAsync(user, "abc", validatePassword: true); + + result.Succeeded.ShouldBeFalse(); + var message = string.Join("; ", result.Errors.Select(e => e.Description)); + message.ShouldContain("Şifre uzunluğu 6 karakterden uzun olmalıdır."); + message.ShouldContain("Parola en az bir alfasayısal olmayan karakter içermelidir."); + message.ShouldContain("Şifre en az bir sayı içermeli ('0'-'9')."); + message.ShouldContain("Şifre en az bir büyük harf içermeli ('A'-'Z')."); + + var invalidUser = new IdentityUser(Guid.NewGuid(), "invalid user?", "not-an-email"); + var invalidResult = await userManager.CreateAsync(invalidUser, "Abp123!"); + + invalidResult.Succeeded.ShouldBeFalse(); + var invalidMessage = string.Join("; ", invalidResult.Errors.Select(e => e.Description)); + invalidMessage.ShouldContain("'invalid user?' kullanıcı adı geçersiz."); + invalidMessage.ShouldContain("'not-an-email' email adresi hatalı."); + + var firstUser = new IdentityUser(Guid.NewGuid(), "dup_user_tr", "dup_user_tr@abp.io"); + (await userManager.CreateAsync(firstUser, "Abp123!")).Succeeded.ShouldBeTrue(); + + var duplicateUser = new IdentityUser(Guid.NewGuid(), "dup_user_tr", "dup_user_tr@abp.io"); + var duplicateResult = await userManager.CreateAsync(duplicateUser, "Abp123!"); + + duplicateResult.Succeeded.ShouldBeFalse(); + var duplicateMessage = string.Join("; ", duplicateResult.Errors.Select(e => e.Description)); + duplicateMessage.ShouldContain("'dup_user_tr' kullanıcı adı zaten alınmış."); + duplicateMessage.ShouldContain("'dup_user_tr@abp.io' email adresi zaten alınmış."); + + var persistedUser = await userManager.FindByIdAsync(firstUser.Id.ToString()); + var addPasswordResult = await userManager.AddPasswordAsync(persistedUser, "Another123!"); + + addPasswordResult.Succeeded.ShouldBeFalse(); + addPasswordResult.Errors.ShouldContain(e => e.Description == "Kullanıcının zaten bir şifresi var."); + + var roleManager = GetRequiredService(); + var roleName = "localized_role_tr"; + (await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), roleName))).Succeeded.ShouldBeTrue(); + + var roleUser = new IdentityUser(Guid.NewGuid(), "role_user_tr", "role_user_tr@abp.io"); + (await userManager.CreateAsync(roleUser, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + (await userManager.AddToRoleAsync(roleUser, roleName)).Succeeded.ShouldBeTrue(); + var alreadyInRoleResult = await userManager.AddToRoleAsync(roleUser, roleName); + alreadyInRoleResult.Succeeded.ShouldBeFalse(); + alreadyInRoleResult.Errors.ShouldContain(e => e.Description == $"Kullanıcı zaten '{roleName}' rolünde."); + + var notInRoleUser = new IdentityUser(Guid.NewGuid(), "notinrole_user_tr", "notinrole_user_tr@abp.io"); + (await userManager.CreateAsync(notInRoleUser, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var notInRoleResult = await userManager.RemoveFromRoleAsync(notInRoleUser, roleName); + notInRoleResult.Succeeded.ShouldBeFalse(); + notInRoleResult.Errors.ShouldContain(e => e.Description == $"Kullanıcı '{roleName}' rolünde değil."); + + var loginUser1 = new IdentityUser(Guid.NewGuid(), "login_user1_tr", "login_user1_tr@abp.io"); + (await userManager.CreateAsync(loginUser1, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var loginInfo = new UserLoginInfo("Google", "assoc_key_tr", "Google"); + (await userManager.AddLoginAsync(loginUser1, loginInfo)).Succeeded.ShouldBeTrue(); + + var loginUser2 = new IdentityUser(Guid.NewGuid(), "login_user2_tr", "login_user2_tr@abp.io"); + (await userManager.CreateAsync(loginUser2, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var loginConflict = await userManager.AddLoginAsync(loginUser2, loginInfo); + loginConflict.Succeeded.ShouldBeFalse(); + loginConflict.Errors.ShouldContain(e => e.Description == "Bu giriş bilgilerine sahip bir kullanıcı zaten var."); + + var noLowerUser = new IdentityUser(Guid.NewGuid(), "nolower_user_tr", "nolower_user_tr@abp.io"); + var noLowerResult = await userManager.CreateAsync(noLowerUser, "ABC123!"); + + noLowerResult.Succeeded.ShouldBeFalse(); + var noLowerMessage = string.Join("; ", noLowerResult.Errors.Select(e => e.Description)); + noLowerMessage.ShouldContain("Şifre en az bir küçük harf içermeli ('a'-'z')."); + + var options = GetRequiredService>().Value; + var originalUniqueChars = options.Password.RequiredUniqueChars; + options.Password.RequiredUniqueChars = 3; + + var uniqueUser = new IdentityUser(Guid.NewGuid(), "unique_user_tr", "unique_user_tr@abp.io"); + var uniqueResult = await userManager.CreateAsync(uniqueUser, "aaaaaa!"); + + uniqueResult.Succeeded.ShouldBeFalse(); + var uniqueMessage = string.Join("; ", uniqueResult.Errors.Select(e => e.Description)); + uniqueMessage.ShouldContain("Şifre en az 3 farklı karakter içermeli."); + + options.Password.RequiredUniqueChars = originalUniqueChars; + + var mismatchUser = new IdentityUser(Guid.NewGuid(), "mismatch_user_tr", "mismatch_user_tr@abp.io"); + (await userManager.CreateAsync(mismatchUser, "Abp123!")).Succeeded.ShouldBeTrue(); + + var mismatchResult = await userManager.ChangePasswordAsync(mismatchUser, "WrongOld123!", "NewAbp123!"); + mismatchResult.Succeeded.ShouldBeFalse(); + mismatchResult.Errors.ShouldContain(e => e.Description == "Hatalı şifre."); + + var recoveryUser = new IdentityUser(Guid.NewGuid(), "recovery_user_tr", "recovery_user_tr@abp.io"); + ObjectHelper.TrySetProperty(recoveryUser, x => x.TwoFactorEnabled, () => true); + + (await userManager.CreateAsync(recoveryUser, "Abp123!")).Succeeded.ShouldBeTrue(); + var recoveryResult = await userManager.RedeemTwoFactorRecoveryCodeAsync(recoveryUser, "invalid-code"); + recoveryResult.Succeeded.ShouldBeFalse(); + recoveryResult.Errors.ShouldContain(e => e.Description == "Kurtarma kodu kullanılamadı."); + + var invalidRoleResult = await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), "")); + invalidRoleResult.Succeeded.ShouldBeFalse(); + invalidRoleResult.Errors.ShouldContain(e => e.Description == "'' rol ismi geçersizdir."); + + var duplicateRoleName = "duplicate_role_tr"; + (await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), duplicateRoleName))).Succeeded.ShouldBeTrue(); + var duplicateRoleResult = await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), duplicateRoleName)); + duplicateRoleResult.Succeeded.ShouldBeFalse(); + duplicateRoleResult.Errors.ShouldContain(e => e.Description == $"'{duplicateRoleName}' rol ismi zaten alınmış."); + } + + user = new IdentityUser(Guid.NewGuid(), "um_localize_user_zh", "um_localize_user_zh@abp.io"); + using (CultureHelper.Use("zh-Hans")) + { + var userManager = GetRequiredService(); + var result = await userManager.CreateAsync(user, "abc", validatePassword: true); + + result.Succeeded.ShouldBeFalse(); + var message = string.Join("; ", result.Errors.Select(e => e.Description)); + message.ShouldContain("密码长度必须大于 6 字符。"); + message.ShouldContain("密码必须至少包含一个非字母数字字符。"); + message.ShouldContain("密码至少包含一位数字 ('0'-'9')。"); + message.ShouldContain("密码至少包含一位大写字母 ('A'-'Z')。"); + + var invalidUser = new IdentityUser(Guid.NewGuid(), "invalid user?", "not-an-email"); + var invalidResult = await userManager.CreateAsync(invalidUser, "Abp123!"); + + invalidResult.Succeeded.ShouldBeFalse(); + var invalidMessage = string.Join("; ", invalidResult.Errors.Select(e => e.Description)); + invalidMessage.ShouldContain("用户名 'invalid user?' 无效。"); + invalidMessage.ShouldContain("邮箱 'not-an-email' 无效。"); + + var firstUser = new IdentityUser(Guid.NewGuid(), "dup_user_zh", "dup_user_zh@abp.io"); + (await userManager.CreateAsync(firstUser, "Abp123!")).Succeeded.ShouldBeTrue(); + + var duplicateUser = new IdentityUser(Guid.NewGuid(), "dup_user_zh", "dup_user_zh@abp.io"); + var duplicateResult = await userManager.CreateAsync(duplicateUser, "Abp123!"); + + duplicateResult.Succeeded.ShouldBeFalse(); + var duplicateMessage = string.Join("; ", duplicateResult.Errors.Select(e => e.Description)); + duplicateMessage.ShouldContain("用户名 'dup_user_zh' 已存在。"); + duplicateMessage.ShouldContain("邮箱 'dup_user_zh@abp.io' 已存在。"); + + var persistedUser = await userManager.FindByIdAsync(firstUser.Id.ToString()); + var addPasswordResult = await userManager.AddPasswordAsync(persistedUser, "Another123!"); + + addPasswordResult.Succeeded.ShouldBeFalse(); + addPasswordResult.Errors.ShouldContain(e => e.Description == "用户已设置密码。"); + + var roleManager = GetRequiredService(); + var roleName = "localized_role_zh"; + (await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), roleName))).Succeeded.ShouldBeTrue(); + + var roleUser = new IdentityUser(Guid.NewGuid(), "role_user_zh", "role_user_zh@abp.io"); + (await userManager.CreateAsync(roleUser, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + (await userManager.AddToRoleAsync(roleUser, roleName)).Succeeded.ShouldBeTrue(); + var alreadyInRoleResult = await userManager.AddToRoleAsync(roleUser, roleName); + alreadyInRoleResult.Succeeded.ShouldBeFalse(); + alreadyInRoleResult.Errors.ShouldContain(e => e.Description == $"用户已具有角色 '{roleName}'。"); + + var notInRoleUser = new IdentityUser(Guid.NewGuid(), "notinrole_user_zh", "notinrole_user_zh@abp.io"); + (await userManager.CreateAsync(notInRoleUser, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var notInRoleResult = await userManager.RemoveFromRoleAsync(notInRoleUser, roleName); + notInRoleResult.Succeeded.ShouldBeFalse(); + notInRoleResult.Errors.ShouldContain(e => e.Description == $"用户不具有 '{roleName}' 角色。"); + + var loginUser1 = new IdentityUser(Guid.NewGuid(), "login_user1_zh", "login_user1_zh@abp.io"); + (await userManager.CreateAsync(loginUser1, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var loginInfo = new UserLoginInfo("Google", "assoc_key_zh", "Google"); + (await userManager.AddLoginAsync(loginUser1, loginInfo)).Succeeded.ShouldBeTrue(); + + var loginUser2 = new IdentityUser(Guid.NewGuid(), "login_user2_zh", "login_user2_zh@abp.io"); + (await userManager.CreateAsync(loginUser2, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var loginConflict = await userManager.AddLoginAsync(loginUser2, loginInfo); + loginConflict.Succeeded.ShouldBeFalse(); + loginConflict.Errors.ShouldContain(e => e.Description == "此登录名的用户已存在。"); + + var noLowerUser = new IdentityUser(Guid.NewGuid(), "nolower_user_zh", "nolower_user_zh@abp.io"); + var noLowerResult = await userManager.CreateAsync(noLowerUser, "ABC123!"); + + noLowerResult.Succeeded.ShouldBeFalse(); + var noLowerMessage = string.Join("; ", noLowerResult.Errors.Select(e => e.Description)); + noLowerMessage.ShouldContain("密码至少包含一位小写字母 ('a'-'z')。"); + + var options = GetRequiredService>().Value; + var originalUniqueChars = options.Password.RequiredUniqueChars; + options.Password.RequiredUniqueChars = 3; + + var uniqueUser = new IdentityUser(Guid.NewGuid(), "unique_user_zh", "unique_user_zh@abp.io"); + var uniqueResult = await userManager.CreateAsync(uniqueUser, "aaaaaa!"); + + uniqueResult.Succeeded.ShouldBeFalse(); + var uniqueMessage = string.Join("; ", uniqueResult.Errors.Select(e => e.Description)); + uniqueMessage.ShouldContain("密码至少包含3个唯一字符。"); + + options.Password.RequiredUniqueChars = originalUniqueChars; + + var mismatchUser = new IdentityUser(Guid.NewGuid(), "mismatch_user_zh", "mismatch_user_zh@abp.io"); + (await userManager.CreateAsync(mismatchUser, "Abp123!")).Succeeded.ShouldBeTrue(); + + var mismatchResult = await userManager.ChangePasswordAsync(mismatchUser, "WrongOld123!", "NewAbp123!"); + mismatchResult.Succeeded.ShouldBeFalse(); + mismatchResult.Errors.ShouldContain(e => e.Description == "密码错误。"); + + var recoveryUser = new IdentityUser(Guid.NewGuid(), "recovery_user_zh", "recovery_user_zh@abp.io"); + ObjectHelper.TrySetProperty(recoveryUser, x => x.TwoFactorEnabled, () => true); + + (await userManager.CreateAsync(recoveryUser, "Abp123!")).Succeeded.ShouldBeTrue(); + var recoveryResult = await userManager.RedeemTwoFactorRecoveryCodeAsync(recoveryUser, "invalid-code"); + recoveryResult.Succeeded.ShouldBeFalse(); + recoveryResult.Errors.ShouldContain(e => e.Description == "恢复代码兑换失败。"); + + var invalidRoleResult = await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), "")); + invalidRoleResult.Succeeded.ShouldBeFalse(); + invalidRoleResult.Errors.ShouldContain(e => e.Description == "角色名 '' 无效。"); + + var duplicateRoleName = "duplicate_role_zh"; + (await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), duplicateRoleName))).Succeeded.ShouldBeTrue(); + var duplicateRoleResult = await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), duplicateRoleName)); + duplicateRoleResult.Succeeded.ShouldBeFalse(); + duplicateRoleResult.Errors.ShouldContain(e => e.Description == $"角色名 '{duplicateRoleName}' 已存在。"); + } + } + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/AbpIdentityResultException_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/AbpIdentityResultException_Tests.cs deleted file mode 100644 index 4a7860563e..0000000000 --- a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/AbpIdentityResultException_Tests.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Shouldly; -using Volo.Abp.Localization; -using Xunit; - -namespace Volo.Abp.Identity; - -public class AbpIdentityResultException_Tests : AbpIdentityDomainTestBase -{ - [Fact] - public void Should_Localize_Messages() - { - var exception = new AbpIdentityResultException( - IdentityResult.Failed( - new IdentityError - { - Code = "PasswordTooShort", - Description = "Passwords must be at least 6 characters." - }, - new IdentityError - { - Code = "PasswordRequiresNonAlphanumeric", - Description = "Passwords must have at least one non alphanumeric character." - }, - new IdentityError - { - Code = "UnknownError", - Description = "Unknown error" - } - ) - ); - - using (CultureHelper.Use("tr")) - { - var localizeMessage = exception.LocalizeMessage(new LocalizationContext(ServiceProvider)); - - localizeMessage.ShouldContain("Şifre uzunluğu 6 karakterden uzun olmalıdır."); - localizeMessage.ShouldContain("Parola en az bir alfasayısal olmayan karakter içermeli"); - localizeMessage.ShouldContain("Bilinmeyen bir hata oluştu."); - } - - using (CultureHelper.Use("en")) - { - var localizeMessage = exception.LocalizeMessage(new LocalizationContext(ServiceProvider)); - - localizeMessage.ShouldContain("Password length must be greater than 6 characters."); - localizeMessage.ShouldContain("Password must contain at least one non-alphanumeric character."); - localizeMessage.ShouldContain("An unknown failure has occurred."); - } - } -} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDynamicInitializer.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDynamicInitializer.cs index 74bdf1821b..ef3cf355bb 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDynamicInitializer.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDynamicInitializer.cs @@ -1,7 +1,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -19,33 +18,19 @@ public class PermissionDynamicInitializer : ITransientDependency public ILogger Logger { get; set; } protected IServiceProvider ServiceProvider { get; } - protected IOptions Options { get; } - [CanBeNull] - protected IHostApplicationLifetime ApplicationLifetime { get; } - protected ICancellationTokenProvider CancellationTokenProvider { get; } - protected IDynamicPermissionDefinitionStore DynamicPermissionDefinitionStore { get; } - protected IStaticPermissionSaver StaticPermissionSaver { get; } - - public PermissionDynamicInitializer( - IServiceProvider serviceProvider, - IOptions options, - ICancellationTokenProvider cancellationTokenProvider, - IDynamicPermissionDefinitionStore dynamicPermissionDefinitionStore, - IStaticPermissionSaver staticPermissionSaver) + + public PermissionDynamicInitializer(IServiceProvider serviceProvider) { Logger = NullLogger.Instance; ServiceProvider = serviceProvider; - Options = options; - ApplicationLifetime = ServiceProvider.GetService(); - CancellationTokenProvider = cancellationTokenProvider; - DynamicPermissionDefinitionStore = dynamicPermissionDefinitionStore; - StaticPermissionSaver = staticPermissionSaver; } public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default) { - var options = Options.Value; + var options = ServiceProvider + .GetRequiredService>() + .Value; if (!options.SaveStaticPermissionsToDatabase && !options.IsDynamicPermissionStoreEnabled) { @@ -54,11 +39,12 @@ public class PermissionDynamicInitializer : ITransientDependency if (runInBackground) { + var applicationLifetime = ServiceProvider.GetService(); Task.Run(async () => { - if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) + if (cancellationToken == default && applicationLifetime?.ApplicationStopping != null) { - cancellationToken = ApplicationLifetime.ApplicationStopping; + cancellationToken = applicationLifetime.ApplicationStopping; } await ExecuteInitializationAsync(options, cancellationToken); }, cancellationToken); @@ -68,20 +54,23 @@ public class PermissionDynamicInitializer : ITransientDependency return ExecuteInitializationAsync(options, cancellationToken); } - protected virtual async Task ExecuteInitializationAsync(PermissionManagementOptions options, CancellationToken cancellationToken) + protected virtual async Task ExecuteInitializationAsync( + PermissionManagementOptions options, + CancellationToken cancellationToken) { try { - using (CancellationTokenProvider.Use(cancellationToken)) + var cancellationTokenProvider = ServiceProvider.GetRequiredService(); + using (cancellationTokenProvider.Use(cancellationToken)) { - if (CancellationTokenProvider.Token.IsCancellationRequested) + if (cancellationTokenProvider.Token.IsCancellationRequested) { return; } await SaveStaticPermissionsToDatabaseAsync(options, cancellationToken); - if (CancellationTokenProvider.Token.IsCancellationRequested) + if (cancellationTokenProvider.Token.IsCancellationRequested) { return; } @@ -104,6 +93,8 @@ public class PermissionDynamicInitializer : ITransientDependency return; } + var staticPermissionSaver = ServiceProvider.GetRequiredService(); + await Policy .Handle() .WaitAndRetryAsync( @@ -118,7 +109,7 @@ public class PermissionDynamicInitializer : ITransientDependency { try { - await StaticPermissionSaver.SaveAsync(); + await staticPermissionSaver.SaveAsync(); } catch (Exception ex) { @@ -129,17 +120,20 @@ public class PermissionDynamicInitializer : ITransientDependency }, cancellationToken); } - protected virtual async Task PreCacheDynamicPermissionsAsync(PermissionManagementOptions options) + protected virtual async Task PreCacheDynamicPermissionsAsync( + PermissionManagementOptions options) { if (!options.IsDynamicPermissionStoreEnabled) { return; } + var dynamicPermissionDefinitionStore = ServiceProvider.GetRequiredService(); + try { // Pre-cache permissions, so first request doesn't wait - await DynamicPermissionDefinitionStore.GetGroupsAsync(); + await dynamicPermissionDefinitionStore.GetGroupsAsync(); } catch (Exception ex) { diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/resource-permission-management-modal.js b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/resource-permission-management-modal.js index 7a6c100c07..971290b941 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/resource-permission-management-modal.js +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/resource-permission-management-modal.js @@ -34,7 +34,7 @@ var abp = abp || {}; }, action: function (data) { volo.abp.permissionManagement.permissions.deleteResource($("#ResourceName").val(), $("#ResourceKey").val(), data.record.providerName, data.record.providerKey).then(function () { - abp.notify.info(l('DeletedSuccessfully')); + abp.notify.success(l('DeletedSuccessfully')); _dataTable.ajax.reloadEx(function (json) { _dataTable.columns.adjust(); }); diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingDynamicInitializer.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingDynamicInitializer.cs index 41db669d9b..af9858353c 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingDynamicInitializer.cs +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingDynamicInitializer.cs @@ -1,7 +1,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -19,33 +18,19 @@ public class SettingDynamicInitializer : ITransientDependency public ILogger Logger { get; set; } protected IServiceProvider ServiceProvider { get; } - protected IOptions Options { get; } - [CanBeNull] - protected IHostApplicationLifetime ApplicationLifetime { get; } - protected ICancellationTokenProvider CancellationTokenProvider { get; } - protected IDynamicSettingDefinitionStore DynamicSettingDefinitionStore { get; } - protected IStaticSettingSaver StaticSettingSaver { get; } - - public SettingDynamicInitializer( - IServiceProvider serviceProvider, - IOptions options, - ICancellationTokenProvider cancellationTokenProvider, - IDynamicSettingDefinitionStore dynamicSettingDefinitionStore, - IStaticSettingSaver staticSettingSaver) + + public SettingDynamicInitializer(IServiceProvider serviceProvider) { Logger = NullLogger.Instance; ServiceProvider = serviceProvider; - Options = options; - ApplicationLifetime = ServiceProvider.GetService(); - CancellationTokenProvider = cancellationTokenProvider; - DynamicSettingDefinitionStore = dynamicSettingDefinitionStore; - StaticSettingSaver = staticSettingSaver; } public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default) { - var options = Options.Value; + var options = ServiceProvider + .GetRequiredService>() + .Value; if (!options.SaveStaticSettingsToDatabase && !options.IsDynamicSettingStoreEnabled) { @@ -54,11 +39,12 @@ public class SettingDynamicInitializer : ITransientDependency if (runInBackground) { + var applicationLifetime = ServiceProvider.GetService(); Task.Run(async () => { - if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) + if (cancellationToken == default && applicationLifetime?.ApplicationStopping != null) { - cancellationToken = ApplicationLifetime.ApplicationStopping; + cancellationToken = applicationLifetime.ApplicationStopping; } await ExecuteInitializationAsync(options, cancellationToken); }, cancellationToken); @@ -69,20 +55,23 @@ public class SettingDynamicInitializer : ITransientDependency return ExecuteInitializationAsync(options, cancellationToken); } - protected virtual async Task ExecuteInitializationAsync(SettingManagementOptions options, CancellationToken cancellationToken) + protected virtual async Task ExecuteInitializationAsync( + SettingManagementOptions options, + CancellationToken cancellationToken) { try { - using (CancellationTokenProvider.Use(cancellationToken)) + var cancellationTokenProvider = ServiceProvider.GetRequiredService(); + using (cancellationTokenProvider.Use(cancellationToken)) { - if (CancellationTokenProvider.Token.IsCancellationRequested) + if (cancellationTokenProvider.Token.IsCancellationRequested) { return; } await SaveStaticSettingsToDatabaseAsync(options, cancellationToken); - if (CancellationTokenProvider.Token.IsCancellationRequested) + if (cancellationTokenProvider.Token.IsCancellationRequested) { return; } @@ -105,6 +94,8 @@ public class SettingDynamicInitializer : ITransientDependency return; } + var staticSettingSaver = ServiceProvider.GetRequiredService(); + await Policy .Handle() .WaitAndRetryAsync( @@ -119,7 +110,7 @@ public class SettingDynamicInitializer : ITransientDependency { try { - await StaticSettingSaver.SaveAsync(); + await staticSettingSaver.SaveAsync(); } catch (Exception ex) { @@ -129,17 +120,20 @@ public class SettingDynamicInitializer : ITransientDependency }, cancellationToken); } - protected virtual async Task PreCacheDynamicSettingsAsync(SettingManagementOptions options) + protected virtual async Task PreCacheDynamicSettingsAsync( + SettingManagementOptions options) { if (!options.IsDynamicSettingStoreEnabled) { return; } + var dynamicSettingDefinitionStore = ServiceProvider.GetRequiredService(); + try { // Pre-cache settings, so first request doesn't wait - await DynamicSettingDefinitionStore.GetAllAsync(); + await dynamicSettingDefinitionStore.GetAllAsync(); } catch (Exception ex) { diff --git a/npm/ng-packs/packages/account/src/lib/account.routes.ts b/npm/ng-packs/packages/account/src/lib/account.routes.ts index 5fb4236ee1..3c674162aa 100644 --- a/npm/ng-packs/packages/account/src/lib/account.routes.ts +++ b/npm/ng-packs/packages/account/src/lib/account.routes.ts @@ -1,4 +1,5 @@ import { Routes } from '@angular/router'; +import { Provider } from '@angular/core'; import { ForgotPasswordComponent, LoginComponent, @@ -23,7 +24,7 @@ import { RouterOutletComponent, } from '@abp/ng.core'; -export function provideAccount(options: AccountConfigOptions = {}) { +export function provideAccount(options: AccountConfigOptions = {}): Provider[] { return [ { provide: ACCOUNT_CONFIG_OPTIONS, useValue: options }, { diff --git a/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts b/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts index 3b77913d2e..41e07f8d1b 100644 --- a/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts +++ b/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts @@ -8,10 +8,10 @@ import { } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { Observable, of } from 'rxjs'; -import { map, take } from 'rxjs/operators'; +import { filter, map, switchMap, take } from 'rxjs/operators'; import { AuthService, IAbpGuard } from '../abstracts'; import { findRoute, getRoutePath } from '../utils/route-utils'; -import { RoutesService, PermissionService, HttpErrorReporterService } from '../services'; +import { RoutesService, PermissionService, HttpErrorReporterService, ConfigStateService } from '../services'; import { isPlatformServer } from '@angular/common'; /** * @deprecated Use `permissionGuard` *function* instead. @@ -25,6 +25,7 @@ export class PermissionGuard implements IAbpGuard { protected readonly authService = inject(AuthService); protected readonly permissionService = inject(PermissionService); protected readonly httpErrorReporter = inject(HttpErrorReporterService); + protected readonly configStateService = inject(ConfigStateService); canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { let { requiredPolicy } = route.data || {}; @@ -38,7 +39,10 @@ export class PermissionGuard implements IAbpGuard { return of(true); } - return this.permissionService.getGrantedPolicy$(requiredPolicy).pipe( + return this.configStateService.getAll$().pipe( + filter(config => !!config?.auth?.grantedPolicies), + take(1), + switchMap(() => this.permissionService.getGrantedPolicy$(requiredPolicy)), take(1), map(access => { if (access) return true; @@ -50,7 +54,6 @@ export class PermissionGuard implements IAbpGuard { if (this.authService.isAuthenticated) { this.httpErrorReporter.reportError({ status: 403 } as HttpErrorResponse); } - return false; }), ); @@ -66,6 +69,7 @@ export const permissionGuard: CanActivateFn = ( const authService = inject(AuthService); const permissionService = inject(PermissionService); const httpErrorReporter = inject(HttpErrorReporterService); + const configStateService = inject(ConfigStateService); const platformId = inject(PLATFORM_ID); let { requiredPolicy } = route.data || {}; @@ -84,7 +88,10 @@ export const permissionGuard: CanActivateFn = ( return of(true); } - return permissionService.getGrantedPolicy$(requiredPolicy).pipe( + return configStateService.getAll$().pipe( + filter(config => !!config?.auth?.grantedPolicies), + take(1), + switchMap(() => permissionService.getGrantedPolicy$(requiredPolicy)), take(1), map(access => { if (access) return true; diff --git a/npm/ng-packs/packages/core/src/lib/services/config-state.service.ts b/npm/ng-packs/packages/core/src/lib/services/config-state.service.ts index 1243e8b2ba..a0d2c561e9 100644 --- a/npm/ng-packs/packages/core/src/lib/services/config-state.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/config-state.service.ts @@ -10,6 +10,7 @@ import { } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/models'; import { INCUDE_LOCALIZATION_RESOURCES_TOKEN } from '../tokens/include-localization-resources.token'; import { InternalStore } from '../utils/internal-store-utils'; +import { EnvironmentService } from './environment.service'; @Injectable({ providedIn: 'root', @@ -17,6 +18,7 @@ import { InternalStore } from '../utils/internal-store-utils'; export class ConfigStateService { private abpConfigService = inject(AbpApplicationConfigurationService); private abpApplicationLocalizationService = inject(AbpApplicationLocalizationService); + private environmentService = inject(EnvironmentService); private readonly includeLocalizationResources = inject(INCUDE_LOCALIZATION_RESOURCES_TOKEN, { optional: true }); private updateSubject = new Subject(); @@ -59,7 +61,18 @@ export class ConfigStateService { this.uiCultureFromAuthCodeFlow ?? appState.localization.currentCulture.cultureName; return this.getlocalizationResource(cultureName).pipe( - map(result => ({ ...appState, localization: { ...appState.localization, ...result } })), + map(result => { + const envLocalization = this.environmentService.getEnvironment()?.localization; + return { + ...appState, + localization: { + ...appState.localization, + ...result, + defaultResourceName: + appState.localization?.defaultResourceName ?? envLocalization?.defaultResourceName, + }, + }; + }), tap(() => (this.uiCultureFromAuthCodeFlow = undefined)), ); } @@ -83,9 +96,18 @@ export class ConfigStateService { return this.getlocalizationResource(lang) .pipe( - tap(result => - this.store.patch({ localization: { ...this.store.state.localization, ...result } }), - ), + tap(result => { + const envLocalization = this.environmentService.getEnvironment()?.localization; + this.store.patch({ + localization: { + ...this.store.state.localization, + ...result, + defaultResourceName: + this.store.state.localization?.defaultResourceName ?? + envLocalization?.defaultResourceName, + }, + }); + }), ) .pipe(map(() => null)); } diff --git a/npm/ng-packs/packages/schematics/src/utils/methods.ts b/npm/ng-packs/packages/schematics/src/utils/methods.ts index d3fc935819..6d31903a78 100644 --- a/npm/ng-packs/packages/schematics/src/utils/methods.ts +++ b/npm/ng-packs/packages/schematics/src/utils/methods.ts @@ -19,4 +19,9 @@ export const getParamValueName = (paramName: string, descriptorName: string) => export function isDictionaryType(type?: string, typeSimple?: string): boolean { const haystacks = [type || '', typeSimple || '']; return haystacks.some(t => /(^|\b)(System\.Collections\.Generic\.)?(I)?Dictionary\s* /(^|\b)(System\.Collections\.Generic\.)?(I)?(List|Enumerable|Collection)\s* `${x}[]`).some(x => x === type); + // Check for array types like Volo.Abp.Content.IRemoteStreamContent[] + if (VOLO_REMOTE_STREAM_CONTENT.map(x => `${x}[]`).some(x => x === type)) { + return true; + } + + // Check for collection types like List, IEnumerable, ICollection, Collection, IList + // This matches any generic type from System.Collections.Generic that implements IEnumerable + if (isCollectionType(type)) { + const { generics } = extractGenerics(type); + if (generics.length > 0 && VOLO_REMOTE_STREAM_CONTENT.includes(generics[0])) { + return true; + } + } + + return false; } function getMethodNameFromAction(action: Action): string { diff --git a/npm/ng-packs/packages/tenant-management/src/lib/tenant-management.routes.ts b/npm/ng-packs/packages/tenant-management/src/lib/tenant-management.routes.ts index 7b38035d2f..236613bbdd 100644 --- a/npm/ng-packs/packages/tenant-management/src/lib/tenant-management.routes.ts +++ b/npm/ng-packs/packages/tenant-management/src/lib/tenant-management.routes.ts @@ -1,4 +1,5 @@ import { Routes } from '@angular/router'; +import { Provider } from '@angular/core'; import { TenantManagementConfigOptions } from './models/config-options'; import { TENANT_MANAGEMENT_CREATE_FORM_PROP_CONTRIBUTORS, @@ -19,7 +20,7 @@ import { TenantsComponent } from './components'; import { eTenantManagementComponents } from './enums'; import { tenantManagementExtensionsResolver } from './resolvers'; -export function provideTenantManagement(options: TenantManagementConfigOptions = {}) { +export function provideTenantManagement(options: TenantManagementConfigOptions = {}): Provider[] { return [ { provide: TENANT_MANAGEMENT_ENTITY_ACTION_CONTRIBUTORS, diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html index afe6f11f48..a29fb3323e 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html @@ -10,7 +10,7 @@ @if (route.iconClass) { } - {{ route.name | abpLazyLocalization | async }} + {{ route.name | abpAsyncLocalization | async }} @@ -20,14 +20,13 @@