Browse Source

Merge branch 'dev' into auto-merge/rel-10-1/4304

pull/24710/head
Ma Liming 8 months ago
committed by GitHub
parent
commit
6d39809257
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 270
      .cursorrules
  2. 372
      .github/copilot-instructions.md
  3. 2
      Directory.Packages.props
  4. 1
      abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
  5. 4
      common.props
  6. 342
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/POST.md
  7. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/ai-management-demo.gif
  8. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/ai-management-workspaces.png
  9. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/community-talk-2025-10-ai.png
  10. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/cover-image.png
  11. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/dotnet-conf-china-2025.png
  12. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/file-sharing.gif
  13. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/live-training-discount.png
  14. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/my-passkey.png
  15. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-login.png
  16. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-registration.png
  17. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-setting.png
  18. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/password-history-settings.png
  19. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/password-history-warning.png
  20. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/referral-program.png
  21. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/reset-password-error-modal.png
  22. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/set-password-error-modal.png
  23. BIN
      docs/en/Blog-Posts/2026-01-08 v10_1_Preview/studio-switch-to-preview.png
  24. 2
      docs/en/Community-Articles/2025-09-02-training-campaign/post.md
  25. 4
      docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/post.md
  26. 170
      docs/en/Community-Articles/2026-01-11/article.md
  27. BIN
      docs/en/Community-Articles/2026-01-11/event-driven-systems.png
  28. BIN
      docs/en/Community-Articles/2026-01-11/message-driven-systems.png
  29. 17
      docs/en/Community-Articles/2026-01-16-meet-abio-at-ndc-london/post.md
  30. BIN
      docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/PuppeteerSharp.png
  31. BIN
      docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/QuestPDF.png
  32. 153
      docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/article.md
  33. BIN
      docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/cover.png
  34. BIN
      docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/itext.jpg
  35. BIN
      docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/pdfsharp.png
  36. BIN
      docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/playwright.png
  37. 1
      docs/en/framework/ui/angular/data-table-column-extensions.md
  38. 189
      docs/en/framework/ui/angular/extensible-table-row-detail.md
  39. BIN
      docs/en/framework/ui/angular/images/row-detail-image.png
  40. 2
      docs/en/framework/ui/angular/localization.md
  41. 4
      docs/en/modules/audit-logging-pro.md
  42. BIN
      docs/en/studio/images/overview/ai-assistant.png
  43. 21
      docs/en/studio/overview.md
  44. 76
      framework/src/Volo.Abp.AspNetCore.Components.Server/Microsoft/AspNetCore/Authentication/Cookies/CookieAuthenticationOptionsExtensions.cs
  45. 31
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs
  46. 6
      framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs
  47. 38
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs
  48. 2
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinderProvider.cs
  49. 53
      framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs
  50. 1
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs
  51. 2
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/ClientPermissionValueProvider.cs
  52. 55
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ClientResourcePermissionValueProvider.cs
  53. 8
      framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQOptions.cs
  54. 11
      framework/src/Volo.Abp.BlazoriseUI/BlazoriseUiObjectExtensionPropertyInfoExtensions.cs
  55. 7
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs
  56. 4
      framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLoggerFactory.cs
  57. 9
      framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
  58. 98
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs
  59. 33
      framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/PostConfigureAbpRabbitMqEventBusOptions.cs
  60. 6
      framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs
  61. 49
      framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs
  62. 69
      framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverter.cs
  63. 116
      framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverterBase.cs
  64. 67
      framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpNullableDateTimeConverter.cs
  65. 2
      framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceDictionary.cs
  66. 2
      framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDatabaseCollection.cs
  67. 2
      framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/MongoModelBuilder.cs
  68. 2
      framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ParameterRebinder.cs
  69. 39
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController.cs
  70. 104
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs
  71. 2
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs
  72. 27
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithJsonProperty.cs
  73. 21
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs
  74. 98
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs
  75. 165
      framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs
  76. 9
      latest-versions.json
  77. 8
      modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json
  78. 5
      modules/docs/app/VoloDocs.Web/Pages/Error.cshtml.cs
  79. 24
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/UserRoleFinder.cs
  80. 6
      modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/RoleResourcePermissionProviderKeyLookupService.cs
  81. 10
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientFinderResult.cs
  82. 9
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/IClientFinder.cs
  83. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/FR.json
  84. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ar.json
  85. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/cs.json
  86. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/de.json
  87. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/el.json
  88. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en-GB.json
  89. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en.json
  90. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/es.json
  91. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fa.json
  92. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fi.json
  93. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hi.json
  94. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hr.json
  95. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hu.json
  96. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/is.json
  97. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/it.json
  98. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/nl.json
  99. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pl-PL.json
  100. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pt-BR.json

270
.cursorrules

@ -0,0 +1,270 @@
# ABP Framework – Cursor Rules
# Scope: ABP Framework repository (abpframework/abp) — for developing ABP itself, not ABP-based applications.
# Goal: Enforce ABP module architecture best practices (DDD, layering, DB/ORM independence),
# maintain backward compatibility, ensure extensibility, and align with ABP contribution guidelines.
## Global Defaults
- Follow existing patterns in this repository first. Before generating new code, search for similar implementations and mirror their structure, naming, and conventions.
- Prefer minimal, focused diffs. Avoid drive-by refactors and formatting churn.
- Preserve public APIs. Avoid breaking changes unless explicitly requested and justified.
- Keep layers clean. Do not introduce forbidden dependencies between packages.
## Module / Package Architecture (Layering)
- Use a layered module structure with explicit dependencies:
- *.Domain.Shared: constants, enums, shared types safe for all layers and 3rd-party clients. MUST NOT contain entities, repositories, domain services, or business objects.
- *.Domain: entities/aggregate roots, repository interfaces, domain services.
- *.Application.Contracts: application service interfaces and DTOs.
- *.Application: application service implementations.
- *.EntityFrameworkCore / *.MongoDb: ORM integration packages depend on *.Domain only. MUST NOT depend on other layers.
- *.HttpApi: REST controllers. MUST depend ONLY on *.Application.Contracts (NOT *.Application).
- *.HttpApi.Client: remote client proxies. MUST depend ONLY on *.Application.Contracts.
- *.Web: UI. MUST depend ONLY on *.HttpApi.
- Enforce dependency direction:
- Web -> HttpApi -> Application.Contracts
- Application -> Domain + Application.Contracts
- Domain -> Domain.Shared
- ORM integration -> Domain
- Do not leak web concerns into application/domain.
## Domain Layer – Entities & Aggregate Roots
- Define entities in the domain layer.
- Entities must be valid at creation:
- Provide a primary constructor that enforces invariants.
- Always include a protected parameterless constructor for ORMs.
- Always initialize sub-collections in the primary constructor.
- Do NOT generate Guid keys inside constructors; accept `id` and generate using `IGuidGenerator` from the calling code.
- Make members `virtual` where appropriate (ORM/proxy compatibility).
- Protect consistency:
- Use non-public setters (private/protected/internal) when needed.
- Provide meaningful domain methods for state transitions; prefer returning `this` from setters when applicable.
- Aggregate roots:
- Always use a single `Id` property. Do NOT use composite keys.
- Prefer `Guid` keys for aggregate roots.
- Inherit from `AggregateRoot<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.

372
.github/copilot-instructions.md

@ -0,0 +1,372 @@
# ABP Framework – GitHub Copilot Instructions
> **Scope**: ABP Framework repository (abpframework/abp) — for developing ABP itself, not ABP-based applications.
>
> **Goal**: Enforce ABP module architecture best practices (DDD, layering, DB/ORM independence), maintain backward compatibility, ensure extensibility, and align with ABP contribution guidelines.
---
## Global Defaults
- Follow existing patterns in this repository first. Before generating new code, search for similar implementations and mirror their structure, naming, and conventions.
- Prefer minimal, focused diffs. Avoid drive-by refactors and formatting churn.
- Preserve public APIs. Avoid breaking changes unless explicitly requested and justified.
- Keep layers clean. Do not introduce forbidden dependencies between packages.
---
## Module / Package Architecture (Layering)
Use a layered module structure with explicit dependencies:
| Layer | Purpose | Allowed Dependencies |
|-------|---------|---------------------|
| `*.Domain.Shared` | Constants, enums, shared types safe for all layers and 3rd-party clients. MUST NOT contain entities, repositories, domain services, or business objects. | None |
| `*.Domain` | Entities/aggregate roots, repository interfaces, domain services. | Domain.Shared |
| `*.Application.Contracts` | Application service interfaces and DTOs. | Domain.Shared |
| `*.Application` | Application service implementations. | Domain, Application.Contracts |
| `*.EntityFrameworkCore` / `*.MongoDb` | ORM integration packages. MUST NOT depend on other layers. | Domain only |
| `*.HttpApi` | REST controllers. MUST depend ONLY on Application.Contracts (NOT Application). | Application.Contracts |
| `*.HttpApi.Client` | Remote client proxies. MUST depend ONLY on Application.Contracts. | Application.Contracts |
| `*.Web` | UI layer. MUST depend ONLY on HttpApi. | HttpApi |
### Dependency Direction
```
Web -> HttpApi -> Application.Contracts
Application -> Domain + Application.Contracts
Domain -> Domain.Shared
ORM integration -> Domain
```
Do not leak web concerns into application/domain.
---
## Domain Layer – Entities & Aggregate Roots
- Define entities in the domain layer.
- Entities must be valid at creation:
- Provide a primary constructor that enforces invariants.
- Always include a `protected` parameterless constructor for ORMs.
- Always initialize sub-collections in the primary constructor.
- Do NOT generate Guid keys inside constructors; accept `id` and generate using `IGuidGenerator` from the calling code.
- Make members `virtual` where appropriate (ORM/proxy compatibility).
- Protect consistency:
- Use non-public setters (`private`/`protected`/`internal`) when needed.
- Provide meaningful domain methods for state transitions.
### Aggregate Roots
- Always use a single `Id` property. Do NOT use composite keys.
- Prefer `Guid` keys for aggregate roots.
- Inherit from `AggregateRoot<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.

2
Directory.Packages.props

@ -122,7 +122,7 @@
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.14.0" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.14.0" />
<PackageVersion Include="Minio" Version="6.0.5" />
<PackageVersion Include="MongoDB.Driver" Version="3.5.2" />
<PackageVersion Include="MongoDB.Driver" Version="3.6.0" />
<PackageVersion Include="NEST" Version="7.17.5" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="Nito.AsyncEx.Context" Version="5.1.2" />

1
abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json

@ -1229,6 +1229,7 @@
"Pricing_Page_HurryUp": "Hurry Up!",
"Pricing_Page_BuyLicense": "Buy a license at <strong>2021 prices</strong> until January 16!",
"Pricing_Page_ValidForExistingCustomers": "Also valid for existing customers and license renewals.",
"Pricing_Page_AdditionalDevCost": "The cost of an additional developer seat for the {0} License is {1}.",
"Pricing_Page_Hint1": "The license price includes a certain number of developer seats. If you have more developers, you can always purchase additional seats.",
"Pricing_Page_Hint2": "You can purchase more developer licenses now or in the future. Licenses are seat-based, so you can transfer a seat from one developer to another.",
"Pricing_Page_Hint3": "You can develop an unlimited count of different products with your license.",

4
common.props

@ -1,8 +1,8 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>10.1.0-rc.2</Version>
<LeptonXVersion>5.1.0-rc.2</LeptonXVersion>
<Version>10.2.0-preview</Version>
<LeptonXVersion>5.2.0-preview</LeptonXVersion>
<NoWarn>$(NoWarn);CS1591;CS0436</NoWarn>
<PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl>
<PackageProjectUrl>https://abp.io/</PackageProjectUrl>

342
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/POST.md

@ -0,0 +1,342 @@
# ABP Platform 10.1 RC Has Been Released
We are happy to release [ABP](https://abp.io) version **10.1 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version.
Try this version and provide feedback for a more stable version of ABP v10.1! Thanks to you in advance.
## Get Started with the 10.1 RC
You can check the [Get Started page](https://abp.io/get-started) to see how to get started with ABP. You can either download [ABP Studio](https://abp.io/get-started#abp-studio-tab) (**recommended**, if you prefer a user-friendly GUI application - desktop application) or use the [ABP CLI](https://abp.io/docs/latest/cli).
By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI:
![studio-switch-to-preview.png](studio-switch-to-preview.png)
## Migration Guide
There are a few breaking changes in this version that may affect your application. Please read the migration guide carefully, if you are upgrading from v10.0 or earlier: [ABP Version 10.1 Migration Guide](https://abp.io/docs/10.1/release-info/migration-guides/abp-10-1).
## What's New with ABP v10.1?
In this section, I will introduce some major features released in this version.
Here is a brief list of titles explained in the next sections:
- Resource-Based Authorization
- Introducing the TickerQ Background Worker Provider
- Angular UI: Improving Authentication Token Handling
- Angular Version Upgrade to v21
- File Management Module: Public File Sharing Support
- Payment Module: Public Page Implementation for Blazor & Angular UIs
- AI Management Module: Blazor & Angular UIs
- Identity PRO Module: Password History Support
- Account PRO Module: Introducing WebAuthn Passkeys
### Resource-Based Authorization
ABP v10.1 introduces **Resource-Based Authorization**, a powerful feature that enables fine-grained access control based on specific resource instances. This enhancement addresses a long-requested feature ([#236](https://github.com/abpframework/abp/issues/236)) that allows you to implement authorization logic that depends on the resource being accessed, not just static roles or permissions.
**What is Resource-Based Authorization?**
Unlike traditional permission-based authorization where you check if a user has a general permission (like "CanEditDocuments"), resource-based authorization allows you to make authorization decisions based on the specific resource instance. For example:
- Allow users to edit only their own blog posts
- Grant access to documents based on ownership or sharing settings
- Implement complex authorization rules that depend on resource properties
![](ai-management-demo.gif)
#### How It Works?
**1. Define resource permissions (`AddResourcePermission`)**:
```csharp
public class MyPermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
//other permissions...
context.AddResourcePermission(
name: BookManagementPermissions.Manage.Resources.Consume,
resourceName: BookManagementPermissions.Manage.Resources.Name,
managementPermissionName: BookManagementPermissions.Manage.ManagePermissions,
L("LocalizedPermissionDisplayName")
);
}
}
```
**2. Use `IResourcePermissionChecker.IsGrantedAsync` in your code to perform the resource permission check**:
```csharp
protected IResourcePermissionChecker ResourcePermissionChecker { get; }
public async Task MyService()
{
if(await ResourcePermissionChecker.IsGrantedAsync(
BookManagementPermissions.Manage.Resources.Consume,
BookManagementPermissions.Manage.Resources.Name,
workspaceConfiguration.WorkspaceId!.Value.ToString()))
{
return;
}
//...
}
```
**3. Use the relevant `ResourcePermissionManagementModel` in your UI:**
> The following code block demonstrates its usage in the Blazor UI, but the same component is also implemented for MVC & Angular UIs (however, component name might be different, please refer to the documentation before using the component).
```xml
<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.
![](file-sharing.gif)
**Example Share URL:**
```text
https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8AK%2BOEpCD...
```
**Configuration:**
You can configure the public share domain through options:
```csharp
Configure<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.
![AI Management Workspaces](ai-management-workspaces.png)
The AI Management Module builds on top of [ABP's AI Infrastructure](https://abp.io/docs/latest/framework/infrastructure/artificial-intelligence) and provides:
- **Multi-Provider Support**: Integrate with OpenAI, Google Gemini, Anthropic Claude, and more from a unified API
- **Workspace-Based Organization**: Organize AI capabilities into separate workspaces for different use cases
- **Built-In Chat Interface**: Ready-to-use chat UI for conversational AI
- **Chat Widget**: Drop-in chat widget component for customer support or AI assistance
- **Resource-Based Permissions**: Control access to specific AI workspaces for users, roles, or clients
Learn more about the AI Management Module in the [announcement post](https://abp.io/community/announcements/introducing-the-ai-management-module-nz9404a9) and [official documentation](https://abp.io/docs/latest/modules/ai-management).
### Identity PRO Module: Password History Support
The [**Identity PRO Module**](https://abp.io/docs/latest/modules/identity-pro) now includes **Password History** support, preventing users from reusing previous passwords. This security feature helps enforce stronger password policies and meet compliance requirements for your organization.
Administrators can enable password reuse prevention by toggling the related setting on the _Administration -> Settings -> Identity Management_ page:
![Password History Settings](password-history-settings.png)
When changing a password, the system checks the specified number of previous passwords and displays an error message if the new password matches any of them:
![](set-password-error-modal.png)
![](reset-password-error-modal.png)
### Account PRO Module: Introducing WebAuthn Passkeys
ABP v10.1 introduces **Passkey authentication**, enabling passwordless sign-in using modern biometric authentication methods. Built on the **WebAuthn standard (FIDO2)**, this feature allows users to authenticate using Face ID, Touch ID, Windows Hello, Android biometrics, security keys, or other platform authenticators.
**What are Passkeys?**
Passkeys are a modern, phishing-resistant authentication method that replaces traditional passwords:
- **Passwordless**: No passwords to remember, type, or manage
- **Secure**: Uses public/private key cryptography stored on the user's device
- **Convenient**: Sign in with a fingerprint, face scan, or device PIN
- **Cross-Platform**: Can sync across devices depending on platform support (Apple, Google, Microsoft)
**How It Works:**
**1. Enable or disable the WebAuthn passkeys feature in the _Settings -> Account -> Passkeys_ page:**
![Passkey Setting](passkey-setting.png)
**2. Add your passkeys in the _Account/Manage_ page:**
![My Passkeys](my-passkey.png)
![Passkey registration](passkey-registration.png)
**3. Use the _Passkey login_ option for passwordless authentication the next time you log in:**
![Passkey Login](passkey-login.png)
> For more information, refer to the [Web Authentication API (WebAuthn) passkeys](https://abp.io/docs/10.1/modules/account/passkey) documentation.
## Community News
### Special Offer: Level Up Your ABP Skills with 33% Off Live Trainings!
![ABP Live Training Discount](./live-training-discount.png)
We're excited to announce a special limited-time offer for developers looking to master the ABP Platform! Get **33% OFF** on all ABP live training sessions and accelerate your learning journey with hands-on guidance from ABP experts.
**Why Join ABP Live Trainings?**
Our live training sessions provide an immersive learning experience where you can:
- **Learn from the Experts**: Get direct instruction from ABP team members and experienced trainers who know the platform inside and out.
- **Hands-On Practice**: Work through real-world scenarios and build actual applications during the sessions.
- **Interactive Q&A**: Ask questions in real-time and get immediate answers to your specific challenges.
- **Comprehensive Coverage**: From fundamentals to advanced topics, our trainings cover everything you need to build production-ready applications with ABP.
- **Certificate of Completion**: Receive a certificate upon completing the training to showcase your ABP expertise.
Don't miss this opportunity to invest in your skills and career. Whether you're new to ABP or looking to advance your expertise, our live trainings provide the structured learning path you need to succeed.
> 👉 [Learn more and claim your discount here](https://abp.io/community/announcements/improve-your-abp-skills-with-33-off-live-trainings-hjnw57xu)
### Introducing the ABP Referral Program
![ABP.IO Referral Program](./referral-program.png)
We're thrilled to announce the launch of the **ABP.IO Referral Program**, a new way for our community members to earn rewards while helping others discover the ABP Platform!
**How It Works:**
ABP's Referral Program is simple and rewarding:
1. **Get Your Unique Referral Link**: Sign up for the program and receive your personalized referral link.
2. **Share with Your Network**: Share your link with colleagues, friends, and fellow developers who could benefit from ABP.
3. **Earn Rewards**: When someone purchases an ABP Commercial license through your referral link, **you earn 5% commission**!
By joining the referral program, you're not just earning rewards and also you're helping other developers discover a platform that can significantly improve their productivity and project success.
> 👉 [Join the ABP.IO Referral Program](https://abp.io/community/announcements/introducing-abp.io-referral-program-b59obhe7)
### Announcing AI Management Module
We are excited to announce the [AI Management Module](https://abp.io/docs/10.0/modules/ai-management), a powerful new module to the ABP Platform that makes managing AI capabilities in your applications easier than ever!
![ABP - AI Management Module Workspaces](ai-management-workspaces.png)
**What is the AI Management Module?**
Built on top of the [ABP Framework's AI infrastructure](https://abp.io/docs/latest/framework/infrastructure/artificial-intelligence), the **AI Management Module** allows you to manage AI workspaces dynamically without touching your code. Whether you're building a customer support chatbot, adding AI-powered search, or creating intelligent automation workflows, this module provides everything you need to manage AI integrations through a user-friendly interface.
**Key Features:**
- **Multi-Provider Support**: Allows integrating with multiple AI providers including OpenAI, Google Gemini, Anthropic Claude, and more from a single unified API.
- **Buit-In Chat Interface**
- **Ready to Use Chat Widget**
- and more... (RAG & MCP supports are on the way!)
👉 [Read the announcement post for more...](https://abp.io/community/announcements/introducing-the-ai-management-module-nz9404a9)
### We Were At .NET Conf China 2025!
![.NET Conf China 2025](./dotnet-conf-china-2025.png)
The ABP team participated in **.NET Conf China 2025** in Shanghai, celebrating the release of .NET 10 (LTS) and the achievements of the .NET community in China.
**Event Highlights:**
The conference brought together hundereds of developers and featured Scott Hanselman's opening keynote announcing .NET 10's availability, focused on four pillars: AI, cloud-native, cross-platform, and performance. The event covered three main themes: performance improvements, AI integration, and cross-platform development, with in-depth sessions on topics ranging from Avalonia and Blazor to AI agents and enterprise adoption.
**ABP's Participation:**
At the ABP booth, we showcased our developer platform with live demonstrations of modular architecture, multi-tenancy support, and built-in authentication systems. We hosted interactive raffles with prizes including ABP stickers, the _Mastering ABP Framework_ book, and Bluetooth headphones. The booth was a hub for sharing experiences, impromptu code walkthroughs, and meaningful conversations with Chinese developers about ABP's future.
> 👉 [Read the full event recap](https://abp.io/community/announcements/.net-conf-china-2025-fz03gfge)
### Community Talks 2025.10: AI-Powered .NET Apps with ABP & Microsoft Agent Framework
![ABP Community Talks - AI-Powered .NET Apps](./community-talk-2025-10-ai.png)
In our latest ABP Community Talks session, we dove deep into the world of **Artificial Intelligence** and its integration with the ABP Framework. This session explored Microsoft's cutting-edge AI libraries: **Extensions AI**, **Semantic Kernel**, and the **Microsoft Agent Framework**.
**What We Covered:**
We introduced the new **AI Management Module**, discussing its current status and roadmap. The session included practical demonstrations on building intelligent applications with the Microsoft Agent Framework within ABP projects, showing how these technologies empower developers to create AI-powered .NET applications.
> 👉 [Missed the live session? Click here to watch the full session](https://www.youtube.com/live/tEcd2H6yXQk)
### New ABP Community Articles
There are exciting articles contributed by the ABP community as always. I will highlight some of them here:
- [Salih Özkara](https://github.com/salihozkara) has published 3 new articles:
- [Building Dynamic XML Sitemaps with ABP Framework](https://abp.io/community/articles/building-dynamic-xml-sitemaps-with-abp-framework-n3q6schd)
- [Implement Automatic Method-Level Caching in ABP Framework](https://abp.io/community/articles/implement-automatic-methodlevel-caching-in-abp-framework-4uzd3wx8)
- [Building Production-Ready LLM Applications with .NET: A Practical Guide](https://abp.io/community/articles/building-production-ready-llm-applications-with-net-ya7qemfa)
- [Adnan Ali](https://abp.io/community/members/adnanaldaim) has published 2 new articles:
- [Integrating AI into ABP.IO Applications: The Complete Guide to Volo.Abp.AI and AI Management Module](https://abp.io/community/articles/integrating-ai-into-abp.io-applications-the-complete-guide-jc9fbjq0)
- [How ABP.IO Framework Cuts Your MVP Development Time by 60%](https://abp.io/community/articles/how-abp.io-framework-cuts-your-mvp-development-time-by-60-8l7m3ugj)
- [My First Look and Experience with Google AntiGravity](https://abp.io/community/articles/my-first-look-and-experience-with-google-antigravity-0hr4sjtf) by [Alper Ebiçoğlu](https://twitter.com/alperebicoglu)
- [TOON vs JSON for LLM Prompts in ABP: Token-Efficient Structured Context](https://abp.io/community/articles/toon-vs-json-b4rn2avd) by [Suhaib Mousa](https://abp.io/community/members/suhaib-mousa)
Thanks to the ABP Community for all the content they have published. You can also [post your ABP-related (text or video) content](https://abp.io/community/posts/create) to the ABP Community.
## Conclusion
This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://abp.io/docs/10.1/release-info/road-map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v10.1 RC and provide feedback to help us release a more stable version.
Thanks for being a part of this community!

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/ai-management-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/ai-management-workspaces.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/community-talk-2025-10-ai.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/cover-image.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/dotnet-conf-china-2025.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/file-sharing.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/live-training-discount.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/my-passkey.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-login.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-registration.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/passkey-setting.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/password-history-settings.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/password-history-warning.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/referral-program.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/reset-password-error-modal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/set-password-error-modal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
docs/en/Blog-Posts/2026-01-08 v10_1_Preview/studio-switch-to-preview.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

2
docs/en/Community-Articles/2025-09-02-training-campaign/post.md

@ -1,6 +1,6 @@
# IMPROVE YOUR ABP SKILLS WITH 33% OFF LIVE TRAININGS!
We have exciting news to share\! As you know, we offer live training packages to help you improve your skills and knowledge of ABP. From September 8th to 19th, we are giving you 33% OFF our live trainings, so you can learn more about the product at a discounted price\!
We have exciting news to share\! As you know, we offer live training packages to help you improve your skills and knowledge of ABP. For a limited time, we are giving you 33% OFF our live trainings, so you can learn more about the product at a discounted price\!
#### Why Join ABP.IO Training?

4
docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/post.md

@ -76,7 +76,7 @@ Installation is straightforward using the [ABP Studio](https://abp.io/studio). Y
- Client Components
- Integration to Startup Templates
### v10.1
### v10.1
- Blazor UI
- Angular UI
- Resource based authorization on Workspaces
@ -103,4 +103,4 @@ The AI Management Module is available now for ABP Team and higher license holder
---
*The AI Management Module is currently in preview. We're excited to hear your feedback as we continue to improve and add new features!*
*The AI Management Module is currently in preview. We're excited to hear your feedback as we continue to improve and add new features!*

170
docs/en/Community-Articles/2026-01-11/article.md

@ -0,0 +1,170 @@
# Async Chain of Persistence Pattern: Designing for Failure in Event-Driven Systems
## Introduction
Messages can get lost while being processed when you use asynchronous messaging or event handling.
The Async Chain of Persistence Pattern makes sure that no message is ever lost from the system by making sure that the message is always stored at every step of the workflow.
## The Fundamental Principle of Pattern
The Async Chain of Persistence Pattern guarantees that no message is ever lost by ensuring that the message is always persistently stored at every step of the workflow. This is where the pattern gets its name. A message cannot be removed from its previous location until it is confirmed to be persistently stored in the subsequent stages of the chain.
It is commonly used in event-driven systems and message-driven systems.
### Event-Driven versus Message-Driven Systems
To understand the pattern, it's important to know the differences between events and messages.
#### The Core Difference
**Event:** Says "something happened", describes the past. Example: `OrderPlaced`, `PaymentCompleted`
**Message:** Says "do this", commands for the future. Example: `CreateOrder`, `SendEmail`
| Property | Event | Message |
|----------|-------|---------|
| **Coupling** | Loose - no one knows who's listening | Tighter - there's a specific receiver |
| **Publishing** | Pub/Sub - 0-N services listen | Point-to-Point - usually 1 service |
| **Tense** | Past tense, immutable | Present/future tense |
| **Error Handling** | If one consumer fails, others continue | If not processed, system breaks |
### Relationship with Async Chain of Persistence
**In event-driven systems:** Each service receives the event → persists it → publishes a new event
![Event-Driven Systems](event-driven-systems.png)
**In message-driven systems:** At each step, the message is kept safe on queue + disk
In both systems, the goal is the same: no message should be lost!
![Message-Driven Systems](message-driven-systems.png)
---
## When Do Messages Get Lost?
There are 3 main scenarios for message loss:
### 1. While Processing a Message (Receiving a Message)
While processing a message in a reply, by default, there is an automatic acknowledgment and subsequent deletion of a message that is in a queue. But in cases where there is a fatal or unrecoverable error in the message processing operation or a service instance crashes, this message gets lost.
### 2. Message Broker Crashes
In the majority of message brokers, the default option for the message persistence state is set to nonpersisting. In other words, the message will be held in the memory of the message broker without any persistence. It has been used for the purpose of obtaining rapid responses and improved throughput. However, in the event of failure of the message broker process, the nonpersistent message will be lost permanently.
### 3. Event Chaining
In event-driven systems, an event is published as a derived message after an operation is done by a service. Message loss in two ways is possible in this scenario:
1. **Risk of Asynchronous Send:** The publish operation is often carried out using the asynchronous send feature. When a fatal error takes place prior to the receipt of acknowledgment of the message publish operation by the publish services, it becomes difficult to determine if the message has been successfully transmitted to the message broker.
2. **Transaction Coordination:** In case an error is detected after the commit of the database but before the derived event is published, the derived event might be lost.
## Implementing the Pattern: 4 Critical Steps
Four critical steps are required to implement the Async Chain of Persistence Pattern:
### 1. Message Persistence
The initial step to ensure that there are no lost messages is to specify the messages as PERSISTENT: When the message broker receives the message, it is saved on disk.
```javascript
var delivery_mode = PERSISTENT
var producer = create_producer(delivery_mode)
// All sent messages are persisted on the message broker
producer.send_message(APPLY_PAYMENT)
// Alternatively
var delivery_mode = PERSISTENT
var producer = create_producer()
producer.send_message(APPLY_PAYMENT, delivery_mode)
```
With this approach, even if the message broker crashes, all messages will still be there when it comes back up.
### 2. Client Acknowledgement Mode
Instead, client-acknowledgement mode needs to be employed. When a message is received in this mode, that message will be retained in the queue until a proper acknowledgment from the processing service has been made. Instead of "auto acknowledge" mode, "client-acknowledgement" mode
Client acknowledgement mode ensures that the message is not lost while processing by the service. When a fatal error is detected during message processing, the service exits without sending an acknowledgement message and thus results in a message redispatched.
**Important Note:** The message has to be acknowledged while the message processing operation is completed so that a repeat message will not be processed.
### 3. Synchronous Send
The synchronous send must be preferred over the asynchronous send.
Although it takes longer to send via synchronous send, since it's a blocking call, it ensures that the message broker received and persisted the message on disk.
```javascript
// Blocking call to publish the derived event
var ack = publish_event(PAYMENT_APPLIED)
if (ack not_successful) {
retry_or_persist(PAYMENT_APPLIED)
}
```
With this approach, the risk of message loss during event chaining is eliminated.
### 4. Last Participant Support
This is the most complex step of the Async Chain of Persistence pattern. It determines when the message should be acknowledged.
#### For Message-Driven Systems:
```javascript
var message = receive_message()
process_message(message)
database.commit()
message.acknowledge()
```
Recommended order: **commit first, ack last.** Otherwise, if the database operation fails, the message will be lost because it has already been removed from the queue.
#### For Event-Driven Systems
In event-driven systems, there are two distinct parties involved: one that acknowledges an event and another that publishes the event that’s been derived. The party that publishes the event that’s been derived should be treated as “last participant.”
```javascript
var event = receive_event()
process_event(event)
database.commit()
message.acknowledge()
var ack = publish_event(PAYMENT_APPLIED)
if (ack not_successful) {
retry_or_persist(PAYMENT_APPLIED)
}
```
In this sequence, the original event is acknowledged after it is completed. If publishing the derived event fails, it can be retried or persisted for later delivery.
---
## Trade-offs
### Advantages
**Preventing Message Loss:**
The major benefit of this pattern is that it doesn't let a message get lost while messages are under processing. This issue, being a serious concern in asynchronous systems, is ruled out due to the pattern.
### Disadvantages
#### 1. Possible Duplicate Messages
Enabling the client-acknowledgement mode can result in the processing of the same message multiple times. If the service instance fails after the database commit but before the message could be acknowledged, the message will be repeated and duplicates can be processed.
**Solution:** In order to determine whether the arriving messages are previously processed or not, the message IDs can be logged and traced back. This also has the drawback of requiring one extra read operation per message in the system.
#### 2. Performance and Throughput
It has also been noted that the time duration in which the persistent message takes to be sent from the message broker could be **up to four times longer** than in the nonpersistent message transmission.
Persisted messages also affect performance in terms of sending and reading the message. It is also common for message brokers to maintain message data in their memory for efficient reading, but there is no assurance that messages will always be resident in memory, depending on memory size, among other factors.
#### 3. Impact of Synchronous Send
Because a synchronous send makes a blocking call until a confirmation is received, there is no other work that can be accomplished before a confirmation is received from the broker for a synchronous send. A persisted message makes this even more apparent.
#### 4. Overall Scalability
This is because, upon receipt, the message broker has to spend more time persisting messages to disk, thus negatively affecting scalability. Persistent messages always give lower total throughput, which can limit scalability under high usage loads and high message volumes.
---
## Conclusion
The Async Chain of Persistence Pattern provides a powerful solution for preventing message loss. Although it has negative effects on performance, throughput, and scalability, these trade-offs are generally acceptable in systems where data loss is critical.
Before implementing the pattern, carefully analyze your system requirements:
- **How critical is message loss?**
- **What are the performance and throughput requirements?**
- **How should the system behave in case of duplicate processing?**
The answers to these questions will help you determine whether the Async Chain of Persistence Pattern is suitable for your system.
## Sample Project
To see an example project where this pattern is implemented, you can check out the repository:
🔗 **[GitHub Repository](https://github.com/fahrigedik/SoftwareArchitecturePatterns)**

BIN
docs/en/Community-Articles/2026-01-11/event-driven-systems.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
docs/en/Community-Articles/2026-01-11/message-driven-systems.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

17
docs/en/Community-Articles/2026-01-16-meet-abio-at-ndc-london/post.md

@ -0,0 +1,17 @@
We are thrilled to announce that **ABP.IO will be sponsoring [NDC London 2026](https://ndclondon.com/),** making the start of 2026 a very exciting time for us\!
NDC London is going to take place from **26th-30th January 2026 at Queen Elizabeth II Center.** This 5-Day event for software developers will have over 90 speakers and 100 sessions. We are excited to be a part of this amazing event once more as devoted supporters of the software development community\!
## Conference Tracks, Topics, and What Developers Can Expect
Developers attending **NDC London 2026** can expect five focused tracks packed with practical, real-world sessions. The conference covers the modern development stack, including **.NET, JavaScript, Cloud, DevOps, Security, Testing, UX, Web**, and emerging technologies, delivered by industry experts with actionable insights developers can apply immediately.
## Discover Previous NDC Events
We have shared **our takeaways from past NDC events** and other conferences [**here**](https://abp.io/community/events/sponsored#gsc.tab=0). You can check them out to learn what we discovered along the way\!
## Stop By Our Booth and Say Hello
We can’t wait to meet fellow developers at NDC London 2026, have meaningful conversations and connect in person. **If you are stopping by our booth, don’t miss our raffle\!** We will be giving away a nice surprise during the event\!
We are looking forward to meeting you there and sharing a few great days focused on software development. See you there\!

BIN
docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/PuppeteerSharp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/QuestPDF.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

153
docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/article.md

@ -0,0 +1,153 @@
# Which Open-Source PDF Libraries Are Recently Popular ? A Data-Driven Look At PDF Topic
So you're looking for a PDF library in .NET, right? Here's the thing - just because something has a million downloads doesn't mean it's what you should use *today*. I'm looking at **recent download momentum** (how many people are actually using it NOW via NuGet) and **GitHub activity** (are they still maintaining this thing or did they abandon it?).
I pulled data from the last ~90 days for the main players in the .NET PDF space. Here's what's actually happening:
## Popularity Comparison of .NET PDF Libraries (*ordered by score*)
| Library | GitHub Stars | Avg Daily NuGet Downloads | Total NuGet Downloads | **Popularity Score** |
|---------|---------------|-----------------------------|----------------------------|---------------------|
| **[Microsoft.Playwright](https://github.com/microsoft/playwright-dotnet)** | [2.9k](https://github.com/microsoft/playwright-dotnet) | [23k](https://www.nuget.org/packages/Microsoft.Playwright) | 39M | **71/100** |
| **[QuestPDF](https://github.com/QuestPDF/QuestPDF)** | [13.7k](https://github.com/QuestPDF/QuestPDF) | [8.2k](https://www.nuget.org/packages/QuestPDF) | 15M | **54/100** |
| **[PDFsharp](https://github.com/empira/PDFsharp)** | [862](https://github.com/empira/PDFsharp) | [9k](https://www.nuget.org/packages/PdfSharp) | 47M | **48/100** |
| **[iText](https://github.com/itext/itext-dotnet)** | [1.9k](https://github.com/itext/itext-dotnet) | [17.2k](https://www.nuget.org/packages/itext) | 16M | **44/100** |
| **[PuppeteerSharp](https://github.com/hardkoded/puppeteer-sharp)** | [3.8k](https://github.com/hardkoded/puppeteer-sharp) | [8.7k](https://www.nuget.org/packages/PuppeteerSharp) | 26M | **40/100** |
**How I calculated the score:** I weighted GitHub Stars (30%), Daily Downloads (40% - because that's what matters NOW), and Total Downloads (30% - for historical context). Everything normalized to 0-100 before weighting. Higher = better momentum overall.
## The Breakdown - What You Actually Need to Know
### [PDFsharp](https://docs.pdfsharp.net/)
![pdfsharp](pdfsharp.png)
**NuGet:** [PdfSharp](https://www.nuget.org/packages/PdfSharp) | **GitHub:** [empira/PDFsharp](https://github.com/empira/PDFsharp)
**What it does:** Code-first PDF stuff - drawing, manipulating, merging, that kind of thing. Not for HTML/browser rendering though, so don't try to convert your React app to PDF with this.
**What's the vibe?** **Stable, but kinda old school.** It's got the biggest total download count (47M!) but only pulling ~9k/day now. They updated it 2 weeks ago (Jan 6) so it's alive, and it supports .NET 8-10 which is nice. The GitHub stars (862) are pretty low compared to the shiny new kids, but honestly? It's been around forever and people still use it. It's the reliable old workhorse.
**Pick this if:**
- You need to build PDFs from scratch with code (not HTML)
- You want to draw graphics, manipulate existing PDFs, merge files
- You don't want browser engines anywhere near your project
---
### [iText](https://itextpdf.com/)
![iText Logo](itext.jpg)
**NuGet:** [itext](https://www.nuget.org/packages/itext/) | **GitHub:** [itext/itext-dotnet](https://github.com/itext/itext-dotnet)
**What it does:** The enterprise beast. Digital signatures, PDF compliance (PDF/A, PDF/UA), forms, all that fancy stuff. Can do HTML-to-PDF too if you need it.
**What's the vibe?** **Actually doing pretty well!** ~17.2k downloads/day (highest for code-first libs), updated literally yesterday (Jan 18). They're moving fast. 1.9k stars isn't huge but the community seems active. The catch? This is the enterprise option - check the licensing before you commit if you're doing commercial work.
**Pick this if:**
- You need digital signatures, PDF compliance, or advanced form stuff
- Your company is cool with licensing fees (or you're doing open source)
- You need serious PDF manipulation features
- You want HTML-to-PDF AND code-based generation in one package
---
### [Microsoft.Playwright](https://playwright.dev/dotnet/)
![Playwright Logo](playwright.png)
**NuGet:** [Microsoft.Playwright](https://www.nuget.org/packages/Microsoft.Playwright) | **GitHub:** [microsoft/playwright-dotnet](https://github.com/microsoft/playwright-dotnet)
**What it does:** Browser automation that can turn HTML/CSS/JS into PDFs. Uses real browser engines (Chromium, WebKit, Firefox) so your PDFs look exactly like they would in a browser.
**What's the vibe?** **Killing it.** ~23k downloads/day (highest in this whole list!). It's Microsoft-backed so you know they're not gonna abandon it anytime soon. Last commit was December 3rd but honestly that's fine, they're actively maintaining. 2.9k stars and climbing. If you need to turn web pages into PDFs, this is probably your best bet right now.
**Pick this if:**
- You need to convert HTML/CSS/JS to PDF and want it to look EXACTLY like the browser
- You're working with SPAs, dynamic content, or web templates
- You also need browser automation/testing (bonus!)
- Layout accuracy is critical (forms, dashboards, etc.)
---
### [PuppeteerSharp](https://www.puppeteersharp.com/)
![PuppeteerSharp Logo](PuppeteerSharp.png)
**NuGet:** [PuppeteerSharp](https://www.nuget.org/packages/PuppeteerSharp) | **GitHub:** [hardkoded/puppeteer-sharp](https://github.com/hardkoded/puppeteer-sharp)
**What it does:** Basically Playwright's older sibling. Uses headless Chromium to turn HTML into PDFs. Same idea, different API.
**What's the vibe?** **Stable but losing ground.** Got updated last week (Jan 12) so it's maintained, but ~8.7k/day is way less than Playwright's ~23k. 3.8k stars is decent though. It works fine, but Playwright is eating its lunch. Still, if you know Puppeteer already or only need Chromium, this might be fine.
**Pick this if:**
- You already know Puppeteer from Node.js and want the same vibe in .NET
- You only need Chromium (don't care about Firefox/WebKit)
- You have existing Puppeteer code you're porting
---
### [QuestPDF](https://github.com/QuestPDF/QuestPDF)
![QuestPDF Logo](QuestPDF.png)
**NuGet:** [QuestPDF](https://www.nuget.org/packages/QuestPDF) | **GitHub:** [QuestPDF/QuestPDF](https://github.com/QuestPDF/QuestPDF)
**What it does:** Build PDFs with fluent C# APIs. Think of it like building a UI layout, but for PDFs. No HTML needed - it's all code, all .NET.
**What's the vibe?** **The community favorite.** 13.7k stars (most by far!), updated yesterday (Jan 18). ~8.2k downloads/day isn't the highest but the community is clearly excited about it. Modern API, active dev, people seem to actually enjoy using it. If you're building reports/invoices from code and want something that feels modern, this is it.
**Pick this if:**
- You want to build PDFs with code (not HTML) and you like fluent APIs
- You're generating reports, invoices, structured documents
- You want zero browser dependencies
- You care about type safety and maintainable code
- You want something that feels modern and well-designed
## Who's Winning Right Now?
Here's what the numbers are telling us:
### Code-First Libraries (Building PDFs with Code)
**[QuestPDF](https://github.com/QuestPDF/QuestPDF)** - Score: 54/100
The people's choice. Most GitHub stars (13.7k), updated yesterday, community loves it. Downloads aren't the highest but the engagement is real. This is what people are excited about.
**[iText](https://github.com/itext/itext-dotnet)** - Score: 44/100
Actually pulling the most daily downloads (~17.2k/day) for code-first libs, also updated yesterday. The enterprise crowd is still using this heavily. Just watch that licensing.
**[PDFsharp](https://github.com/empira/PDFsharp)** - Score: 48/100
The old reliable. 47M total downloads but only ~9k/day now. It works, it's stable, but it's not where the momentum is. Still a solid choice if you need something battle-tested.
### HTML/Browser-Based Libraries (Turning Web Pages into PDFs)
**[Microsoft.Playwright](https://github.com/microsoft/playwright-dotnet)** - Score: 71/100
Winner winner. ~23k downloads/day (highest overall), Microsoft backing, actively maintained. If you need HTML-to-PDF, this is probably the move.
**[PuppeteerSharp](https://github.com/hardkoded/puppeteer-sharp)** - Score: 40/100
Still kicking around at ~8.7k/day but Playwright is clearly the future. Updated last week so it's not dead, just... less popular.
## TL;DR - What Should You Actually Use?
**Building PDFs from code (not HTML):**
- **QuestPDF** - If you want something modern and the community is raving about it (13.7k stars!)
- **iText** - If you need enterprise features and can handle the licensing
- **PDFsharp** - If you want the battle-tested option that's been around forever
**Converting HTML/web pages to PDF:**
- **Playwright** - Just use this. It's winning right now (~23k/day), Microsoft-backed, actively maintained. Game over.
- **PuppeteerSharp** - Only if you really need Chromium-only or you're migrating from Node.js Puppeteer
**Bottom line:** For HTML-to-PDF, Playwright is dominating. For code-first, QuestPDF has the hype but iText has the downloads. Choose your fighter.
---
*Numbers from GitHub and NuGet as of January 19, 2026. Daily downloads are from the last 90 days.*

BIN
docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

BIN
docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/itext.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/pdfsharp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/playwright.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

1
docs/en/framework/ui/angular/data-table-column-extensions.md

@ -342,4 +342,5 @@ export const identityEntityPropContributors = {
## See Also
- [Extensible Table Row Detail](extensible-table-row-detail.md)
- [Customizing Application Modules Guide](../../architecture/modularity/extending/customizing-application-modules-guide.md)

189
docs/en/framework/ui/angular/extensible-table-row-detail.md

@ -0,0 +1,189 @@
```json
//[doc-seo]
{
"Description": "Learn how to add expandable row details to data tables using the Extensible Table Row Detail component in ABP Framework Angular UI."
}
```
# Extensible Table Row Detail for Angular UI
## Introduction
The `<abp-extensible-table-row-detail>` component allows you to add expandable row details to any `<abp-extensible-table>`. When users click the expand icon, additional content is revealed below the row.
<img alt="Extensible Table Row Detail Example" src="./images/row-detail-image.png" width="800px" style="max-width:100%">
## Quick Start
### Step 1. Import the Component
Import `ExtensibleTableRowDetailComponent` in your component:
```typescript
import {
ExtensibleTableComponent,
ExtensibleTableRowDetailComponent
} from '@abp/ng.components/extensible';
@Component({
// ...
imports: [
ExtensibleTableComponent,
ExtensibleTableRowDetailComponent,
],
})
export class MyComponent { }
```
### Step 2. Add Row Detail Template
Place `<abp-extensible-table-row-detail>` inside `<abp-extensible-table>` with an `ng-template`:
```html
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list">
<abp-extensible-table-row-detail>
<ng-template let-row="row" let-expanded="expanded">
<div class="p-3">
<h5>{%{{{ row.name }}}%}</h5>
<p>ID: {%{{{ row.id }}}%}</p>
<p>Status: {%{{{ row.isActive ? 'Active' : 'Inactive' }}}%}</p>
</div>
</ng-template>
</abp-extensible-table-row-detail>
</abp-extensible-table>
```
An expand/collapse chevron icon will automatically appear in the first column of each row.
## API
### ExtensibleTableRowDetailComponent
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `rowHeight` | `string` &#124; `number` | `'100%'` | Height of the expanded row detail area |
### Template Context Variables
| Variable | Type | Description |
|----------|------|-------------|
| `row` | `R` | The current row data object |
| `expanded` | `boolean` | Whether the row is currently expanded |
## Usage Examples
### Basic Example
Display additional information when a row is expanded:
```html
<abp-extensible-table [data]="data.items" [list]="list">
<abp-extensible-table-row-detail>
<ng-template let-row="row">
<div class="p-3 border rounded m-2">
<strong>Details for: {%{{{ row.name }}}%}</strong>
<pre>{%{{{ row | json }}}%}</pre>
</div>
</ng-template>
</abp-extensible-table-row-detail>
</abp-extensible-table>
```
### With Custom Row Height
Specify a fixed height for the detail area:
```html
<abp-extensible-table-row-detail [rowHeight]="200">
<ng-template let-row="row">
<div class="p-3">Fixed 200px height content</div>
</ng-template>
</abp-extensible-table-row-detail>
```
### Using Expanded State
Apply conditional styling based on expansion state:
```html
<abp-extensible-table-row-detail>
<ng-template let-row="row" let-expanded="expanded">
<div class="p-3" [class.fade-in]="expanded">
<p>This row is {%{{{ expanded ? 'expanded' : 'collapsed' }}}%}</p>
</div>
</ng-template>
</abp-extensible-table-row-detail>
```
### With Badges and Localization
```html
<abp-extensible-table-row-detail>
<ng-template let-row="row">
<div class="p-3 bg-light border rounded m-2">
<div class="row">
<div class="col-md-6">
<p class="mb-1"><strong>{%{{{ 'MyModule::Name' | abpLocalization }}}%}</strong></p>
<p class="text-muted">{%{{{ row.name }}}%}</p>
</div>
<div class="col-md-6">
<p class="mb-1"><strong>{%{{{ 'MyModule::Status' | abpLocalization }}}%}</strong></p>
<p>
@if (row.isActive) {
<span class="badge bg-success">{%{{{ 'AbpUi::Yes' | abpLocalization }}}%}</span>
} @else {
<span class="badge bg-secondary">{%{{{ 'AbpUi::No' | abpLocalization }}}%}</span>
}
</p>
</div>
</div>
</div>
</ng-template>
</abp-extensible-table-row-detail>
```
## Alternative: Direct Template Input
For simpler use cases, you can use the `rowDetailTemplate` input on `<abp-extensible-table>` directly:
```html
<abp-extensible-table
[data]="data.items"
[list]="list"
[rowDetailTemplate]="detailTemplate"
/>
<ng-template #detailTemplate let-row="row">
<div class="p-3">{%{{{ row.name }}}%}</div>
</ng-template>
```
## Events
### rowDetailToggle
The `rowDetailToggle` output emits when a row is expanded or collapsed:
```html
<abp-extensible-table
[data]="data.items"
[list]="list"
(rowDetailToggle)="onRowToggle($event)"
>
<abp-extensible-table-row-detail>
<ng-template let-row="row">...</ng-template>
</abp-extensible-table-row-detail>
</abp-extensible-table>
```
```typescript
onRowToggle(row: MyDto) {
console.log('Row toggled:', row);
}
```
## See Also
- [Data Table Column Extensions](data-table-column-extensions.md)
- [Entity Action Extensions](entity-action-extensions.md)
- [Extensions Overview](extensions-overall.md)

BIN
docs/en/framework/ui/angular/images/row-detail-image.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

2
docs/en/framework/ui/angular/localization.md

@ -12,7 +12,7 @@ Before exploring _the localization pipe_ and _the localization service_, you sho
The localization key format consists of two sections which are **Resource Name** and **Key**.
`ResourceName::Key`
> If you do not specify the resource name, the `defaultResourceName` which is declared in `environment.ts` will be considered as default.
> If you do not specify the resource name, the `defaultResourceName` will be used. The value is first retrieved from the backend API response. If the backend does not provide a `defaultResourceName`, the value declared in `environment.ts` will be used as a fallback.
```ts
const environment = {

4
docs/en/modules/audit-logging-pro.md

@ -143,7 +143,7 @@ Configure<ExpiredAuditLogDeleterOptions>(options =>
// The Hangfire Cron expression is different from the Quartz Cron expression, Please refer to the following links:
// https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/crontriggers.html#cron-expressions
// https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html
options.ExcelFileCleanupOptions.CronExpression = "0 23 * * *"; // Quartz Cron expression is "0 23 * * * ?"
options.ExcelFileCleanupOptions.CronExpression = "0 23 * * *"; // Quartz Cron expression is "0 0 23 * * ?"
});
```
@ -164,7 +164,7 @@ Configure<AuditLogExcelFileOptions>(options =>
// The Hangfire Cron expression is different from the Quartz Cron expression, Please refer to the following links:
// https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/crontriggers.html#cron-expressions
// https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html
options.ExcelFileCleanupOptions.CronExpression = "0 23 * * *"; // Quartz Cron expression is "0 23 * * * ?"
options.ExcelFileCleanupOptions.CronExpression = "0 23 * * *"; // Quartz Cron expression is "0 0 23 * * ?"
});
```

BIN
docs/en/studio/images/overview/ai-assistant.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

21
docs/en/studio/overview.md

@ -1,7 +1,7 @@
```json
//[doc-seo]
{
"Description": "Explore ABP Studio's key features like Solution Explorer and Kubernetes Integration to optimize your ABP application development and management."
"Description": "Explore ABP Studio's key features like Solution Explorer, Kubernetes Integration, and AI Assistant to optimize your ABP application development and management."
}
```
@ -19,7 +19,7 @@
## Introduction
ABP Studio, a comprehensive desktop application, offers a wide range of features and functionalities tailored to streamline the development and management of ABP-based applications. This article provides an overview of the key components of ABP Studio, including the Solution Explorer, Solution Runner, Kubernetes Integration, Application Monitoring Area, Background Tasks, Notifications and Logs. Understanding these components is essential for efficiently utilizing ABP Studio to its full potential.
ABP Studio, a comprehensive desktop application, offers a wide range of features and functionalities tailored to streamline the development and management of ABP-based applications. This article provides an overview of the key components of ABP Studio, including the Solution Explorer, Solution Runner, Kubernetes Integration, AI Assistant, Application Monitoring Area, Background Tasks, Notifications and Logs. Understanding these components is essential for efficiently utilizing ABP Studio to its full potential.
![overview-intro](./images/overview/overview-intro.png)
@ -69,7 +69,7 @@ The Welcome Screen is the initial user interface users encounter upon starting A
## Sidebar
Located on the left side of the interface, the Left Area is a panel that provides quick access to various functionalities like [Solution Explorer](./solution-explorer.md), [Solution Runner](./running-applications.md), and Kubernetes Integration. Now let's examine each item.
Located on the left side of the interface, the Left Area is a panel that provides quick access to various functionalities like [Solution Explorer](./solution-explorer.md), [Solution Runner](./running-applications.md), Kubernetes Integration, and AI Assistant. Now let's examine each item.
### Solution Explorer
@ -99,6 +99,21 @@ This pane is dedicated to managing Kubernetes services. It simplifies the proces
![kubernetes-integration-kubernetes-pane](./images/overview/kubernetes-integration-kubernetes.png)
### AI Assistant
The AI Assistant is an integrated chat interface within ABP Studio that provides intelligent assistance for ABP-related questions. You can access it from the left sidebar by clicking the AI icon.
![ai-assistant](./images/overview/ai-assistant.png)
Key features of the AI Assistant include:
- **Chat Interface**: Ask anything about ABP Framework, and get instant, context-aware responses to help with your development workflow.
- **File Attachments**: Attach files to your conversations to get more specific assistance with your code or configuration.
- **Recent Chats**: Access your conversation history to continue previous discussions or reference past solutions.
- **Credits System**: The AI Assistant uses a credits-based system, with usage tracked and displayed in the interface.
> **Note**: Review the Privacy Notice available in the AI Assistant panel to understand how your data is handled.
## Application Monitoring Area
The [Application Monitoring Area](./monitoring-applications.md) is a functional component within ABP Studio that displays real-time operational status and metrics for applications. It indicates the active state of applications, the count of running instances, and so on. The user interface is organized into several tabs: Overall, Browse, HTTP Requests, Events, Exceptions, and Logs. Now let's examine the **Overall** tab.

76
framework/src/Volo.Abp.AspNetCore.Components.Server/Microsoft/AspNetCore/Authentication/Cookies/CookieAuthenticationOptionsExtensions.cs

@ -1,86 +1,16 @@
using System;
using System.Threading.Tasks;
using Duende.IdentityModel.Client;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.Threading;
namespace Microsoft.AspNetCore.Authentication.Cookies;
public static class CookieAuthenticationOptionsExtensions
{
/// <summary>
/// Introspect access token on validating the principal.
/// Check the access_token is expired or inactive.
/// </summary>
/// <param name="options"></param>
/// <param name="oidcAuthenticationScheme"></param>
/// <returns></returns>
[Obsolete("Use CheckTokenExpiration method instead.")]
public static CookieAuthenticationOptions IntrospectAccessToken(this CookieAuthenticationOptions options, string oidcAuthenticationScheme = "oidc")
{
options.Events.OnValidatePrincipal = async principalContext =>
{
if (principalContext.Principal == null || principalContext.Principal.Identity == null || !principalContext.Principal.Identity.IsAuthenticated)
{
return;
}
var logger = principalContext.HttpContext.RequestServices.GetRequiredService<ILogger<CookieAuthenticationOptions>>();
var accessToken = principalContext.Properties.GetTokenValue("access_token");
if (!accessToken.IsNullOrWhiteSpace())
{
var openIdConnectOptions = await GetOpenIdConnectOptions(principalContext, oidcAuthenticationScheme);
var response = await openIdConnectOptions.Backchannel.IntrospectTokenAsync(new TokenIntrospectionRequest
{
Address = openIdConnectOptions.Configuration?.IntrospectionEndpoint ?? openIdConnectOptions.Authority!.EnsureEndsWith('/') + "connect/introspect",
ClientId = openIdConnectOptions.ClientId!,
ClientSecret = openIdConnectOptions.ClientSecret,
Token = accessToken
});
if (response.IsError)
{
logger.LogError(response.Error);
await SignOutAsync(principalContext);
return;
}
if (!response.IsActive)
{
logger.LogError("The access_token is not active.");
await SignOutAsync(principalContext);
return;
}
logger.LogInformation("The access_token is active.");
}
else
{
logger.LogError("The access_token is not found in the cookie properties, Please make sure SaveTokens of OpenIdConnectOptions is set as true.");
await SignOutAsync(principalContext);
}
};
return options;
}
private async static Task<OpenIdConnectOptions> GetOpenIdConnectOptions(CookieValidatePrincipalContext principalContext, string oidcAuthenticationScheme)
{
var openIdConnectOptions = principalContext.HttpContext.RequestServices.GetRequiredService<IOptionsMonitor<OpenIdConnectOptions>>().Get(oidcAuthenticationScheme);
if (openIdConnectOptions.Configuration == null && openIdConnectOptions.ConfigurationManager != null)
{
var cancellationTokenProvider = principalContext.HttpContext.RequestServices.GetRequiredService<ICancellationTokenProvider>();
openIdConnectOptions.Configuration = await openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(cancellationTokenProvider.Token);
}
return openIdConnectOptions;
}
private async static Task SignOutAsync(CookieValidatePrincipalContext principalContext)
{
principalContext.RejectPrincipal();
await principalContext.HttpContext.SignOutAsync(principalContext.Scheme.Name);
return options.CheckTokenExpiration(oidcAuthenticationScheme, null, TimeSpan.FromMinutes(1));
}
}

31
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@ -23,7 +24,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker;
public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelperService<TTagHelper>
where TTagHelper : AbpDatePickerBaseTagHelper<TTagHelper>
{
protected readonly Dictionary<Type, Func<object, string>> SupportedInputTypes;
protected readonly FrozenDictionary<Type, Func<object, string>> SupportedInputTypes;
protected readonly IJsonSerializer JsonSerializer;
protected readonly IHtmlGenerator Generator;
@ -103,7 +104,7 @@ public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelp
return string.Empty;
}
}
};
}.ToFrozenDictionary();
}
protected virtual T? GetAttribute<T>() where T : Attribute
@ -136,7 +137,7 @@ public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelp
? await ProcessButtonAndGetContentAsync(context, output, "calendar", "open")
: "";
var clearButtonContent = TagHelper.ClearButton == true || (!TagHelper.ClearButton.HasValue && TagHelper.AutoUpdateInput != true)
? await ProcessButtonAndGetContentAsync(context, output, "times", "clear", visible:!TagHelper.SingleOpenAndClearButton)
? await ProcessButtonAndGetContentAsync(context, output, "times", "clear", visible: !TagHelper.SingleOpenAndClearButton)
: "";
var labelContent = await GetLabelAsHtmlAsync(context, output, TagHelperOutput);
@ -269,7 +270,7 @@ public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelp
{
var attrList = new TagHelperAttributeList();
if(options == null)
if (options == null)
{
return attrList;
}
@ -401,29 +402,29 @@ public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelp
attrList.Add("data-visible-date-format", options.VisibleDateFormat);
}
if(!options.InputDateFormat.IsNullOrEmpty())
if (!options.InputDateFormat.IsNullOrEmpty())
{
attrList.Add("data-input-date-format", options.InputDateFormat);
}
if(options.Ranges != null && options.Ranges.Any())
if (options.Ranges != null && options.Ranges.Any())
{
var ranges = options.Ranges.ToDictionary(r => r.Label, r => r.Dates);
attrList.Add("data-ranges", JsonSerializer.Serialize(ranges));
}
if(options.AlwaysShowCalendars != null)
if (options.AlwaysShowCalendars != null)
{
attrList.Add("data-always-show-calendars", options.AlwaysShowCalendars.ToString()!.ToLowerInvariant());
}
if(options.ShowCustomRangeLabel == false)
if (options.ShowCustomRangeLabel == false)
{
attrList.Add("data-show-custom-range-label", options.ShowCustomRangeLabel.ToString()!.ToLowerInvariant());
}
if(options.Options != null)
if (options.Options != null)
{
attrList.Add("data-options", JsonSerializer.Serialize(options.Options));
}
@ -443,7 +444,7 @@ public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelp
attrList.Add("id", options.PickerId);
}
if(!options.SingleOpenAndClearButton)
if (!options.SingleOpenAndClearButton)
{
attrList.Add("data-single-open-and-clear-button", options.SingleOpenAndClearButton.ToString().ToLowerInvariant());
}
@ -614,7 +615,8 @@ public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelp
{
return string.Empty;
}
var labelTagHelper = new LabelTagHelper(Generator) {
var labelTagHelper = new LabelTagHelper(Generator)
{
ViewContext = TagHelper.ViewContext,
For = modelExpression
};
@ -764,7 +766,8 @@ public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelp
TagHelper.Size = attribute.Size;
}
return TagHelper.Size switch {
return TagHelper.Size switch
{
AbpFormControlSize.Small => "form-control-sm",
AbpFormControlSize.Medium => "form-control-md",
AbpFormControlSize.Large => "form-control-lg",
@ -785,14 +788,14 @@ public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelp
protected virtual async Task<string> GetValidationAsHtmlByInputAsync(TagHelperContext context,
TagHelperOutput output,
[NotNull]ModelExpression @for)
[NotNull] ModelExpression @for)
{
var validationMessageTagHelper =
new ValidationMessageTagHelper(Generator) { For = @for, ViewContext = TagHelper.ViewContext };
var attributeList = new TagHelperAttributeList { { "class", "text-danger" } };
if(!output.Attributes.TryGetAttribute("name", out var nameAttribute) || nameAttribute == null || nameAttribute.Value == null)
if (!output.Attributes.TryGetAttribute("name", out var nameAttribute) || nameAttribute == null || nameAttribute.Value == null)
{
if (nameAttribute != null)
{

6
framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
@ -8,7 +9,8 @@ namespace Volo.Abp.ObjectExtending;
public static class MvcUiObjectExtensionPropertyInfoExtensions
{
private static readonly HashSet<Type> NumberTypes = new HashSet<Type> {
private static readonly FrozenSet<Type> NumberTypes = new HashSet<Type>
{
typeof(int),
typeof(long),
typeof(byte),
@ -33,7 +35,7 @@ public static class MvcUiObjectExtensionPropertyInfoExtensions
typeof(float?),
typeof(double?),
typeof(decimal?)
};
}.ToFrozenSet();
public static string? GetInputFormatOrNull(this IBasicObjectExtensionPropertyInfo property)
{

38
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs

@ -2,27 +2,57 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.Extensions.Logging;
using Volo.Abp.Timing;
namespace Volo.Abp.AspNetCore.Mvc.ModelBinding;
public class AbpDateTimeModelBinder : IModelBinder
{
private readonly ILogger<AbpDateTimeModelBinder> _logger;
private readonly DateTimeModelBinder _dateTimeModelBinder;
private readonly IClock _clock;
private readonly ICurrentTimezoneProvider _currentTimezoneProvider;
private readonly ITimezoneProvider _timezoneProvider;
public AbpDateTimeModelBinder(IClock clock, DateTimeModelBinder dateTimeModelBinder)
public AbpDateTimeModelBinder(
ILogger<AbpDateTimeModelBinder> logger,
DateTimeModelBinder dateTimeModelBinder,
IClock clock,
ICurrentTimezoneProvider currentTimezoneProvider,
ITimezoneProvider timezoneProvider)
{
_clock = clock;
_logger = logger;
_dateTimeModelBinder = dateTimeModelBinder;
_clock = clock;
_currentTimezoneProvider = currentTimezoneProvider;
_timezoneProvider = timezoneProvider;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
await _dateTimeModelBinder.BindModelAsync(bindingContext);
if (bindingContext.Result.IsModelSet && bindingContext.Result.Model is DateTime dateTime)
if (!bindingContext.Result.IsModelSet || bindingContext.Result.Model is not DateTime dateTime)
{
bindingContext.Result = ModelBindingResult.Success(_clock.Normalize(dateTime));
return;
}
if (dateTime.Kind == DateTimeKind.Unspecified &&
_clock.SupportsMultipleTimezone &&
!_currentTimezoneProvider.TimeZone.IsNullOrWhiteSpace())
{
try
{
var timezoneInfo = _timezoneProvider.GetTimeZoneInfo(_currentTimezoneProvider.TimeZone);
dateTime = new DateTimeOffset(dateTime, timezoneInfo.GetUtcOffset(dateTime)).UtcDateTime;
}
catch
{
_logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", _currentTimezoneProvider.TimeZone);
}
}
bindingContext.Result = ModelBindingResult.Success(_clock.Normalize(dateTime));
}
}

2
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinderProvider.cs

@ -50,6 +50,6 @@ public class AbpDateTimeModelBinderProvider : IModelBinderProvider
{
const DateTimeStyles supportedStyles = DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AdjustToUniversal;
var dateTimeModelBinder = new DateTimeModelBinder(supportedStyles, context.Services.GetRequiredService<ILoggerFactory>());
return new AbpDateTimeModelBinder(context.Services.GetRequiredService<IClock>(), dateTimeModelBinder);
return ActivatorUtilities.CreateInstance<AbpDateTimeModelBinder>(context.Services, dateTimeModelBinder);
}
}

53
framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs

@ -14,44 +14,58 @@ namespace Microsoft.Extensions.DependencyInjection;
public static class CookieAuthenticationOptionsExtensions
{
/// <summary>
/// Check the access_token is expired or inactive.
/// Check if the access_token is expired or inactive.
/// </summary>
public static CookieAuthenticationOptions CheckTokenExpiration(this CookieAuthenticationOptions options, string oidcAuthenticationScheme = "oidc", TimeSpan? advance = null, TimeSpan? validationInterval = null)
{
advance ??= TimeSpan.FromMinutes(3);
validationInterval ??= TimeSpan.FromMinutes(1);
var previousHandler = options.Events.OnValidatePrincipal;
options.Events.OnValidatePrincipal = async principalContext =>
{
if (principalContext.Principal == null || principalContext.Principal.Identity == null || !principalContext.Principal.Identity.IsAuthenticated)
{
await InvokePreviousHandlerAsync(principalContext, previousHandler);
return;
}
var logger = principalContext.HttpContext.RequestServices.GetRequiredService<ILogger<CookieAuthenticationOptions>>();
var tokenExpiresAt = principalContext.Properties.GetString(".Token.expires_at");
if (!tokenExpiresAt.IsNullOrWhiteSpace() && DateTimeOffset.TryParseExact(tokenExpiresAt, "o", null, DateTimeStyles.RoundtripKind, out var expiresAt) &&
expiresAt < DateTimeOffset.UtcNow.Subtract(advance.Value))
if (!tokenExpiresAt.IsNullOrWhiteSpace() && DateTimeOffset.TryParseExact(tokenExpiresAt, "o", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var expiresAt) &&
expiresAt <= DateTimeOffset.UtcNow.Add(advance.Value))
{
logger.LogInformation("The access_token is expired.");
await SignOutAsync(principalContext);
logger.LogInformation("The access_token expires within {AdvanceSeconds}s; signing out.", advance.Value.TotalSeconds);
await SignOutAndInvokePreviousHandlerAsync(principalContext, previousHandler);
return;
}
if (principalContext.Properties.IssuedUtc != null && DateTimeOffset.UtcNow.Subtract(principalContext.Properties.IssuedUtc.Value) > validationInterval)
{
logger.LogInformation($"Check the access_token is active every {validationInterval.Value.TotalSeconds} seconds.");
logger.LogInformation("Checking access_token activity every {Seconds} seconds.", validationInterval.Value.TotalSeconds);
var accessToken = principalContext.Properties.GetTokenValue("access_token");
if (!accessToken.IsNullOrWhiteSpace())
{
var openIdConnectOptions = await GetOpenIdConnectOptions(principalContext, oidcAuthenticationScheme);
var introspectionEndpoint = openIdConnectOptions.Configuration?.IntrospectionEndpoint;
if (introspectionEndpoint.IsNullOrWhiteSpace() && !openIdConnectOptions.Authority.IsNullOrWhiteSpace())
{
introspectionEndpoint = openIdConnectOptions.Authority.EnsureEndsWith('/') + "connect/introspect";
}
if (introspectionEndpoint.IsNullOrWhiteSpace())
{
logger.LogWarning("No introspection endpoint configured. Skipping token activity check.");
await InvokePreviousHandlerAsync(principalContext, previousHandler);
return;
}
var clientId = principalContext.Properties.GetString("client_id");
var clientSecret = principalContext.Properties.GetString("client_secret");
var response = await openIdConnectOptions.Backchannel.IntrospectTokenAsync(new TokenIntrospectionRequest
{
Address = openIdConnectOptions.Configuration?.IntrospectionEndpoint ?? openIdConnectOptions.Authority!.EnsureEndsWith('/') + "connect/introspect",
Address = introspectionEndpoint,
ClientId = clientId ?? openIdConnectOptions.ClientId!,
ClientSecret = clientSecret ?? openIdConnectOptions.ClientSecret,
Token = accessToken
@ -59,15 +73,15 @@ public static class CookieAuthenticationOptionsExtensions
if (response.IsError)
{
logger.LogError(response.Error);
await SignOutAsync(principalContext);
logger.LogError("Token introspection error: {Error}", response.Error);
await SignOutAndInvokePreviousHandlerAsync(principalContext, previousHandler);
return;
}
if (!response.IsActive)
{
logger.LogError("The access_token is not active.");
await SignOutAsync(principalContext);
await SignOutAndInvokePreviousHandlerAsync(principalContext, previousHandler);
return;
}
@ -76,10 +90,12 @@ public static class CookieAuthenticationOptionsExtensions
}
else
{
logger.LogError("The access_token is not found in the cookie properties, Please make sure SaveTokens of OpenIdConnectOptions is set as true.");
logger.LogError("The access_token is not found in the cookie properties. Ensure SaveTokens of OpenIdConnectOptions is true.");
await SignOutAsync(principalContext);
}
}
await InvokePreviousHandlerAsync(principalContext, previousHandler);
};
return options;
@ -97,9 +113,20 @@ public static class CookieAuthenticationOptionsExtensions
return openIdConnectOptions;
}
private async static Task SignOutAsync(CookieValidatePrincipalContext principalContext)
private static async Task SignOutAsync(CookieValidatePrincipalContext principalContext)
{
principalContext.RejectPrincipal();
await principalContext.HttpContext.SignOutAsync(principalContext.Scheme.Name);
}
private static Task InvokePreviousHandlerAsync(CookieValidatePrincipalContext principalContext, Func<CookieValidatePrincipalContext, Task>? previousHandler)
{
return previousHandler != null ? previousHandler(principalContext) : Task.CompletedTask;
}
private static async Task SignOutAndInvokePreviousHandlerAsync(CookieValidatePrincipalContext principalContext, Func<CookieValidatePrincipalContext, Task>? previousHandler)
{
await SignOutAsync(principalContext);
await InvokePreviousHandlerAsync(principalContext, previousHandler);
}
}

1
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs

@ -47,6 +47,7 @@ public class AbpAuthorizationModule : AbpModule
options.ResourceValueProviders.Add<UserResourcePermissionValueProvider>();
options.ResourceValueProviders.Add<RoleResourcePermissionValueProvider>();
options.ResourceValueProviders.Add<ClientResourcePermissionValueProvider>();
});
Configure<AbpVirtualFileSystemOptions>(options =>

2
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/ClientPermissionValueProvider.cs

@ -44,7 +44,7 @@ public class ClientPermissionValueProvider : PermissionValueProvider
var clientId = context.Principal?.FindFirst(AbpClaimTypes.ClientId)?.Value;
if (clientId == null)
{
return new MultiplePermissionGrantResult(permissionNames); ;
return new MultiplePermissionGrantResult(permissionNames);
}
using (CurrentTenant.Change(null))

55
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ClientResourcePermissionValueProvider.cs

@ -0,0 +1,55 @@
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Security.Claims;
namespace Volo.Abp.Authorization.Permissions.Resources;
public class ClientResourcePermissionValueProvider : ResourcePermissionValueProvider
{
public const string ProviderName = "C";
public override string Name => ProviderName;
protected ICurrentTenant CurrentTenant { get; }
public ClientResourcePermissionValueProvider(IResourcePermissionStore resourcePermissionStore, ICurrentTenant currentTenant)
: base(resourcePermissionStore)
{
CurrentTenant = currentTenant;
}
public override async Task<PermissionGrantResult> CheckAsync(ResourcePermissionValueCheckContext context)
{
var clientId = context.Principal?.FindFirst(AbpClaimTypes.ClientId)?.Value;
if (clientId == null)
{
return PermissionGrantResult.Undefined;
}
using (CurrentTenant.Change(null))
{
return await ResourcePermissionStore.IsGrantedAsync(context.Permission.Name, context.ResourceName, context.ResourceKey, Name, clientId)
? PermissionGrantResult.Granted
: PermissionGrantResult.Undefined;
}
}
public override async Task<MultiplePermissionGrantResult> CheckAsync(ResourcePermissionValuesCheckContext context)
{
var permissionNames = context.Permissions.Select(x => x.Name).Distinct().ToArray();
Check.NotNullOrEmpty(permissionNames, nameof(permissionNames));
var clientId = context.Principal?.FindFirst(AbpClaimTypes.ClientId)?.Value;
if (clientId == null)
{
return new MultiplePermissionGrantResult(permissionNames);
}
using (CurrentTenant.Change(null))
{
return await ResourcePermissionStore.IsGrantedAsync(permissionNames, context.ResourceName, context.ResourceKey, Name, clientId);
}
}
}

8
framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQOptions.cs

@ -5,11 +5,11 @@ namespace Volo.Abp.BackgroundWorkers.TickerQ;
public class AbpBackgroundWorkersTickerQOptions
{
private readonly Dictionary<Type, AbpBackgroundWorkersCronTickerConfiguration> _onfigurations;
private readonly Dictionary<Type, AbpBackgroundWorkersCronTickerConfiguration> _configurations;
public AbpBackgroundWorkersTickerQOptions()
{
_onfigurations = new Dictionary<Type, AbpBackgroundWorkersCronTickerConfiguration>();
_configurations = new Dictionary<Type, AbpBackgroundWorkersCronTickerConfiguration>();
}
public void AddConfiguration<TWorker>(AbpBackgroundWorkersCronTickerConfiguration configuration)
@ -19,7 +19,7 @@ public class AbpBackgroundWorkersTickerQOptions
public void AddConfiguration(Type workerType, AbpBackgroundWorkersCronTickerConfiguration configuration)
{
_onfigurations[workerType] = configuration;
_configurations[workerType] = configuration;
}
public AbpBackgroundWorkersCronTickerConfiguration? GetConfigurationOrNull<TJob>()
@ -29,6 +29,6 @@ public class AbpBackgroundWorkersTickerQOptions
public AbpBackgroundWorkersCronTickerConfiguration? GetConfigurationOrNull(Type workerType)
{
return _onfigurations.GetValueOrDefault(workerType);
return _configurations.GetValueOrDefault(workerType);
}
}

11
framework/src/Volo.Abp.BlazoriseUI/BlazoriseUiObjectExtensionPropertyInfoExtensions.cs

@ -1,5 +1,6 @@
using Blazorise;
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@ -11,7 +12,8 @@ namespace Volo.Abp.BlazoriseUI;
public static class BlazoriseUiObjectExtensionPropertyInfoExtensions
{
private static readonly HashSet<Type> NumberTypes = new HashSet<Type> {
private static readonly FrozenSet<Type> NumberTypes = new HashSet<Type>
{
typeof(int),
typeof(long),
typeof(byte),
@ -36,13 +38,14 @@ public static class BlazoriseUiObjectExtensionPropertyInfoExtensions
typeof(float?),
typeof(double?),
typeof(decimal?)
};
}.ToFrozenSet();
private static readonly HashSet<Type> TextEditSupportedAttributeTypes = new HashSet<Type> {
private static readonly FrozenSet<Type> TextEditSupportedAttributeTypes = new HashSet<Type>
{
typeof(EmailAddressAttribute),
typeof(UrlAttribute),
typeof(PhoneAttribute)
};
}.ToFrozenSet();
public static string? GetDateEditInputFormatOrNull(this IBasicObjectExtensionPropertyInfo property)
{

7
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs

@ -3,14 +3,12 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NuGet.Versioning;
using Volo.Abp.Cli.Http;
using Volo.Abp.Cli.LIbs;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
@ -28,14 +26,12 @@ public class NpmPackagesUpdater : ITransientDependency
private readonly PackageJsonFileFinder _packageJsonFileFinder;
private readonly NpmGlobalPackagesChecker _npmGlobalPackagesChecker;
private readonly Dictionary<string, string> _fileVersionStorage = new Dictionary<string, string>();
private readonly CliHttpClientFactory _cliHttpClientFactory;
private readonly Dictionary<string, string> _fileVersionStorage = [];
public NpmPackagesUpdater(
PackageJsonFileFinder packageJsonFileFinder,
NpmGlobalPackagesChecker npmGlobalPackagesChecker,
ICancellationTokenProvider cancellationTokenProvider,
CliHttpClientFactory cliHttpClientFactory,
IInstallLibsService installLibsService,
ICmdHelper cmdHelper)
{
@ -44,7 +40,6 @@ public class NpmPackagesUpdater : ITransientDependency
CancellationTokenProvider = cancellationTokenProvider;
InstallLibsService = installLibsService;
CmdHelper = cmdHelper;
_cliHttpClientFactory = cliHttpClientFactory;
Logger = NullLogger<NpmPackagesUpdater>.Instance;
}

4
framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLoggerFactory.cs

@ -5,10 +5,10 @@ namespace Volo.Abp.Logging;
public class DefaultInitLoggerFactory : IInitLoggerFactory
{
private readonly Dictionary<Type, object> _cache = new Dictionary<Type, object>();
private readonly Dictionary<Type, object> _cache = [];
public virtual IInitLogger<T> Create<T>()
{
return (IInitLogger<T>)_cache.GetOrAdd(typeof(T), () => new DefaultInitLogger<T>()); ;
return (IInitLogger<T>)_cache.GetOrAdd(typeof(T), () => new DefaultInitLogger<T>());
}
}

9
framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs

@ -1,5 +1,6 @@
using System;
using System.Collections;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
@ -13,14 +14,14 @@ namespace Volo.Abp.Reflection;
public static class TypeHelper
{
private static readonly HashSet<Type> FloatingTypes = new HashSet<Type>
private static readonly FrozenSet<Type> FloatingTypes = new HashSet<Type>
{
typeof(float),
typeof(double),
typeof(decimal)
};
}.ToFrozenSet();
private static readonly HashSet<Type> NonNullablePrimitiveTypes = new HashSet<Type>
private static readonly FrozenSet<Type> NonNullablePrimitiveTypes = new HashSet<Type>
{
typeof(byte),
typeof(short),
@ -37,7 +38,7 @@ public static class TypeHelper
typeof(DateTimeOffset),
typeof(TimeSpan),
typeof(Guid)
};
}.ToFrozenSet();
public static bool IsNonNullablePrimitiveType(Type type)
{

98
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs

@ -5,6 +5,7 @@ using System.Linq;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
@ -108,12 +109,17 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
}
var entityType = entity.GetType();
var entityFullName = entityType.FullName!;
if (entityEntry.Metadata.HasSharedClrType && !entityEntry.Metadata.IsOwned())
{
entityFullName = entityEntry.Metadata.Name;
}
var entityChange = new EntityChangeInfo
{
ChangeType = changeType,
EntityEntry = entityEntry,
EntityId = entityId,
EntityTypeFullName = entityType.FullName,
EntityTypeFullName = entityFullName,
PropertyChanges = GetPropertyChanges(entityEntry),
EntityTenantId = GetTenantId(entity)
};
@ -181,48 +187,102 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
foreach (var property in properties)
{
if (entityEntry.Metadata.IsMappedToJson() && property.GetJsonPropertyName() == null)
{
continue;
}
var propertyEntry = entityEntry.Property(property.Name);
if (ShouldSavePropertyHistory(propertyEntry, isCreated || isDeleted) && !IsSoftDeleted(entityEntry))
{
var propertyType = DeterminePropertyTypeFromEntry(property, propertyEntry);
propertyChanges.Add(new EntityPropertyChangeInfo
{
NewValue = isDeleted ? null : JsonSerializer.Serialize(propertyEntry.CurrentValue!).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength),
OriginalValue = isCreated ? null : JsonSerializer.Serialize(propertyEntry.OriginalValue!).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength),
PropertyName = property.Name,
PropertyTypeFullName = property.ClrType.GetFirstGenericArgumentIfNullable().FullName!
PropertyTypeFullName = propertyType.FullName!
});
}
}
if (AbpEfCoreNavigationHelper != null)
if (AbpEfCoreNavigationHelper == null)
{
return propertyChanges;
}
foreach (var (navigationEntry, index) in entityEntry.Navigations.Select((value, i) => ( value, i )))
{
foreach (var (navigationEntry, index) in entityEntry.Navigations.Select((value, i) => ( value, i )))
var propertyInfo = navigationEntry.Metadata.PropertyInfo;
if (propertyInfo != null &&
propertyInfo.IsDefined(typeof(DisableAuditingAttribute), true))
{
var propertyInfo = navigationEntry.Metadata.PropertyInfo;
if (propertyInfo != null &&
propertyInfo.IsDefined(typeof(DisableAuditingAttribute), true))
continue;
}
if (navigationEntry.Metadata.TargetEntityType.IsMappedToJson() && navigationEntry is ReferenceEntry referenceEntry && referenceEntry.TargetEntry != null)
{
foreach (var propertyChange in GetPropertyChanges(referenceEntry.TargetEntry))
{
continue;
propertyChange.PropertyName = $"{referenceEntry.Metadata.Name}.{propertyChange.PropertyName}";
propertyChanges.Add(propertyChange);
}
if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, index))
continue;
}
if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, index))
{
var abpNavigationEntry = AbpEfCoreNavigationHelper.GetNavigationEntry(entityEntry, index);
var isCollection = navigationEntry.Metadata.IsCollection;
propertyChanges.Add(new EntityPropertyChangeInfo
{
var abpNavigationEntry = AbpEfCoreNavigationHelper.GetNavigationEntry(entityEntry, index);
var isCollection = navigationEntry.Metadata.IsCollection;
propertyChanges.Add(new EntityPropertyChangeInfo
{
PropertyName = navigationEntry.Metadata.Name,
PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName!,
OriginalValue = GetNavigationPropertyValue(abpNavigationEntry?.OriginalValue, isCollection),
NewValue = GetNavigationPropertyValue(abpNavigationEntry?.CurrentValue, isCollection)
});
}
PropertyName = navigationEntry.Metadata.Name,
PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName!,
OriginalValue = GetNavigationPropertyValue(abpNavigationEntry?.OriginalValue, isCollection),
NewValue = GetNavigationPropertyValue(abpNavigationEntry?.CurrentValue, isCollection)
});
}
}
return propertyChanges;
}
/// <summary>
/// Determines the CLR type of a property based on its EF Core metadata and the values in the given <see cref="PropertyEntry"/>.
/// </summary>
/// <param name="property">The EF Core property metadata that provides the declared CLR type.</param>
/// <param name="propertyEntry">The property entry that contains the current and original values for the property.</param>
/// <returns>
/// The most specific CLR type inferred for the property. This is normally the property's declared CLR type (with
/// nullable wrappers removed). If the declared type is <see cref="object"/>, the type is inferred from the
/// runtime type of <see cref="PropertyEntry.CurrentValue"/> or, if that is <c>null</c>, from
/// <see cref="PropertyEntry.OriginalValue"/>. If both values are <c>null</c>, the declared CLR type
/// (which may remain <see cref="object"/>) is returned.
/// </returns>
protected virtual Type DeterminePropertyTypeFromEntry(IProperty property, PropertyEntry propertyEntry)
{
var propertyType = property.ClrType.GetFirstGenericArgumentIfNullable();
if (propertyType != typeof(object))
{
return propertyType;
}
if (propertyEntry.CurrentValue != null)
{
propertyType = propertyEntry.CurrentValue.GetType().GetFirstGenericArgumentIfNullable();
}
else if (propertyEntry.OriginalValue != null)
{
propertyType = propertyEntry.OriginalValue.GetType().GetFirstGenericArgumentIfNullable();
}
return propertyType;
}
protected virtual string? GetNavigationPropertyValue(object? entity, bool isCollection)
{
switch (entity)

33
framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/PostConfigureAbpRabbitMqEventBusOptions.cs

@ -1,3 +1,4 @@
using System.Collections.Frozen;
using System.Collections.Generic;
using Microsoft.Extensions.Options;
@ -5,23 +6,23 @@ namespace Volo.Abp.EventBus.RabbitMq;
public class PostConfigureAbpRabbitMqEventBusOptions : IPostConfigureOptions<AbpRabbitMqEventBusOptions>
{
private readonly HashSet<string> _uint64QueueArguments =
[
"x-delivery-limit",
"x-expires",
"x-message-ttl",
"x-max-length",
"x-max-length-bytes",
"x-quorum-initial-group-size",
"x-quorum-target-group-size",
"x-stream-filter-size-bytes",
"x-stream-max-segment-size-bytes",
];
private readonly FrozenSet<string> _uint64QueueArguments = new HashSet<string>
{
"x-delivery-limit",
"x-expires",
"x-message-ttl",
"x-max-length",
"x-max-length-bytes",
"x-quorum-initial-group-size",
"x-quorum-target-group-size",
"x-stream-filter-size-bytes",
"x-stream-max-segment-size-bytes",
}.ToFrozenSet();
private readonly HashSet<string> _boolQueueArguments =
[
"x-single-active-consumer"
];
private readonly FrozenSet<string> _boolQueueArguments = new HashSet<string>
{
"x-single-active-consumer"
}.ToFrozenSet();
public virtual void PostConfigure(string? name, AbpRabbitMqEventBusOptions options)
{

6
framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@ -10,7 +11,8 @@ internal static class ProxyScriptingJsFuncHelper
{
private const string ValidJsVariableNameChars = "abcdefghijklmnopqrstuxwvyzABCDEFGHIJKLMNOPQRSTUXWVYZ0123456789_";
private static readonly HashSet<string> ReservedWords = new HashSet<string> {
private static readonly FrozenSet<string> ReservedWords = new HashSet<string>
{
"abstract",
"else",
"instanceof",
@ -71,7 +73,7 @@ internal static class ProxyScriptingJsFuncHelper
"in",
"static",
"with"
};
}.ToFrozenSet();
public static string NormalizeJsVariableName(string name, string additionalChars = "")
{

49
framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs

@ -2,6 +2,8 @@
using System.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
@ -15,15 +17,24 @@ namespace Volo.Abp.Json.Newtonsoft;
public class AbpDateTimeConverter : DateTimeConverterBase, ITransientDependency
{
private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK";
public ILogger<AbpDateTimeConverter> Logger { get; set; }
private readonly DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind;
private readonly CultureInfo _culture = CultureInfo.InvariantCulture;
private readonly IClock _clock;
private readonly AbpJsonOptions _options;
private readonly ICurrentTimezoneProvider _currentTimezoneProvider;
private readonly ITimezoneProvider _timezoneProvider;
private bool _skipDateTimeNormalization;
public AbpDateTimeConverter(IClock clock, IOptions<AbpJsonOptions> options)
public AbpDateTimeConverter(IClock clock, IOptions<AbpJsonOptions> options, ICurrentTimezoneProvider currentTimezoneProvider, ITimezoneProvider timezoneProvider)
{
Logger = NullLogger<AbpDateTimeConverter>.Instance;
_clock = clock;
_currentTimezoneProvider = currentTimezoneProvider;
_timezoneProvider = timezoneProvider;
_options = options.Value;
}
@ -41,19 +52,14 @@ public class AbpDateTimeConverter : DateTimeConverterBase, ITransientDependency
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
var nullable = Nullable.GetUnderlyingType(objectType) != null;
if (reader.TokenType == JsonToken.Null)
switch (reader.TokenType)
{
if (!nullable)
{
case JsonToken.Null when !nullable:
throw new JsonSerializationException($"Cannot convert null value to {objectType.FullName}.");
}
return null;
}
if (reader.TokenType == JsonToken.Date)
{
return Normalize(reader.Value!.To<DateTime>());
case JsonToken.Null:
return null;
case JsonToken.Date:
return Normalize(reader.Value!.To<DateTime>());
}
if (reader.TokenType != JsonToken.String)
@ -108,7 +114,7 @@ public class AbpDateTimeConverter : DateTimeConverterBase, ITransientDependency
}
}
static internal bool ShouldNormalize(MemberInfo member, JsonProperty property)
internal static bool ShouldNormalize(MemberInfo member, JsonProperty property)
{
if (property.PropertyType != typeof(DateTime) &&
property.PropertyType != typeof(DateTime?))
@ -121,6 +127,23 @@ public class AbpDateTimeConverter : DateTimeConverterBase, ITransientDependency
protected virtual DateTime Normalize(DateTime dateTime)
{
if (dateTime.Kind != DateTimeKind.Unspecified ||
!_clock.SupportsMultipleTimezone ||
_currentTimezoneProvider.TimeZone.IsNullOrWhiteSpace())
{
return _skipDateTimeNormalization ? dateTime : _clock.Normalize(dateTime);
}
try
{
var timezoneInfo = _timezoneProvider.GetTimeZoneInfo(_currentTimezoneProvider.TimeZone);
dateTime = new DateTimeOffset(dateTime, timezoneInfo.GetUtcOffset(dateTime)).UtcDateTime;
}
catch
{
Logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", _currentTimezoneProvider.TimeZone);
}
return _skipDateTimeNormalization
? dateTime
: _clock.Normalize(dateTime);

69
framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverter.cs

@ -1,84 +1,43 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Timing;
namespace Volo.Abp.Json.SystemTextJson.JsonConverters;
public class AbpDateTimeConverter : JsonConverter<DateTime>, ITransientDependency
public class AbpDateTimeConverter : AbpDateTimeConverterBase<DateTime>, ITransientDependency
{
private readonly IClock _clock;
private readonly AbpJsonOptions _options;
private bool _skipDateTimeNormalization;
public AbpDateTimeConverter(IClock clock, IOptions<AbpJsonOptions> abpJsonOptions)
public AbpDateTimeConverter(
IClock clock,
IOptions<AbpJsonOptions> abpJsonOptions,
ICurrentTimezoneProvider currentTimezoneProvider,
ITimezoneProvider timezoneProvider)
: base(clock, abpJsonOptions, currentTimezoneProvider, timezoneProvider)
{
_clock = clock;
_options = abpJsonOptions.Value;
}
public virtual AbpDateTimeConverter SkipDateTimeNormalization()
{
_skipDateTimeNormalization = true;
IsSkipDateTimeNormalization = true;
return this;
}
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (_options.InputDateTimeFormats.Any())
{
if (reader.TokenType == JsonTokenType.String)
{
foreach (var format in _options.InputDateTimeFormats)
{
var s = reader.GetString();
if (DateTime.TryParseExact(s, format, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d1))
{
return Normalize(d1);
}
}
}
else
{
throw new JsonException("Reader's TokenType is not String!");
}
}
if (reader.TryGetDateTime(out var d3))
{
return Normalize(d3);
}
var dateText = reader.GetString();
if (!dateText.IsNullOrWhiteSpace())
if (Options.InputDateTimeFormats.Any() && reader.TokenType != JsonTokenType.String)
{
if (DateTime.TryParse(dateText, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d4))
{
return Normalize(d4);
}
throw new JsonException("Reader's TokenType is not String!");
}
throw new JsonException("Can't get datetime from the reader!");
return TryReadDateTime(ref reader, out var result)
? result
: throw new JsonException("Can't get datetime from the reader!");
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
if (_options.OutputDateTimeFormat.IsNullOrWhiteSpace())
{
writer.WriteStringValue(Normalize(value));
}
else
{
writer.WriteStringValue(Normalize(value).ToString(_options.OutputDateTimeFormat, CultureInfo.CurrentUICulture));
}
}
protected virtual DateTime Normalize(DateTime dateTime)
{
return _skipDateTimeNormalization ? dateTime : _clock.Normalize(dateTime);
WriteDateTime(writer, value);
}
}

116
framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverterBase.cs

@ -0,0 +1,116 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.Timing;
namespace Volo.Abp.Json.SystemTextJson.JsonConverters;
public abstract class AbpDateTimeConverterBase<T> : JsonConverter<T>
{
public ILogger<AbpDateTimeConverterBase<T>> Logger { get; set; }
protected IClock Clock { get; }
protected AbpJsonOptions Options { get; }
protected ICurrentTimezoneProvider CurrentTimezoneProvider { get; }
protected ITimezoneProvider TimezoneProvider { get; }
protected bool IsSkipDateTimeNormalization { get; set; }
protected AbpDateTimeConverterBase(
IClock clock,
IOptions<AbpJsonOptions> abpJsonOptions,
ICurrentTimezoneProvider currentTimezoneProvider,
ITimezoneProvider timezoneProvider)
{
Logger = NullLogger<AbpDateTimeConverterBase<T>>.Instance;
Clock = clock;
CurrentTimezoneProvider = currentTimezoneProvider;
TimezoneProvider = timezoneProvider;
Options = abpJsonOptions.Value;
}
protected bool TryReadDateTime(ref Utf8JsonReader reader, out DateTime value)
{
value = default;
if (Options.InputDateTimeFormats.Any())
{
if (reader.TokenType != JsonTokenType.String)
{
return false;
}
var s = reader.GetString();
foreach (var format in Options.InputDateTimeFormats)
{
if (!DateTime.TryParseExact(s, format, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d1))
{
continue;
}
value = Normalize(d1);
return true;
}
}
if (reader.TryGetDateTime(out var d2))
{
value = Normalize(d2);
return true;
}
var dateText = reader.GetString();
if (dateText.IsNullOrWhiteSpace())
{
return false;
}
if (!DateTime.TryParse(dateText, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d3))
{
return false;
}
value = Normalize(d3);
return true;
}
protected void WriteDateTime(Utf8JsonWriter writer, DateTime value)
{
if (Options.OutputDateTimeFormat.IsNullOrWhiteSpace())
{
writer.WriteStringValue(Normalize(value));
}
else
{
writer.WriteStringValue(Normalize(value).ToString(Options.OutputDateTimeFormat, CultureInfo.CurrentUICulture));
}
}
protected virtual DateTime Normalize(DateTime dateTime)
{
if (dateTime.Kind != DateTimeKind.Unspecified ||
!Clock.SupportsMultipleTimezone ||
CurrentTimezoneProvider.TimeZone.IsNullOrWhiteSpace())
{
return IsSkipDateTimeNormalization ? dateTime : Clock.Normalize(dateTime);
}
try
{
var timezoneInfo = TimezoneProvider.GetTimeZoneInfo(CurrentTimezoneProvider.TimeZone);
dateTime = new DateTimeOffset(dateTime, timezoneInfo.GetUtcOffset(dateTime)).UtcDateTime;
}
catch
{
Logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", CurrentTimezoneProvider.TimeZone);
}
return IsSkipDateTimeNormalization ? dateTime : Clock.Normalize(dateTime);
}
}

67
framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpNullableDateTimeConverter.cs

@ -1,65 +1,39 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Timing;
namespace Volo.Abp.Json.SystemTextJson.JsonConverters;
public class AbpNullableDateTimeConverter : JsonConverter<DateTime?>, ITransientDependency
public class AbpNullableDateTimeConverter : AbpDateTimeConverterBase<DateTime?>, ITransientDependency
{
private readonly IClock _clock;
private readonly AbpJsonOptions _options;
private bool _skipDateTimeNormalization;
public AbpNullableDateTimeConverter(IClock clock, IOptions<AbpJsonOptions> abpJsonOptions)
public AbpNullableDateTimeConverter(
IClock clock,
IOptions<AbpJsonOptions> abpJsonOptions,
ICurrentTimezoneProvider currentTimezoneProvider,
ITimezoneProvider timezoneProvider)
: base(clock, abpJsonOptions, currentTimezoneProvider, timezoneProvider)
{
_clock = clock;
_options = abpJsonOptions.Value;
}
public virtual AbpNullableDateTimeConverter SkipDateTimeNormalization()
{
_skipDateTimeNormalization = true;
IsSkipDateTimeNormalization = true;
return this;
}
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (_options.InputDateTimeFormats.Any())
{
if (reader.TokenType == JsonTokenType.String)
{
foreach (var format in _options.InputDateTimeFormats)
{
var s = reader.GetString();
if (DateTime.TryParseExact(s, format, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d1))
{
return Normalize(d1);
}
}
}
else
{
throw new JsonException("Reader's TokenType is not String!");
}
}
if (reader.TryGetDateTime(out var d2))
if (Options.InputDateTimeFormats.Any() && reader.TokenType != JsonTokenType.String)
{
return Normalize(d2);
throw new JsonException("Reader's TokenType is not String!");
}
var dateText = reader.GetString();
if (!dateText.IsNullOrWhiteSpace())
if (TryReadDateTime(ref reader, out var result))
{
if (DateTime.TryParse(dateText, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d3))
{
return Normalize(d3);
}
return result;
}
return null;
@ -70,22 +44,9 @@ public class AbpNullableDateTimeConverter : JsonConverter<DateTime?>, ITransient
if (value == null)
{
writer.WriteNullValue();
return;
}
else
{
if (_options.OutputDateTimeFormat.IsNullOrWhiteSpace())
{
writer.WriteStringValue(Normalize(value.Value));
}
else
{
writer.WriteStringValue(Normalize(value.Value).ToString(_options.OutputDateTimeFormat, CultureInfo.CurrentUICulture));
}
}
}
protected virtual DateTime Normalize(DateTime dateTime)
{
return _skipDateTimeNormalization ? dateTime : _clock.Normalize(dateTime);
WriteDateTime(writer, value.Value);
}
}

2
framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceDictionary.cs

@ -6,7 +6,7 @@ namespace Volo.Abp.Localization;
public class LocalizationResourceDictionary : Dictionary<string, LocalizationResourceBase>
{
private readonly Dictionary<Type, LocalizationResourceBase> _resourcesByTypes = new();
private readonly Dictionary<Type, LocalizationResourceBase> _resourcesByTypes = [];
public LocalizationResource Add<TResouce>(string? defaultCultureName = null)
{

2
framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDatabaseCollection.cs

@ -9,7 +9,7 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb;
public class MemoryDatabaseCollection<TEntity> : IMemoryDatabaseCollection<TEntity>
where TEntity : class, IEntity
{
private readonly Dictionary<string, byte[]> _dictionary = new Dictionary<string, byte[]>();
private readonly Dictionary<string, byte[]> _dictionary = [];
private readonly IMemoryDbSerializer _memoryDbSerializer;

2
framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/MongoModelBuilder.cs

@ -17,7 +17,7 @@ public class MongoModelBuilder : IMongoModelBuilder
{
private readonly Dictionary<Type, object> _entityModelBuilders;
private static readonly object SyncObj = new object();
private static readonly object SyncObj = new();
public MongoModelBuilder()
{

2
framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ParameterRebinder.cs

@ -15,7 +15,7 @@ internal class ParameterRebinder : ExpressionVisitor
internal ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
{
_map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
_map = map ?? [];
}
internal static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map,

39
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController.cs

@ -13,12 +13,24 @@ public class ModelBindingController : AbpController
return input.Kind.ToString().ToLower();
}
[HttpGet("DateTimeKind_WithResult")]
public string DateTimeKind_WithResult(DateTime input)
{
return input.Kind.ToString().ToLower() + "_" + input.ToString("O").ToLower();
}
[HttpGet("NullableDateTimeKind")]
public string NullableDateTimeKind(DateTime? input)
{
return input.Value.Kind.ToString().ToLower();
}
[HttpGet("NullableDateTimeKind_WithResult")]
public string NullableDateTimeKind_WithResult(DateTime? input)
{
return input.Value.Kind.ToString().ToLower() + "_" + input.Value.ToString("O").ToLower();
}
[HttpGet("DisableDateTimeNormalizationDateTimeKind")]
public string DisableDateTimeNormalizationDateTimeKind([DisableDateTimeNormalization] DateTime input)
{
@ -40,6 +52,19 @@ public class ModelBindingController : AbpController
input.InnerModel.Time4.Kind.ToString().ToLower();
}
[HttpGet("ComplexTypeDateTimeKind_WithResult")]
public string ComplexTypeDateTimeKind_WithResult(GetDateTimeKindModel input)
{
return input.Time1.Kind.ToString().ToLower() + "_" +
input.Time1.ToString("O").ToLower() + "_" +
input.Time2.Kind.ToString().ToLower() + "_" +
input.Time2.ToString("O").ToLower() + "_" +
input.Time3.Value.Kind.ToString().ToLower() + "_" +
input.Time3.Value.ToString("O").ToLower() + "_" +
input.InnerModel.Time4.Kind.ToString().ToLower() + "_" +
input.InnerModel.Time4.ToString("O").ToLower();
}
//JSON input and output.
[HttpPost("ComplexTypeDateTimeKind_JSON")]
public string ComplexTypeDateTimeKind_JSON([FromBody] GetDateTimeKindModel input)
@ -50,6 +75,20 @@ public class ModelBindingController : AbpController
input.InnerModel.Time4.Kind.ToString().ToLower();
}
//JSON input and output.
[HttpPost("ComplexTypeDateTimeKind_JSON_WithResult")]
public string ComplexTypeDateTimeKind_JSON_WithResult([FromBody] GetDateTimeKindModel input)
{
return input.Time1.Kind.ToString().ToLower() + "_" +
input.Time1.ToString("O").ToLower() + "_" +
input.Time2.Kind.ToString().ToLower() + "_" +
input.Time2.ToString("O").ToLower() + "_" +
input.Time3.Value.Kind.ToString().ToLower() + "_" +
input.Time3.Value.ToString("O").ToLower() + "_" +
input.InnerModel.Time4.Kind.ToString().ToLower() + "_" +
input.InnerModel.Time4.ToString("O").ToLower();
}
[HttpPost("Guid_Json_Test")]
public GuidJsonModel Guid_Json_Test([FromBody] GuidJsonModel input)
{

104
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs

@ -19,34 +19,79 @@ public abstract class ModelBindingController_Tests : AspNetCoreMvcTestBase
[Fact]
public async Task DateTimeKind_Test()
{
var response = await Client.GetAsync("/api/model-Binding-test/DateTimeKind?input=2010-01-01T00:00:00Z");
var response = await Client.GetAsync("/api/model-Binding-test/DateTimeKind?input=2020-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
resultAsString.ShouldBe(Kind.ToString().ToLower());
}
[Fact]
public async Task DateTimeKind_WithTimezone_Test()
{
var response = await Client.GetAsync("/api/model-Binding-test/DateTimeKind_WithResult?input=2020-01-01T00:00:00&__timezone=Europe/Istanbul");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
var dateTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Unspecified);
switch (Kind)
{
case DateTimeKind.Utc:
dateTime = new DateTime(2019, 12, 31, 21, 0, 0, DateTimeKind.Utc); //Turkey is UTC+3
break;
case DateTimeKind.Local:
dateTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Local);
break;
}
resultAsString.ShouldBe($"{Kind.ToString().ToLower()}_{dateTime.ToString("O").ToLower()}");
}
[Fact]
public async Task NullableDateTimeKind_Test()
{
var response =
await Client.GetAsync("/api/model-Binding-test/NullableDateTimeKind?input=2010-01-01T00:00:00Z");
await Client.GetAsync("/api/model-Binding-test/NullableDateTimeKind?input=2020-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
resultAsString.ShouldBe(Kind.ToString().ToLower());
}
[Fact]
public async Task NullableDateTimeKind_WithTimezone_Test()
{
var response =
await Client.GetAsync("/api/model-Binding-test/NullableDateTimeKind_WithResult?input=2020-01-01T00:00:00&__timezone=Europe/Istanbul");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
var dateTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Unspecified);
switch (Kind)
{
case DateTimeKind.Utc:
dateTime = new DateTime(2019, 12, 31, 21, 0, 0, DateTimeKind.Utc); //Turkey is UTC+3
break;
case DateTimeKind.Local:
dateTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Local);
break;
}
resultAsString.ShouldBe($"{Kind.ToString().ToLower()}_{dateTime.ToString("O").ToLower()}");
}
[Fact]
public async Task DisableDateTimeNormalizationDateTimeKind_Test()
{
var response =
await Client.GetAsync(
"/api/model-Binding-test/DisableDateTimeNormalizationDateTimeKind?input=2010-01-01T00:00:00Z");
"/api/model-Binding-test/DisableDateTimeNormalizationDateTimeKind?input=2020-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
//Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is UTC
//Time parameter(2020-01-01T00:00:00Z) with time zone information, so the default Kind is UTC
//https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50?view=aspnetcore-3.1&tabs=visual-studio#datetime-values-are-model-bound-as-utc-times
resultAsString.ShouldBe(DateTimeKind.Utc.ToString().ToLower());
}
@ -56,11 +101,11 @@ public abstract class ModelBindingController_Tests : AspNetCoreMvcTestBase
{
var response =
await Client.GetAsync(
"/api/model-Binding-test/DisableDateTimeNormalizationNullableDateTimeKind?input=2010-01-01T00:00:00Z");
"/api/model-Binding-test/DisableDateTimeNormalizationNullableDateTimeKind?input=2020-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
//Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is UTC
//Time parameter(2020-01-01T00:00:00Z) with time zone information, so the default Kind is UTC
//https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50?view=aspnetcore-3.1&tabs=visual-studio#datetime-values-are-model-bound-as-utc-times
resultAsString.ShouldBe(DateTimeKind.Utc.ToString().ToLower());
}
@ -69,14 +114,14 @@ public abstract class ModelBindingController_Tests : AspNetCoreMvcTestBase
public async Task ComplexTypeDateTimeKind_Test()
{
var response = await Client.GetAsync("/api/model-Binding-test/ComplexTypeDateTimeKind?" +
"Time1=2010-01-01T00:00:00Z&" +
"Time2=2010-01-01T00:00:00Z&" +
"Time3=2010-01-01T00:00:00Z&" +
"InnerModel.Time4=2010-01-01T00:00:00Z");
"Time1=2020-01-01T00:00:00Z&" +
"Time2=2020-01-01T00:00:00Z&" +
"Time3=2020-01-01T00:00:00Z&" +
"InnerModel.Time4=2020-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
//Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is UTC
//Time parameter(2020-01-01T00:00:00Z) with time zone information, so the default Kind is UTC
//https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50?view=aspnetcore-3.1&tabs=visual-studio#datetime-values-are-model-bound-as-utc-times
resultAsString.ShouldBe($"utc_{Kind.ToString().ToLower()}_{Kind.ToString().ToLower()}_utc");
}
@ -84,7 +129,7 @@ public abstract class ModelBindingController_Tests : AspNetCoreMvcTestBase
[Fact]
public async Task ComplexTypeDateTimeKind_JSON_Test()
{
var time = DateTime.Parse("2010-01-01T00:00:00Z");
var time = DateTime.Parse("2020-01-01T00:00:00Z");
var response = await Client.PostAsync("/api/model-Binding-test/ComplexTypeDateTimeKind_JSON",
new StringContent(JsonSerializer.Serialize(
new GetDateTimeKindModel
@ -104,6 +149,41 @@ public abstract class ModelBindingController_Tests : AspNetCoreMvcTestBase
resultAsString.ShouldBe($"local_{Kind.ToString().ToLower()}_{Kind.ToString().ToLower()}_local");
}
[Fact]
public async Task ComplexTypeDateTimeKind_JSON_WithTimezone_Test()
{
var time = DateTime.Parse("2020-01-01T00:00:00");
var response = await Client.PostAsync("/api/model-Binding-test/ComplexTypeDateTimeKind_JSON_WithResult?__timezone=Europe/Istanbul",
new StringContent(JsonSerializer.Serialize(
new GetDateTimeKindModel
{
Time1 = time,
Time2 = time,
Time3 = time,
InnerModel = new GetDateTimeKindModel.GetDateTimeKindInnerModel
{
Time4 = time
}
}
), Encoding.UTF8, MimeTypes.Application.Json));
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
var dateTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Unspecified);
switch (Kind)
{
case DateTimeKind.Utc:
dateTime = new DateTime(2019, 12, 31, 21, 0, 0, DateTimeKind.Utc); //Turkey is UTC+3
break;
case DateTimeKind.Local:
dateTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Local);
break;
}
resultAsString.ShouldBe($"unspecified_{time.ToString("O").ToLower()}_{Kind.ToString().ToLower()}_{dateTime.ToString("O").ToLower()}_{Kind.ToString().ToLower()}_{dateTime.ToString("O").ToLower()}_unspecified_{time.ToString("O").ToLower()}");
}
[Fact]
public async Task Guid_Json_Test()
{

2
framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs

@ -59,6 +59,8 @@ public class AbpAuditingTestModule : AbpModule
"AppEntityWithValueObject",
type => type == typeof(AppEntityWithValueObject) || type == typeof(AppEntityWithValueObjectAddress))
);
options.EntityHistorySelectors.Add(new NamedTypeSelector(nameof(AppEntityWithJsonProperty), type => type == typeof(AppEntityWithJsonProperty)));
});
context.Services.AddType<Auditing_Tests.MyAuditedObject1>();

27
framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithJsonProperty.cs

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities.Auditing;
namespace Volo.Abp.Auditing.App.Entities;
public class AppEntityWithJsonProperty : FullAuditedAggregateRoot<Guid>
{
public string Name { get; set; }
public JsonPropertyObject Data { get; set; }
public int Count { get; set; }
public AppEntityWithJsonProperty()
{
}
public AppEntityWithJsonProperty(Guid id, string name) : base(id)
{
Name = name;
}
}
public class JsonPropertyObject : Dictionary<string, object>
{
}

21
framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs

@ -30,6 +30,7 @@ public class AbpAuditingTestDbContext : AbpDbContext<AbpAuditingTestDbContext>
public DbSet<AppEntityWithNavigations> AppEntityWithNavigations { get; set; }
public DbSet<AppEntityWithNavigationChildOneToMany> AppEntityWithNavigationChildOneToMany { get; set; }
public DbSet<AppEntityWithNavigationsAndDisableAuditing> AppEntityWithNavigationsAndDisableAuditing { get; set; }
public DbSet<AppEntityWithJsonProperty> EntitiesWithObjectProperty { get; set; }
public AbpAuditingTestDbContext(DbContextOptions<AbpAuditingTestDbContext> options)
: base(options)
@ -56,5 +57,25 @@ public class AbpAuditingTestDbContext : AbpDbContext<AbpAuditingTestDbContext>
b.HasMany(x => x.ManyToMany).WithMany(x => x.ManyToMany).UsingEntity<AppEntityWithNavigationsAndAppEntityWithNavigationChildManyToMany>();
});
modelBuilder.Entity<AppEntityWithJsonProperty>(b =>
{
b.ConfigureByConvention();
b.OwnsOne(x => x.Data, b2 =>
{
b2.ToJson();
b2.Property<object>("Name")
.HasConversion<string>(
v => v.ToString(),
v => v
);
b2.Property<object>("Value")
.HasConversion<string>(
v => v.ToString(),
v => v
);
});
});
}
}

98
framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs

@ -720,6 +720,104 @@ public class Auditing_Tests : AbpAuditingTestBase
x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigationChildManyToMany.ManyToMany) &&
x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List<AppEntityWithNavigations>).FullName));
#pragma warning restore 4014
}
[Fact]
public async Task Should_Write_AuditLog_For_Json_Property_Changes()
{
var entityId = Guid.NewGuid();
var repository = ServiceProvider.GetRequiredService<IBasicRepository<AppEntityWithJsonProperty, Guid>>();
using (var scope = _auditingManager.BeginScope())
{
using (var uow = _unitOfWorkManager.Begin())
{
var entity = new AppEntityWithJsonProperty(entityId, "Test Entity")
{
Data = new JsonPropertyObject()
{
{ "Name", "String Name" },
{ "Value", "String Value"}
},
Count = 10
};
await repository.InsertAsync(entity);
await uow.CompleteAsync();
await scope.SaveAsync();
}
}
#pragma warning disable 4014
AuditingStore.Received().SaveAsync(Arg.Is<AuditLogInfo>(x => x.EntityChanges.Count == 1 &&
x.EntityChanges[0].ChangeType == EntityChangeType.Created &&
x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithJsonProperty).FullName &&
x.EntityChanges[0].PropertyChanges.Count == 4 &&
x.EntityChanges[0].PropertyChanges[0].OriginalValue == null &&
x.EntityChanges[0].PropertyChanges[0].NewValue == "10" &&
x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithJsonProperty.Count) &&
x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(int).FullName &&
x.EntityChanges[0].PropertyChanges[1].OriginalValue == null &&
x.EntityChanges[0].PropertyChanges[1].NewValue == "\"Test Entity\"" &&
x.EntityChanges[0].PropertyChanges[1].PropertyName == nameof(AppEntityWithJsonProperty.Name) &&
x.EntityChanges[0].PropertyChanges[1].PropertyTypeFullName == typeof(string).FullName &&
x.EntityChanges[0].PropertyChanges[2].OriginalValue == null &&
x.EntityChanges[0].PropertyChanges[2].NewValue == "\"String Name\"" &&
x.EntityChanges[0].PropertyChanges[2].PropertyName == "Data.Name" &&
x.EntityChanges[0].PropertyChanges[2].PropertyTypeFullName == typeof(string).FullName &&
x.EntityChanges[0].PropertyChanges[3].OriginalValue == null &&
x.EntityChanges[0].PropertyChanges[3].NewValue == "\"String Value\"" &&
x.EntityChanges[0].PropertyChanges[3].PropertyName == "Data.Value" &&
x.EntityChanges[0].PropertyChanges[3].PropertyTypeFullName == typeof(string).FullName));
AuditingStore.ClearReceivedCalls();
#pragma warning restore 4014
using (var scope = _auditingManager.BeginScope())
{
using (var uow = _unitOfWorkManager.Begin())
{
var entity = await repository.GetAsync(entityId);
entity.Name = "Updated Test Entity";
entity.Data["Name"] = "Updated String Name";
entity.Data["Value"] = "Updated String Value";
await repository.UpdateAsync(entity);
await uow.CompleteAsync();
await scope.SaveAsync();
}
}
#pragma warning disable 4014
AuditingStore.Received().SaveAsync(Arg.Is<AuditLogInfo>(x => x.EntityChanges.Count == 1 &&
x.EntityChanges[0].ChangeType == EntityChangeType.Updated &&
x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithJsonProperty).FullName &&
x.EntityChanges[0].PropertyChanges.Count == 3 &&
x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"Test Entity\"" &&
x.EntityChanges[0].PropertyChanges[0].NewValue == "\"Updated Test Entity\"" &&
x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithJsonProperty.Name) &&
x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(string).FullName &&
x.EntityChanges[0].PropertyChanges[1].OriginalValue == "\"String Name\"" &&
x.EntityChanges[0].PropertyChanges[1].NewValue == "\"Updated String Name\"" &&
x.EntityChanges[0].PropertyChanges[1].PropertyName == "Data.Name" &&
x.EntityChanges[0].PropertyChanges[1].PropertyTypeFullName == typeof(string).FullName &&
x.EntityChanges[0].PropertyChanges[2].OriginalValue == "\"String Value\"" &&
x.EntityChanges[0].PropertyChanges[2].NewValue == "\"Updated String Value\"" &&
x.EntityChanges[0].PropertyChanges[2].PropertyName == "Data.Value" &&
x.EntityChanges[0].PropertyChanges[2].PropertyTypeFullName == typeof(string).FullName));
AuditingStore.ClearReceivedCalls();
#pragma warning restore 4014
}
}

165
framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs

@ -1,37 +1,38 @@
using System;
using System.Collections.Generic;
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.AutoMapper;
using Volo.Abp.Localization;
using Volo.Abp.MultiLingualObjects.TestObjects;
using Volo.Abp.Testing;
using Xunit;
namespace Volo.Abp.MultiLingualObjects;
public class MultiLingualObjectManager_Tests : AbpIntegratedTest<AbpMultiLingualObjectsTestModule>
{
private readonly IMultiLingualObjectManager _multiLingualObjectManager;
private readonly MultiLingualBook _book;
private readonly List<MultiLingualBook> _books;
private readonly IMapperAccessor _mapperAccessor;
private readonly Dictionary<string, string> _testTranslations = new()
{
["ar"] = "C# التعمق في",
["zh-Hans"] = "深入理解C#",
["en"] = "C# in Depth"
};
using Volo.Abp.Localization;
using Volo.Abp.MultiLingualObjects.TestObjects;
using Volo.Abp.Testing;
using Xunit;
namespace Volo.Abp.MultiLingualObjects;
public class MultiLingualObjectManager_Tests : AbpIntegratedTest<AbpMultiLingualObjectsTestModule>
{
private readonly IMultiLingualObjectManager _multiLingualObjectManager;
private readonly MultiLingualBook _book;
private readonly List<MultiLingualBook> _books;
private readonly IMapperAccessor _mapperAccessor;
private readonly FrozenDictionary<string, string> _testTranslations = new Dictionary<string, string>
{
["ar"] = "C# التعمق في",
["zh-Hans"] = "深入理解C#",
["en"] = "C# in Depth"
}.ToFrozenDictionary();
public MultiLingualObjectManager_Tests()
{
public MultiLingualObjectManager_Tests()
{
_multiLingualObjectManager = ServiceProvider.GetRequiredService<IMultiLingualObjectManager>();
//Single Lookup
_book = GetTestBook("en", "zh-Hans");
//Bulk lookup
_book = GetTestBook("en", "zh-Hans");
//Bulk lookup
_books = new List<MultiLingualBook>
{
//has no translations
@ -45,14 +46,14 @@ public class MultiLingualObjectManager_Tests : AbpIntegratedTest<AbpMultiLingual
//arabic + english + chineese
GetTestBook("en", "ar", "zh-Hans")
};
_mapperAccessor = ServiceProvider.GetRequiredService<IMapperAccessor>();
_mapperAccessor = ServiceProvider.GetRequiredService<IMapperAccessor>();
}
MultiLingualBook GetTestBook(params string[] included)
{
var id = Guid.NewGuid();
//Single book
var res = new MultiLingualBook(id, 100);
var id = Guid.NewGuid();
//Single book
var res = new MultiLingualBook(id, 100);
foreach (var language in included)
{
res.Translations.Add(new MultiLingualBookTranslation
@ -65,45 +66,45 @@ public class MultiLingualObjectManager_Tests : AbpIntegratedTest<AbpMultiLingual
return res;
}
[Fact]
public async Task GetTranslationAsync()
{
using (CultureHelper.Use("en-us"))
{
var translation = await _multiLingualObjectManager.GetTranslationAsync<MultiLingualBook, MultiLingualBookTranslation>(_book);
translation.ShouldNotBeNull();
translation.Name.ShouldBe(_testTranslations["en"]);
}
}
[Fact]
public async Task GetTranslationFromListAsync()
{
using (CultureHelper.Use("en-us"))
{
var translation = await _multiLingualObjectManager.GetTranslationAsync(_book.Translations);
translation.ShouldNotBeNull();
translation.Name.ShouldBe(_testTranslations["en"]);
}
}
[Fact]
public async Task Should_Get_Specified_Language()
{
using (CultureHelper.Use("zh-Hans"))
{
var translation = await _multiLingualObjectManager.GetTranslationAsync<MultiLingualBook, MultiLingualBookTranslation>(_book, culture: "en");
translation.ShouldNotBeNull();
translation.Name.ShouldBe(_testTranslations["en"]);
}
[Fact]
public async Task GetTranslationAsync()
{
using (CultureHelper.Use("en-us"))
{
var translation = await _multiLingualObjectManager.GetTranslationAsync<MultiLingualBook, MultiLingualBookTranslation>(_book);
translation.ShouldNotBeNull();
translation.Name.ShouldBe(_testTranslations["en"]);
}
}
[Fact]
public async Task GetTranslationFromListAsync()
{
using (CultureHelper.Use("en-us"))
{
var translation = await _multiLingualObjectManager.GetTranslationAsync(_book.Translations);
translation.ShouldNotBeNull();
translation.Name.ShouldBe(_testTranslations["en"]);
}
}
[Fact]
public async Task Should_Get_Specified_Language()
{
using (CultureHelper.Use("zh-Hans"))
{
var translation = await _multiLingualObjectManager.GetTranslationAsync<MultiLingualBook, MultiLingualBookTranslation>(_book, culture: "en");
translation.ShouldNotBeNull();
translation.Name.ShouldBe(_testTranslations["en"]);
}
}
[Fact]
public async Task GetBulkTranslationsAsync()
{
using (CultureHelper.Use("en-us"))
{
[Fact]
public async Task GetBulkTranslationsAsync()
{
using (CultureHelper.Use("en-us"))
{
var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync<MultiLingualBook, MultiLingualBookTranslation>(_books);
foreach (var (entity, translation) in translations)
{
@ -117,26 +118,26 @@ public class MultiLingualObjectManager_Tests : AbpIntegratedTest<AbpMultiLingual
translation.ShouldBeNull();
}
}
}
}
[Fact]
public async Task GetBulkTranslationsFromListAsync()
}
}
[Fact]
public async Task GetBulkTranslationsFromListAsync()
{
using (CultureHelper.Use("en-us"))
using (CultureHelper.Use("en-us"))
{
var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync(_books.Select(x => x.Translations));
foreach (var translation in translations)
{
translation?.Name.ShouldBe(_testTranslations["en"]);
}
}
}
[Fact]
}
}
[Fact]
public async Task TestBulkMapping()
{
using (CultureHelper.Use("en-us"))
using (CultureHelper.Use("en-us"))
{
var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync<MultiLingualBook, MultiLingualBookTranslation>(_books);
var translationsDict = translations.ToDictionary(x => x.entity.Id, x => x.translation);
@ -152,5 +153,5 @@ public class MultiLingualObjectManager_Tests : AbpIntegratedTest<AbpMultiLingual
Assert.Equal(og.Translations.FirstOrDefault(x => x.Language == "en")?.Name, m.Name);
}
}
}
}
}
}

9
latest-versions.json

@ -1,4 +1,13 @@
[
{
"version": "10.0.2",
"releaseDate": "",
"type": "stable",
"message": "",
"leptonx": {
"version": "5.0.2"
}
},
{
"version": "9.3.7",
"releaseDate": "",

8
modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json

@ -34,14 +34,14 @@
"DisplayName:PhoneNumber": "Telefon numarası",
"PersonalSettings": "Kişisel ayarlar",
"PersonalSettingsSaved": "Kişisel ayarlar kaydedildi",
"PersonalSettingsChangedConfirmationModalTitle": "Personal info changed",
"PersonalSettingsChangedConfirmationModalDescription": "If you want to apply these changes, you have to login. Do you want to log out?",
"PersonalSettingsChangedConfirmationModalTitle": "Kişisel bilgiler değişti",
"PersonalSettingsChangedConfirmationModalDescription": "Değişikliklerin yansıtılması için tekrar giriş yapmanız gerekmektedir. Şimdi oturumu kapatmak istiyor musunuz?",
"PasswordChanged": "Şifre değiştirildi",
"NewPasswordConfirmFailed": "Lütfen yeni şifreyi onaylayın.",
"NewPasswordSameAsOld": "Yeni şifre eski şifre ile aynı olamaz.",
"Manage": "Manage",
"Manage": "Yönet",
"MyAccount": "Hesabım",
"DisplayName:Abp.Account.IsSelfRegistrationEnabled": "self-registration etkin mi ?",
"DisplayName:Abp.Account.IsSelfRegistrationEnabled": "Kendi kendine kayıt etkin mi?",
"Description:Abp.Account.IsSelfRegistrationEnabled": "Bir kullanıcının hesabı kendisi tarafından kaydedip kaydedememesidir.",
"DisplayName:Abp.Account.EnableLocalLogin": "Yerel bir hesapla kimlik doğrulaması",
"Description:Abp.Account.EnableLocalLogin": "Sunucunun, kullanıcıların yerel bir hesapla kimlik doğrulamasına izin verip vermeyeceğini belirtir.",

5
modules/docs/app/VoloDocs.Web/Pages/Error.cshtml.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Net;
using Microsoft.AspNetCore.Diagnostics;
@ -48,7 +49,7 @@ namespace VoloDocs.Web.Pages
#region Error Messages
/*For more ASCII arts http://patorjk.com/software/taag/#p=display&h=0&f=Big&t=400*/
private readonly Dictionary<int, string> _errorMessages = new Dictionary<int, string>
private readonly FrozenDictionary<int, string> _errorMessages = new Dictionary<int, string>
{
{
400, @"
@ -131,7 +132,7 @@ Ooops! Our server is experiencing a mild case of the hiccups."
Looks like we're having some server issues."
}
};
}.ToFrozenDictionary();
#endregion
}
}

24
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/UserRoleFinder.cs

@ -35,10 +35,10 @@ public class UserRoleFinder : IUserRoleFinder, ITransientDependency
{
page = page < 1 ? 1 : page;
var users = await IdentityUserRepository.GetListAsync(filter: filter, skipCount: (page - 1) * 10, maxResultCount: 10);
return users.Select(user => new UserFinderResult
return users.Select(x => new UserFinderResult
{
Id = user.Id,
UserName = user.UserName
Id = x.Id,
UserName = x.UserName
}).ToList();
}
}
@ -49,10 +49,10 @@ public class UserRoleFinder : IUserRoleFinder, ITransientDependency
{
page = page < 1 ? 1 : page;
var roles = await IdentityRoleRepository.GetListAsync(filter: filter, skipCount: (page - 1) * 10, maxResultCount: 10);
return roles.Select(user => new RoleFinderResult
return roles.Select(x => new RoleFinderResult
{
Id = user.Id,
RoleName = user.Name
Id = x.Id,
RoleName = x.Name
}).ToList();
}
}
@ -62,10 +62,10 @@ public class UserRoleFinder : IUserRoleFinder, ITransientDependency
using (IdentityUserRepository.DisableTracking())
{
var users = await IdentityUserRepository.GetListByIdsAsync(ids);
return users.Select(user => new UserFinderResult
return users.Select(x => new UserFinderResult
{
Id = user.Id,
UserName = user.UserName
Id = x.Id,
UserName = x.UserName
}).ToList();
}
}
@ -75,10 +75,10 @@ public class UserRoleFinder : IUserRoleFinder, ITransientDependency
using (IdentityUserRepository.DisableTracking())
{
var roles = await IdentityRoleRepository.GetListAsync(names);
return roles.Select(user => new RoleFinderResult
return roles.Select(x => new RoleFinderResult
{
Id = user.Id,
RoleName = user.Name
Id = x.Id,
RoleName = x.Name
}).ToList();
}
}

6
modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/RoleResourcePermissionProviderKeyLookupService.cs

@ -30,9 +30,9 @@ public class RoleResourcePermissionProviderKeyLookupService : IResourcePermissio
return roles.Select(r => new ResourcePermissionProviderKeyInfo(r.RoleName, r.RoleName)).ToList();
}
public virtual async Task<List<ResourcePermissionProviderKeyInfo>> SearchAsync(string[] keys, CancellationToken cancellationToken = default)
public virtual Task<List<ResourcePermissionProviderKeyInfo>> SearchAsync(string[] keys, CancellationToken cancellationToken = default)
{
var roles = await UserRoleFinder.SearchRoleByNamesAsync(keys.Distinct().ToArray());
return roles.Select(r => new ResourcePermissionProviderKeyInfo(r.RoleName, r.RoleName)).ToList();
// Keys are role names
return Task.FromResult(keys.Select(x => new ResourcePermissionProviderKeyInfo(x, x)).ToList());
}
}

10
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientFinderResult.cs

@ -0,0 +1,10 @@
using System;
namespace Volo.Abp.IdentityServer.Clients;
public class ClientFinderResult
{
public Guid Id { get; set; }
public string ClientId { get; set; }
}

9
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/IClientFinder.cs

@ -0,0 +1,9 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Volo.Abp.IdentityServer.Clients;
public interface IClientFinder
{
Task<List<ClientFinderResult>> SearchAsync(string filter, int page = 1);
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/FR.json

@ -11,6 +11,7 @@
"InvalidUsername": "Nom d'utilisateur ou mot de passe invalide!",
"InvalidAuthenticatorCode": "Code d'authentification invalide !",
"InvalidRecoveryCode": "Code de récupération invalide !",
"TheTargetUserIsNotLinkedToYou": "L'utilisateur cible n'est pas lié à vous!"
"TheTargetUserIsNotLinkedToYou": "L'utilisateur cible n'est pas lié à vous!",
"ClientResourcePermissionProviderKeyLookupService": "Client"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ar.json

@ -11,6 +11,7 @@
"InvalidUsername": "اسم المستخدم أو كلمة المرور غير صالحة!",
"InvalidAuthenticatorCode": "كود المصدق غير صالح!",
"InvalidRecoveryCode": "رمز الاسترداد غير صالح!",
"TheTargetUserIsNotLinkedToYou": "المستخدم المستهدف غير مرتبط بك!"
"TheTargetUserIsNotLinkedToYou": "المستخدم المستهدف غير مرتبط بك!",
"ClientResourcePermissionProviderKeyLookupService": "العميل"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/cs.json

@ -11,6 +11,7 @@
"InvalidUsername": "Neplatné uživatelské jméno či heslo!",
"InvalidAuthenticatorCode": "Neplatný ověřovací kód!",
"InvalidRecoveryCode": "Neplatný kód pro obnovení!",
"TheTargetUserIsNotLinkedToYou": "Cílový uživatel s vámi není spojen!"
"TheTargetUserIsNotLinkedToYou": "Cílový uživatel s vámi není spojen!",
"ClientResourcePermissionProviderKeyLookupService": "Klient"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/de.json

@ -11,6 +11,7 @@
"InvalidUsername": "Ungültiger Benutzername oder Passwort!",
"InvalidAuthenticatorCode": "Ungültiger Authentifizierungscode!",
"InvalidRecoveryCode": "Ungültiger Wiederherstellungscode!",
"TheTargetUserIsNotLinkedToYou": "Der Zielbenutzer ist nicht mit Ihnen verknüpft!"
"TheTargetUserIsNotLinkedToYou": "Der Zielbenutzer ist nicht mit Ihnen verknüpft!",
"ClientResourcePermissionProviderKeyLookupService": "Client"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/el.json

@ -10,6 +10,7 @@
"LoginIsNotAllowed": "Δεν επιτρέπεται να συνδεθείτε! Ο λογαριασμός σας είναι ανενεργός ή χρειάζεται να επιβεβαιώσετε το email/τον αριθμό τηλεφώνου σας.",
"InvalidUsername": "Μη έγκυρο όνομα ή κωδικός!",
"InvalidAuthenticatorCode": "Μη έγκυρος κωδικός ελέγχου ταυτότητας!",
"TheTargetUserIsNotLinkedToYou": "Ο χρήστης-στόχος δεν είναι συνδεδεμένος με εσάς!"
"TheTargetUserIsNotLinkedToYou": "Ο χρήστης-στόχος δεν είναι συνδεδεμένος με εσάς!",
"ClientResourcePermissionProviderKeyLookupService": "Πελάτης"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en-GB.json

@ -9,6 +9,7 @@
"InvalidUserNameOrPassword": "Invalid username or password!",
"LoginIsNotAllowed": "You are not allowed to login! Your account is inactive or needs to confirm your email/phone number.",
"InvalidUsername": "Invalid username or password!",
"TheTargetUserIsNotLinkedToYou": "The target user is not linked to you!"
"TheTargetUserIsNotLinkedToYou": "The target user is not linked to you!",
"ClientResourcePermissionProviderKeyLookupService": "Client"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en.json

@ -11,6 +11,7 @@
"InvalidUsername": "Invalid username or password!",
"InvalidAuthenticatorCode": "Invalid authenticator code!",
"InvalidRecoveryCode": "Invalid recovery code!",
"TheTargetUserIsNotLinkedToYou": "The target user is not linked to you!"
"TheTargetUserIsNotLinkedToYou": "The target user is not linked to you!",
"ClientResourcePermissionProviderKeyLookupService": "Client"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/es.json

@ -11,6 +11,7 @@
"InvalidUsername": "Nombre de usuario icorrecto",
"InvalidAuthenticatorCode": "¡Código de autenticador no válido!",
"InvalidRecoveryCode": "¡Código de recuperación no válido!",
"TheTargetUserIsNotLinkedToYou": "El usuario de destino no está asociado a usted."
"TheTargetUserIsNotLinkedToYou": "El usuario de destino no está asociado a usted.",
"ClientResourcePermissionProviderKeyLookupService": "Cliente"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fa.json

@ -11,6 +11,7 @@
"InvalidUsername": "نام کاربری یا رمز عبور نامعتبر!",
"InvalidAuthenticatorCode": "کد احراز هویت نامعتبر!",
"InvalidRecoveryCode": "کد بازیابی نامعتبر!",
"TheTargetUserIsNotLinkedToYou": "کاربر هدف به شما پیوند داده نشده است!"
"TheTargetUserIsNotLinkedToYou": "کاربر هدف به شما پیوند داده نشده است!",
"ClientResourcePermissionProviderKeyLookupService": "کلاینت"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fi.json

@ -11,6 +11,7 @@
"InvalidUsername": "Väärä käyttäjänimi tai salasana!",
"InvalidAuthenticatorCode": "Virheellinen todennuskoodi!",
"InvalidRecoveryCode": "Virheellinen palautuskoodi!",
"TheTargetUserIsNotLinkedToYou": "Kohdekäyttäjä ei ole linkitetty sinuun!"
"TheTargetUserIsNotLinkedToYou": "Kohdekäyttäjä ei ole linkitetty sinuun!",
"ClientResourcePermissionProviderKeyLookupService": "Asiakas"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hi.json

@ -11,6 +11,7 @@
"InvalidUsername": "अमान्य उपयोगकर्ता नाम या पासवर्ड!",
"InvalidAuthenticatorCode": "अमान्य प्रमाणक कोड!",
"InvalidRecoveryCode": "अमान्य पुनर्प्राप्ति कोड!",
"TheTargetUserIsNotLinkedToYou": "लक्ष्य उपयोगकर्ता आपसे जुड़ा नहीं है!"
"TheTargetUserIsNotLinkedToYou": "लक्ष्य उपयोगकर्ता आपसे जुड़ा नहीं है!",
"ClientResourcePermissionProviderKeyLookupService": "क्लाइंट"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hr.json

@ -11,6 +11,7 @@
"InvalidUsername": "Neispravno korisničko ime ili lozinka!",
"InvalidAuthenticatorCode": "Nevažeći kod autentifikatora!",
"InvalidRecoveryCode": "Nevažeći kod za oporavak!",
"TheTargetUserIsNotLinkedToYou": "Ciljani korisnik nije povezan s vama!"
"TheTargetUserIsNotLinkedToYou": "Ciljani korisnik nije povezan s vama!",
"ClientResourcePermissionProviderKeyLookupService": "Klijent"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hu.json

@ -11,6 +11,7 @@
"InvalidUsername": "Érvénytelen felhasználónév vagy jelszó!",
"InvalidAuthenticatorCode": "Érvénytelen hitelesítő kód!",
"InvalidRecoveryCode": "Érvénytelen helyreállítási kód!",
"TheTargetUserIsNotLinkedToYou": "A célfelhasználó nincs hozzád kapcsolódva!"
"TheTargetUserIsNotLinkedToYou": "A célfelhasználó nincs hozzád kapcsolódva!",
"ClientResourcePermissionProviderKeyLookupService": "Kliens"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/is.json

@ -11,6 +11,7 @@
"InvalidUsername": "Ógilt notendanafn eða lykilorð!",
"InvalidAuthenticatorCode": "Ógildur auðkenningarkóði!",
"InvalidRecoveryCode": "Ógildur endurheimtarkóði!",
"TheTargetUserIsNotLinkedToYou": "Marknotandinn er ekki tengdur þér!"
"TheTargetUserIsNotLinkedToYou": "Marknotandinn er ekki tengdur þér!",
"ClientResourcePermissionProviderKeyLookupService": "Biðlari"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/it.json

@ -11,6 +11,7 @@
"InvalidUsername": "Username o password non validi!",
"InvalidAuthenticatorCode": "Codice autenticatore non valido!",
"InvalidRecoveryCode": "Codice di ripristino non valido!",
"TheTargetUserIsNotLinkedToYou": "L'utente indicato non è collegato a te!"
"TheTargetUserIsNotLinkedToYou": "L'utente indicato non è collegato a te!",
"ClientResourcePermissionProviderKeyLookupService": "Client"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/nl.json

@ -11,6 +11,7 @@
"InvalidUsername": "Ongeldige gebruikersnaam of wachtwoord!",
"InvalidAuthenticatorCode": "Ongeldige authenticatiecode!",
"InvalidRecoveryCode": "Ongeldige herstelcode!",
"TheTargetUserIsNotLinkedToYou": "De beoogde gebruiker is niet aan jou gekoppeld!"
"TheTargetUserIsNotLinkedToYou": "De beoogde gebruiker is niet aan jou gekoppeld!",
"ClientResourcePermissionProviderKeyLookupService": "Client"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pl-PL.json

@ -11,6 +11,7 @@
"InvalidUsername": "Nieprawidłowa nazwa użytkownika lub hasło!",
"InvalidAuthenticatorCode": "Nieprawidłowy kod uwierzytelniający!",
"InvalidRecoveryCode": "Nieprawidłowy kod odzyskiwania!",
"TheTargetUserIsNotLinkedToYou": "Docelowy użytkownik nie jest z Tobą powiązany!"
"TheTargetUserIsNotLinkedToYou": "Docelowy użytkownik nie jest z Tobą powiązany!",
"ClientResourcePermissionProviderKeyLookupService": "Klient"
}
}

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pt-BR.json

@ -11,6 +11,7 @@
"InvalidUsername": "Nome de usuário ou senha inválidos!",
"InvalidAuthenticatorCode": "Código de autenticador inválido!",
"InvalidRecoveryCode": "Código de recuperação inválido!",
"TheTargetUserIsNotLinkedToYou": "O usuário-alvo não está vinculado a você!"
"TheTargetUserIsNotLinkedToYou": "O usuário-alvo não está vinculado a você!",
"ClientResourcePermissionProviderKeyLookupService": "Cliente"
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save