@ -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<TKey>` 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<TEntity, TKey>` (or `IReadOnlyRepository<...>` when suitable). |
|||
- SHOULD NOT expose `IQueryable` in the public contract. |
|||
- Internal implementations MAY use `IRepository<TEntity, TKey>` 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<TKey>`, 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<TEntity>` ONLY for aggregate roots. |
|||
- Do NOT include setters in the interface. |
|||
- DbContext class: |
|||
- Inherit `AbpDbContext<TDbContext>`. |
|||
- 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<TDbContextInterface, TEntity, TKey>`. |
|||
- 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<TEntity>` 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<TMongoDbContextInterface, TEntity, TKey>`. |
|||
- 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<TResource>("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<TModule>` or `AbpApplicationTestBase<TModule>` 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. |
|||
@ -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<TKey>` 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<TEntity, TKey>` (or `IReadOnlyRepository<...>` when suitable). |
|||
- SHOULD NOT expose `IQueryable` in the public contract. |
|||
- Internal implementations MAY use `IRepository<TEntity, TKey>` 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<ProductDto> GetAsync(Guid id); |
|||
Task<PagedResultDto<ProductDto>> GetListAsync(GetProductListInput input); |
|||
Task<ProductDto> CreateAsync(CreateProductInput input); |
|||
Task<ProductDto> 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<TKey>`, 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<Product> Products { get; } // No setters, aggregate roots only |
|||
} |
|||
``` |
|||
|
|||
### DbContext Class |
|||
```csharp |
|||
[ConnectionStringName("ModuleName")] |
|||
public class ModuleNameDbContext : AbpDbContext<ModuleNameDbContext>, IModuleNameDbContext |
|||
{ |
|||
public static string TablePrefix { get; set; } = ModuleNameConsts.DefaultDbTablePrefix; |
|||
public static string? Schema { get; set; } = ModuleNameConsts.DefaultDbSchema; |
|||
|
|||
public DbSet<Product> 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<TDbContextInterface, TEntity, TKey>`. |
|||
- 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<Product> 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<TMongoDbContextInterface, TEntity, TKey>`. |
|||
- 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<TResource>("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<TModule>` or `AbpApplicationTestBase<TModule>` 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. |
|||
@ -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: |
|||
|
|||
 |
|||
|
|||
## 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 |
|||
|
|||
 |
|||
|
|||
#### 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 |
|||
<ResourcePermissionManagementModal @ref="PermissionManagementModal" /> |
|||
|
|||
@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. |
|||
|
|||
 |
|||
|
|||
**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<FileManagementWebOptions>(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. |
|||
|
|||
 |
|||
|
|||
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: |
|||
|
|||
 |
|||
|
|||
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: |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
### 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:** |
|||
|
|||
 |
|||
|
|||
**2. Add your passkeys in the _Account/Manage_ page:** |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
**3. Use the _Passkey login_ option for passwordless authentication the next time you log in:** |
|||
|
|||
 |
|||
|
|||
> 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! |
|||
|
|||
 |
|||
|
|||
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 |
|||
|
|||
 |
|||
|
|||
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! |
|||
|
|||
 |
|||
|
|||
**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! |
|||
|
|||
 |
|||
|
|||
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 |
|||
|
|||
 |
|||
|
|||
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! |
|||
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 665 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 22 KiB |
@ -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 |
|||
|
|||
 |
|||
|
|||
**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! |
|||
|
|||
 |
|||
|
|||
--- |
|||
|
|||
## 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)** |
|||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 13 KiB |
@ -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\! |
|||
@ -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) |
|||
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 |
|||
|
|||
@ -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<FaqOptions>(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. |
|||
|
After Width: | Height: | Size: 18 KiB |
@ -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) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The order -999 ensures that this provider is executed right after the
|
|||
/// Microsoft.AspNetCore.Mvc.ApiExplorer.DefaultApiDescriptionProvider.
|
|||
/// </summary>
|
|||
public int Order => -999; |
|||
|
|||
public virtual void OnProvidersExecuting(ApiDescriptionProviderContext context) |
|||
{ |
|||
foreach (var result in context.Results.Where(x => x.IsRemoteService())) |
|||
{ |
|||
var actionProducesResponseTypeAttributes = |
|||
ReflectionHelper.GetAttributesOfMemberOrDeclaringType<ProducesResponseTypeAttribute>( |
|||
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 |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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<AbpTimingTestModule> |
|||
{ |
|||
[Fact] |
|||
public void GetTimezones_Should_Filter_Invalid_Timezones() |
|||
{ |
|||
var validTimeZoneId = "UTC"; |
|||
var invalidTimeZoneId = "Invalid/Zone"; |
|||
|
|||
var timezones = new List<NameValue> |
|||
{ |
|||
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(); |
|||
} |
|||
} |
|||
@ -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<IdentityErrorDescriber>(); |
|||
|
|||
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<IUnitOfWorkManager>().Begin()) |
|||
{ |
|||
var user = new IdentityUser(Guid.NewGuid(), "um_localize_user", "um_localize_user@abp.io"); |
|||
|
|||
using (CultureHelper.Use("en")) |
|||
{ |
|||
var userManager = GetRequiredService<IdentityUserManager>(); |
|||
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<IdentityRoleManager>(); |
|||
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<IOptions<IdentityOptions>>().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<IdentityUserManager>(); |
|||
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<IdentityRoleManager>(); |
|||
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<IOptions<IdentityOptions>>().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<IdentityUserManager>(); |
|||
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<IdentityRoleManager>(); |
|||
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<IOptions<IdentityOptions>>().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}' 已存在。"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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."); |
|||
} |
|||
} |
|||
} |
|||