diff --git a/Directory.Packages.props b/Directory.Packages.props index 45b24e171e..e28e2e49c5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -121,7 +121,7 @@ - + diff --git a/ai-rules/README.md b/ai-rules/README.md new file mode 100644 index 0000000000..ed9370b591 --- /dev/null +++ b/ai-rules/README.md @@ -0,0 +1,151 @@ +# ABP AI Rules + +This folder contains AI rules (Cursor `.mdc` format) for ABP based solutions. These rules help AI assistants understand ABP-specific patterns, conventions, and best practices when working with ABP-based applications. + +## Purpose + +This folder serves as a central repository for ABP-specific AI rules. The community can contribute, improve, and maintain these rules collaboratively. + +When you create a new ABP solution, these rules are included in your project based on your configuration. This provides AI assistants with ABP-specific context, helping them generate code that follows ABP conventions. + +> **Important**: These rules are ABP-specific. They don't cover general .NET or ASP.NET Core patterns—AI assistants already know those. Instead, they focus on ABP's unique architecture, module system, and conventions. + +## How Rules Work + +Large language models don't retain memory between completions. Rules provide persistent, reusable context at the prompt level. + +When applied, rule contents are included at the start of the model context. This gives the AI consistent guidance for generating code, interpreting edits, or helping with workflows. + +## Mini Glossary (ABP Terms) + +- **Application service**: Use-case orchestration (ABP’s primary “business API” surface). Usually exposed remotely via Auto API Controllers or explicit controllers. +- **Auto API Controllers**: ABP can auto-generate HTTP endpoints from `IApplicationService` contracts. +- **Client proxy**: Generated client-side code (Angular/JS/C#) to call remote application services. +- **Integration service (microservices)**: Application-service-like contract intended for **service-to-service** communication; typically exposed separately and consumed via generated C# proxies. +- **Domain vs Application**: Domain holds business rules/invariants; Application coordinates domain + infrastructure and returns DTOs. + +## File Structure + +``` +ai-rules/ +├── README.md +├── common/ # Rules for all ABP projects +│ ├── abp-core.mdc # Core ABP conventions (alwaysApply: true) +│ ├── ddd-patterns.mdc # DDD patterns (Entity, AggregateRoot, Repository) +│ ├── application-layer.mdc # Application services, DTOs, validation +│ ├── authorization.mdc # Permissions and authorization +│ ├── multi-tenancy.mdc # Multi-tenant entities and data isolation +│ ├── infrastructure.mdc # Settings, Features, Caching, Events, Jobs +│ ├── dependency-rules.mdc # Layer dependencies and guardrails +│ ├── development-flow.mdc # Development workflow +│ └── cli-commands.mdc # ABP CLI commands reference +├── ui/ # UI-specific rules (applied by globs) +│ ├── blazor.mdc # Blazor UI patterns +│ ├── angular.mdc # Angular UI patterns +│ └── mvc.mdc # MVC/Razor Pages patterns +├── data/ # Data layer rules (applied by globs) +│ ├── ef-core.mdc # Entity Framework Core patterns +│ └── mongodb.mdc # MongoDB patterns +├── testing/ # Testing rules +│ └── patterns.mdc # Unit and integration test patterns +└── template-specific/ # Template-specific rules + ├── app-nolayers.mdc # Single-layer app template + ├── module.mdc # Module template + └── microservice.mdc # Microservice template +``` + +### Rule Format + +Each rule is a markdown file with frontmatter metadata: + +```markdown +--- +description: "Describes when this rule should apply - used by AI to decide relevance" +globs: "src/**/*.cs" +alwaysApply: false +--- + +# Rule Title + +Your rule content here... +``` + +### Frontmatter Properties + +| Property | Description | +|----------|-------------| +| `description` | Brief description of what the rule covers. Used by AI to determine relevance. | +| `globs` | File patterns that trigger this rule (e.g., `**/*.cs`, `*.Domain/**`). | +| `alwaysApply` | If `true`, rule is always included. If `false`, AI decides based on context. | + +### Rule Types + +| Type | When Applied | +|------|--------------| +| **Always Apply** | Every chat session (`alwaysApply: true`) | +| **Apply Intelligently** | When AI decides it's relevant based on `description` | +| **Apply to Specific Files** | When file matches `globs` pattern | +| **Apply Manually** | When @-mentioned in chat (e.g., `@my-rule`) | + +## Rule Categories + +### Common Rules +Core ABP patterns that apply to all DDD-based templates (app, module, microservice): +- `abp-core.mdc` - Always applied, covers module system, DI conventions, base classes +- `ddd-patterns.mdc` - Entity, AggregateRoot, Repository, Domain Services +- `application-layer.mdc` - Application services, DTOs, validation, error handling +- `authorization.mdc` - Permission system and authorization +- `infrastructure.mdc` - Settings, Features, Caching, Events, Background Jobs +- `dependency-rules.mdc` - Layer dependencies and project structure +- `development-flow.mdc` - Development workflow for adding features + +### UI Rules (Applied by Globs) +- `blazor.mdc` - Applied to `**/*.razor`, `**/Blazor/**/*.cs` +- `angular.mdc` - Applied to `**/angular/**/*.ts` +- `mvc.mdc` - Applied to `**/*.cshtml`, `**/Pages/**/*.cs` + +### Data Rules (Applied by Globs) +- `ef-core.mdc` - Applied to `**/*.EntityFrameworkCore/**/*.cs` +- `mongodb.mdc` - Applied to `**/*.MongoDB/**/*.cs` + +### Template-Specific Rules +- `app-nolayers.mdc` - For single-layer web application template +- `module.mdc` - For reusable module template +- `microservice.mdc` - For microservice template + +## Best Practices + +Good rules are focused, actionable, and scoped: + +- **Keep rules under 500 lines** - Split large rules into multiple, composable rules +- **Provide concrete examples** - Reference actual files or include code snippets +- **Be specific, not vague** - Write rules like clear internal documentation +- **Reference files instead of copying** - This keeps rules short and prevents staleness +- **Start simple** - Add rules only when you notice AI making the same mistake repeatedly + +## What to Avoid + +- **Copying entire style guides**: Use a linter instead. AI already knows common style conventions. +- **Documenting every possible command**: AI knows common tools like `dotnet` and `npm`. +- **Adding instructions for edge cases that rarely apply**: Keep rules focused on patterns you use frequently. +- **Duplicating what's already in your codebase**: Point to canonical examples instead of copying code. +- **Including non-ABP patterns**: Don't add generic .NET/ASP.NET Core guidance—focus on ABP-specific conventions. + +## Contributing + +We welcome community contributions to improve these rules! You can open a PR to add new rules or improve existing ones. + +Please review our [Contribution Guide](../CONTRIBUTING.md) and [Code of Conduct](../CODE_OF_CONDUCT.md) before contributing. + +### Contribution Guidelines + +- Each rule should focus on a single ABP concept or pattern +- Use clear, actionable language +- Include examples where helpful +- Test your rules by using them in a real ABP project +- Keep ABP-specific focus—don't add general .NET patterns + +## Related Resources + +- [Cursor Rules Documentation](https://cursor.com/docs/context/rules) +- [ABP Framework Documentation](https://abp.io/docs) diff --git a/ai-rules/common/abp-core.mdc b/ai-rules/common/abp-core.mdc new file mode 100644 index 0000000000..673c4199e6 --- /dev/null +++ b/ai-rules/common/abp-core.mdc @@ -0,0 +1,182 @@ +--- +description: "Core ABP Framework conventions - module system, dependency injection, and base classes" +alwaysApply: true +--- + +# ABP Core Conventions + +> **Documentation**: https://abp.io/docs/latest +> **API Reference**: https://abp.io/docs/api/ + +## Module System +Every ABP application/module has a module class that configures services: + +```csharp +[DependsOn( + typeof(AbpDddDomainModule), + typeof(AbpEntityFrameworkCoreModule) +)] +public class MyAppModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Service registration and configuration + } +} +``` + +> **Note**: Middleware configuration (`OnApplicationInitialization`) should only be done in the final host application, not in reusable modules. + +## Dependency Injection Conventions + +### Automatic Registration +ABP automatically registers services implementing marker interfaces: +- `ITransientDependency` → Transient lifetime +- `ISingletonDependency` → Singleton lifetime +- `IScopedDependency` → Scoped lifetime + +Classes inheriting from `ApplicationService`, `DomainService`, `AbpController` are also auto-registered. + +### Repository Usage +You can use the generic `IRepository` for simple CRUD operations. Define custom repository interfaces only when you need custom query methods: + +```csharp +// Simple CRUD - Generic repository is fine +public class BookAppService : ApplicationService +{ + private readonly IRepository _bookRepository; // ✅ OK for simple operations +} + +// Custom queries needed - Define custom interface +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); // Custom query +} + +public class BookAppService : ApplicationService +{ + private readonly IBookRepository _bookRepository; // ✅ Use custom when needed +} +``` + +### Exposing Services +```csharp +[ExposeServices(typeof(IMyService))] +public class MyService : IMyService, ITransientDependency { } +``` + +## Important Base Classes + +| Base Class | Purpose | +|------------|---------| +| `Entity` | Basic entity with ID | +| `AggregateRoot` | DDD aggregate root | +| `DomainService` | Domain business logic | +| `ApplicationService` | Use case orchestration | +| `AbpController` | REST API controller | + +ABP base classes already inject commonly used services as properties. Before injecting a service, check if it's already available: + +| Property | Available In | Description | +|----------|--------------|-------------| +| `GuidGenerator` | All base classes | Generate GUIDs | +| `Clock` | All base classes | Current time (use instead of `DateTime`) | +| `CurrentUser` | All base classes | Authenticated user info | +| `CurrentTenant` | All base classes | Multi-tenancy context | +| `L` (StringLocalizer) | `ApplicationService`, `AbpController` | Localization | +| `AuthorizationService` | `ApplicationService`, `AbpController` | Permission checks | +| `FeatureChecker` | `ApplicationService`, `AbpController` | Feature availability | +| `DataFilter` | All base classes | Data filtering (soft-delete, tenant) | +| `UnitOfWorkManager` | `ApplicationService`, `DomainService` | Unit of work management | +| `LoggerFactory` | All base classes | Create loggers | +| `Logger` | All base classes | Logging (auto-created) | +| `LazyServiceProvider` | All base classes | Lazy service resolution | + +**Useful methods from base classes:** +- `CheckPolicyAsync()` - Check permission and throw if not granted +- `IsGrantedAsync()` - Check permission without throwing + +## Async Best Practices +- Use async all the way - never use `.Result` or `.Wait()` +- All async methods should end with `Async` suffix +- ABP automatically handles `CancellationToken` in most cases (e.g., from `HttpContext.RequestAborted`) +- Only pass `CancellationToken` explicitly when implementing custom cancellation logic + +## Time Handling +Never use `DateTime.Now` or `DateTime.UtcNow` directly. Use ABP's `IClock` service: + +```csharp +// In classes inheriting from base classes (ApplicationService, DomainService, etc.) +public class BookAppService : ApplicationService +{ + public void DoSomething() + { + var now = Clock.Now; // ✅ Already available as property + } +} + +// In other services - inject IClock +public class MyService : ITransientDependency +{ + private readonly IClock _clock; + + public MyService(IClock clock) => _clock = clock; + + public void DoSomething() + { + var now = _clock.Now; // ✅ Correct + // var now = DateTime.Now; // ❌ Wrong - not testable, ignores timezone settings + } +} +``` + +> **Tip**: Before injecting a service, check if it's already available as a property in your base classes. + +## Business Exceptions +Use `BusinessException` for domain rule violations with namespaced error codes: + +```csharp +throw new BusinessException("MyModule:BookNameAlreadyExists") + .WithData("Name", bookName); +``` + +Configure localization mapping: +```csharp +Configure(options => +{ + options.MapCodeNamespace("MyModule", typeof(MyModuleResource)); +}); +``` + +## Localization +- In base classes (`ApplicationService`, `AbpController`, etc.): Use `L["Key"]` - this is the `IStringLocalizer` property +- In other services: Inject `IStringLocalizer` +- Always localize user-facing messages and exceptions + +**Localization file location**: `*.Domain.Shared/Localization/{ResourceName}/{lang}.json` + +```json +// Example: MyProject.Domain.Shared/Localization/MyProject/en.json +{ + "culture": "en", + "texts": { + "Menu:Home": "Home", + "Welcome": "Welcome", + "BookName": "Book Name" + } +} +``` + +## ❌ Never Use (ABP Anti-Patterns) + +| Don't Use | Use Instead | +|-----------|-------------| +| Minimal APIs | ABP Controllers or Auto API Controllers | +| MediatR | Application Services | +| `DbContext` directly in App Services | `IRepository` | +| `AddScoped/AddTransient/AddSingleton` | `ITransientDependency`, `ISingletonDependency` | +| `DateTime.Now` | `IClock` / `Clock.Now` | +| Custom UnitOfWork | ABP's `IUnitOfWorkManager` | +| Manual HTTP calls from UI | ABP client proxies (`generate-proxy`) | +| Hardcoded role checks | Permission-based authorization | +| Business logic in Controllers | Application Services | diff --git a/ai-rules/common/application-layer.mdc b/ai-rules/common/application-layer.mdc new file mode 100644 index 0000000000..7df134c590 --- /dev/null +++ b/ai-rules/common/application-layer.mdc @@ -0,0 +1,232 @@ +--- +description: "ABP Application Services, DTOs, validation, and error handling patterns" +globs: "**/*.Application/**/*.cs,**/Application/**/*.cs,**/*AppService*.cs,**/*Dto*.cs" +alwaysApply: false +--- + +# ABP Application Layer Patterns + +> **Docs**: https://abp.io/docs/latest/framework/architecture/domain-driven-design/application-services + +## Application Service Structure + +### Interface (Application.Contracts) +```csharp +public interface IBookAppService : IApplicationService +{ + Task GetAsync(Guid id); + Task> GetListAsync(GetBookListInput input); + Task CreateAsync(CreateBookDto input); + Task UpdateAsync(Guid id, UpdateBookDto input); + Task DeleteAsync(Guid id); +} +``` + +### Implementation (Application) +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + private readonly IBookRepository _bookRepository; + private readonly BookManager _bookManager; + private readonly BookMapper _bookMapper; + + public BookAppService( + IBookRepository bookRepository, + BookManager bookManager, + BookMapper bookMapper) + { + _bookRepository = bookRepository; + _bookManager = bookManager; + _bookMapper = bookMapper; + } + + public async Task GetAsync(Guid id) + { + var book = await _bookRepository.GetAsync(id); + return _bookMapper.MapToDto(book); + } + + [Authorize(BookStorePermissions.Books.Create)] + public async Task CreateAsync(CreateBookDto input) + { + var book = await _bookManager.CreateAsync(input.Name, input.Price); + await _bookRepository.InsertAsync(book); + return _bookMapper.MapToDto(book); + } + + [Authorize(BookStorePermissions.Books.Edit)] + public async Task UpdateAsync(Guid id, UpdateBookDto input) + { + var book = await _bookRepository.GetAsync(id); + await _bookManager.ChangeNameAsync(book, input.Name); + book.SetPrice(input.Price); + await _bookRepository.UpdateAsync(book); + return _bookMapper.MapToDto(book); + } +} +``` + +## Application Service Best Practices +- Don't repeat entity name in method names (`GetAsync` not `GetBookAsync`) +- Accept/return DTOs only, never entities +- ID not inside UpdateDto - pass separately +- Use custom repositories when you need custom queries, generic repository is fine for simple CRUD +- Call `UpdateAsync` explicitly (don't assume change tracking) +- Don't call other app services in same module +- Don't use `IFormFile`/`Stream` - pass `byte[]` from controllers +- Use base class properties (`Clock`, `CurrentUser`, `GuidGenerator`, `L`) instead of injecting these services + +## DTO Naming Conventions + +| Purpose | Convention | Example | +|---------|------------|---------| +| Query input | `Get{Entity}Input` | `GetBookInput` | +| List query input | `Get{Entity}ListInput` | `GetBookListInput` | +| Create input | `Create{Entity}Dto` | `CreateBookDto` | +| Update input | `Update{Entity}Dto` | `UpdateBookDto` | +| Single entity output | `{Entity}Dto` | `BookDto` | +| List item output | `{Entity}ListItemDto` | `BookListItemDto` | + +## DTO Location +- Define DTOs in `*.Application.Contracts` project +- This allows sharing with clients (Blazor, HttpApi.Client) + +## Validation + +### Data Annotations +```csharp +public class CreateBookDto +{ + [Required] + [StringLength(100, MinimumLength = 3)] + public string Name { get; set; } + + [Range(0, 999.99)] + public decimal Price { get; set; } +} +``` + +### Custom Validation with IValidatableObject +Before adding custom validation, decide if it's a **domain rule** or **application rule**: +- **Domain rule**: Put validation in entity constructor or domain service (enforces business invariants) +- **Application rule**: Use DTO validation (input format, required fields) + +Only use `IValidatableObject` for application-level validation that can't be expressed with data annotations: + +```csharp +public class CreateBookDto : IValidatableObject +{ + public string Name { get; set; } + public string Description { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (Name == Description) + { + yield return new ValidationResult( + "Name and Description cannot be the same!", + new[] { nameof(Name), nameof(Description) } + ); + } + } +} +``` + +### FluentValidation +```csharp +public class CreateBookDtoValidator : AbstractValidator +{ + public CreateBookDtoValidator() + { + RuleFor(x => x.Name).NotEmpty().Length(3, 100); + RuleFor(x => x.Price).GreaterThan(0); + } +} +``` + +## Error Handling + +### Business Exceptions +```csharp +throw new BusinessException("BookStore:010001") + .WithData("BookName", name); +``` + +### Entity Not Found +```csharp +var book = await _bookRepository.FindAsync(id); +if (book == null) +{ + throw new EntityNotFoundException(typeof(Book), id); +} +``` + +### User-Friendly Exceptions +```csharp +throw new UserFriendlyException(L["BookNotAvailable"]); +``` + +### HTTP Status Code Mapping +Status code mapping is **configurable** in ABP (do not rely on a fixed mapping in business logic). + +| Exception | Typical HTTP Status | +|-----------|-------------| +| `AbpValidationException` | 400 | +| `AbpAuthorizationException` | 401/403 | +| `EntityNotFoundException` | 404 | +| `BusinessException` | 403 (but configurable) | +| Other exceptions | 500 | + +## Auto API Controllers +ABP automatically generates API controllers for application services: +- Interface must inherit `IApplicationService` (which already has `[RemoteService]` attribute) +- HTTP methods determined by method name prefix (Get, Create, Update, Delete) +- Use `[RemoteService(false)]` to disable auto API generation for specific methods + +## Object Mapping (Mapperly / AutoMapper) +ABP supports **both Mapperly and AutoMapper** integrations. But the default mapping library is Mapperly. You need to first check the project's active mapping library. +- Prefer the mapping provider already used in the solution (check existing mapping files / loaded modules). +- In mixed solutions, explicitly setting the default provider may be required (see `docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md`). + +### Mapperly (compile-time) +Define mappers as partial classes: + +```csharp +[Mapper] +public partial class BookMapper +{ + public partial BookDto MapToDto(Book book); + public partial List MapToDtoList(List books); +} +``` + +Register in module: +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddSingleton(); +} +``` + +Usage in application service: +```csharp +public class BookAppService : ApplicationService +{ + private readonly BookMapper _bookMapper; + + public BookAppService(BookMapper bookMapper) + { + _bookMapper = bookMapper; + } + + public BookDto GetBook(Book book) + { + return _bookMapper.MapToDto(book); + } +} +``` + +> **Note**: Mapperly generates mapping code at compile-time, providing better performance than runtime mappers. + +### AutoMapper (runtime) +If the solution uses AutoMapper, mappings are typically defined in `Profile` classes and registered via ABP's AutoMapper integration. diff --git a/ai-rules/common/authorization.mdc b/ai-rules/common/authorization.mdc new file mode 100644 index 0000000000..b7885271c1 --- /dev/null +++ b/ai-rules/common/authorization.mdc @@ -0,0 +1,183 @@ +--- +description: "ABP permission system and authorization patterns" +globs: "**/*Permission*.cs,**/*AppService*.cs,**/*Controller*.cs" +alwaysApply: false +--- + +# ABP Authorization + +> **Docs**: https://abp.io/docs/latest/framework/fundamentals/authorization + +## Permission Definition +Define permissions in `*.Application.Contracts` project: + +```csharp +public static class BookStorePermissions +{ + public const string GroupName = "BookStore"; + + public static class Books + { + public const string Default = GroupName + ".Books"; + public const string Create = Default + ".Create"; + public const string Edit = Default + ".Edit"; + public const string Delete = Default + ".Delete"; + } +} +``` + +Register in provider: +```csharp +public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore")); + + var booksPermission = bookStoreGroup.AddPermission( + BookStorePermissions.Books.Default, + L("Permission:Books")); + + booksPermission.AddChild( + BookStorePermissions.Books.Create, + L("Permission:Books.Create")); + + booksPermission.AddChild( + BookStorePermissions.Books.Edit, + L("Permission:Books.Edit")); + + booksPermission.AddChild( + BookStorePermissions.Books.Delete, + L("Permission:Books.Delete")); + } + + private static LocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} +``` + +## Using Permissions + +### Declarative (Attribute) +```csharp +[Authorize(BookStorePermissions.Books.Create)] +public virtual async Task CreateAsync(CreateBookDto input) +{ + // Only users with Books.Create permission can execute +} +``` + +### Programmatic Check +```csharp +public class BookAppService : ApplicationService +{ + public async Task DoSomethingAsync() + { + // Check and throw if not granted + await CheckPolicyAsync(BookStorePermissions.Books.Edit); + + // Or check without throwing + if (await IsGrantedAsync(BookStorePermissions.Books.Delete)) + { + // Has permission + } + } +} +``` + +### Allow Anonymous Access +```csharp +[AllowAnonymous] +public virtual async Task GetPublicBookAsync(Guid id) +{ + // No authentication required +} +``` + +## Current User +Access authenticated user info via `CurrentUser` property (available in base classes like `ApplicationService`, `DomainService`, `AbpController`): + +```csharp +public class BookAppService : ApplicationService +{ + public async Task DoSomethingAsync() + { + // CurrentUser is available from base class - no injection needed + var userId = CurrentUser.Id; + var userName = CurrentUser.UserName; + var email = CurrentUser.Email; + var isAuthenticated = CurrentUser.IsAuthenticated; + var roles = CurrentUser.Roles; + var tenantId = CurrentUser.TenantId; + } +} + +// In other services, inject ICurrentUser +public class MyService : ITransientDependency +{ + private readonly ICurrentUser _currentUser; + public MyService(ICurrentUser currentUser) => _currentUser = currentUser; +} +``` + +### Ownership Validation +```csharp +public async Task UpdateMyBookAsync(Guid bookId, UpdateBookDto input) +{ + var book = await _bookRepository.GetAsync(bookId); + + if (book.CreatorId != CurrentUser.Id) + { + throw new AbpAuthorizationException(); + } + + // Update book... +} +``` + +## Multi-Tenancy Permissions +Control permission availability per tenant side: + +```csharp +bookStoreGroup.AddPermission( + BookStorePermissions.Books.Default, + L("Permission:Books"), + multiTenancySide: MultiTenancySides.Tenant // Only for tenants +); +``` + +Options: `MultiTenancySides.Host`, `Tenant`, or `Both` + +## Feature-Dependent Permissions +```csharp +booksPermission.RequireFeatures("BookStore.PremiumFeature"); +``` + +## Permission Management +Grant/revoke permissions programmatically: + +```csharp +public class MyService : ITransientDependency +{ + private readonly IPermissionManager _permissionManager; + + public async Task GrantPermissionToUserAsync(Guid userId, string permissionName) + { + await _permissionManager.SetForUserAsync(userId, permissionName, true); + } + + public async Task GrantPermissionToRoleAsync(string roleName, string permissionName) + { + await _permissionManager.SetForRoleAsync(roleName, permissionName, true); + } +} +``` + +## Security Best Practices +- Never trust client input for user identity +- Use `CurrentUser` property (from base class) or inject `ICurrentUser` +- Validate ownership in application service methods +- Filter queries by current user when appropriate +- Don't expose sensitive fields in DTOs diff --git a/ai-rules/common/cli-commands.mdc b/ai-rules/common/cli-commands.mdc new file mode 100644 index 0000000000..3e406898a1 --- /dev/null +++ b/ai-rules/common/cli-commands.mdc @@ -0,0 +1,90 @@ +--- +description: "ABP CLI commands: generate-proxy, install-libs, add-package-ref, new-module, install-module, update, clean, suite generate (CRUD pages)" +globs: "**/*.csproj,**/appsettings*.json" +alwaysApply: false +--- + +# ABP CLI Commands + +> **Full documentation**: https://abp.io/docs/latest/cli +> Use `abp help [command]` for detailed options. + +## Generate Client Proxies + +```bash +# URL flag: `-u` (short) or `--url` (long). Use whichever your team prefers, but keep it consistent. +# +# Angular (host must be running) +abp generate-proxy -t ng + +# C# client proxies +abp generate-proxy -t csharp -u https://localhost:44300 + +# Integration services only (microservices) +abp generate-proxy -t csharp -u https://localhost:44300 -st integration + +# JavaScript +abp generate-proxy -t js -u https://localhost:44300 +``` + +## Install Client-Side Libraries + +```bash +# Install NPM packages for MVC/Blazor Server +abp install-libs +``` + +## Add Package Reference + +```bash +# Add project reference with module dependency +abp add-package-ref Acme.BookStore.Domain +abp add-package-ref Acme.BookStore.Domain -t Acme.BookStore.Application +``` + +## Module Operations + +```bash +# Create new module in solution +abp new-module Acme.OrderManagement -t module:ddd + +# Install published module +abp install-module Volo.Blogging + +# Add ABP NuGet package +abp add-package Volo.Abp.Caching.StackExchangeRedis +``` + +## Update & Clean + +```bash +abp update # Update all ABP packages +abp update --version 8.0.0 # Specific version +abp clean # Delete bin/obj folders +``` + +## ABP Suite (CRUD Generation) + +Generate CRUD pages from entity JSON (created via Suite UI): + +```bash +abp suite generate --entity .suite/entities/Book.json --solution ./Acme.BookStore.sln +``` + +> **Note**: Entity JSON files are created when you generate an entity via ABP Suite UI. They are stored in `.suite/entities/` folder. +> **Suite docs**: https://abp.io/docs/latest/suite + +## Quick Reference + +| Task | Command | +|------|---------| +| Angular proxies | `abp generate-proxy -t ng` | +| C# proxies | `abp generate-proxy -t csharp -u URL` | +| Install JS libs | `abp install-libs` | +| Add reference | `abp add-package-ref PackageName` | +| Create module | `abp new-module ModuleName` | +| Install module | `abp install-module ModuleName` | +| Update packages | `abp update` | +| Clean solution | `abp clean` | +| Suite CRUD | `abp suite generate -e entity.json -s solution.sln` | +| Get help | `abp help [command]` | diff --git a/ai-rules/common/ddd-patterns.mdc b/ai-rules/common/ddd-patterns.mdc new file mode 100644 index 0000000000..7eccbdbcbe --- /dev/null +++ b/ai-rules/common/ddd-patterns.mdc @@ -0,0 +1,241 @@ +--- +description: "ABP DDD patterns - Entities, Aggregate Roots, Repositories, Domain Services" +globs: "**/*.Domain/**/*.cs,**/Domain/**/*.cs,**/Entities/**/*.cs" +alwaysApply: false +--- + +# ABP DDD Patterns + +> **Docs**: https://abp.io/docs/latest/framework/architecture/domain-driven-design + +## Rich Domain Model vs Anemic Domain Model + +ABP promotes **Rich Domain Model** pattern where entities contain both data AND behavior: + +| Anemic (Anti-pattern) | Rich (Recommended) | +|----------------------|-------------------| +| Entity = data only | Entity = data + behavior | +| Logic in services | Logic in entity methods | +| Public setters | Private setters with methods | +| No validation in entity | Entity enforces invariants | + +**Encapsulation is key**: Protect entity state by using private setters and exposing behavior through methods. + +## Entities + +### Entity Example (Rich Model) +```csharp +public class OrderLine : Entity +{ + public Guid ProductId { get; private set; } + public int Count { get; private set; } + public decimal Price { get; private set; } + + protected OrderLine() { } // For ORM + + internal OrderLine(Guid id, Guid productId, int count, decimal price) : base(id) + { + ProductId = productId; + SetCount(count); // Validates through method + Price = price; + } + + public void SetCount(int count) + { + if (count <= 0) + throw new BusinessException("Orders:InvalidCount"); + Count = count; + } +} +``` + +## Aggregate Roots + +Aggregate roots are consistency boundaries that: +- Own their child entities +- Enforce business rules +- Publish domain events + +```csharp +public class Order : AggregateRoot +{ + public string OrderNumber { get; private set; } + public Guid CustomerId { get; private set; } + public OrderStatus Status { get; private set; } + public ICollection Lines { get; private set; } + + protected Order() { } // For ORM + + public Order(Guid id, string orderNumber, Guid customerId) : base(id) + { + OrderNumber = Check.NotNullOrWhiteSpace(orderNumber, nameof(orderNumber)); + CustomerId = customerId; + Status = OrderStatus.Created; + Lines = new List(); + } + + public void AddLine(Guid lineId, Guid productId, int count, decimal price) + { + // Business rule: Can only add lines to created orders + if (Status != OrderStatus.Created) + throw new BusinessException("Orders:CannotModifyOrder"); + + Lines.Add(new OrderLine(lineId, productId, count, price)); + } + + public void Complete() + { + if (Status != OrderStatus.Created) + throw new BusinessException("Orders:CannotCompleteOrder"); + + Status = OrderStatus.Completed; + + // Publish events for side effects + AddLocalEvent(new OrderCompletedEvent(Id)); // Same transaction + AddDistributedEvent(new OrderCompletedEto { OrderId = Id }); // Cross-service + } +} +``` + +### Domain Events +- `AddLocalEvent()` - Handled within same transaction, can access full entity +- `AddDistributedEvent()` - Handled asynchronously, use ETOs (Event Transfer Objects) + +### Entity Best Practices +- **Encapsulation**: Private setters, public methods that enforce rules +- **Primary constructor**: Enforce invariants, accept `id` parameter +- **Protected parameterless constructor**: Required for ORM +- **Initialize collections**: In primary constructor +- **Virtual members**: For ORM proxy compatibility +- **Reference by Id**: Don't add navigation properties to other aggregates +- **Don't generate GUID in constructor**: Use `IGuidGenerator` externally + +## Repository Pattern + +### When to Use Custom Repository +- **Generic repository** (`IRepository`): Sufficient for simple CRUD operations +- **Custom repository**: Only when you need custom query methods + +### Interface (Domain Layer) +```csharp +// Define custom interface only when custom queries are needed +public interface IOrderRepository : IRepository +{ + Task FindByOrderNumberAsync(string orderNumber, bool includeDetails = false); + Task> GetListByCustomerAsync(Guid customerId, bool includeDetails = false); +} +``` + +### Repository Best Practices +- **One repository per aggregate root only** - Never create repositories for child entities +- Child entities must be accessed/modified only through their aggregate root +- Creating repositories for child entities breaks data consistency (bypasses aggregate root's business rules) +- In ABP, use `AddDefaultRepositories()` without `includeAllEntities: true` to enforce this +- Define custom repository only when custom queries are needed +- ABP handles `CancellationToken` automatically; add parameter only for explicit cancellation control +- Single entity methods: `includeDetails = true` by default +- List methods: `includeDetails = false` by default +- Don't return projection classes +- Interface in Domain, implementation in data layer + +```csharp +// ✅ Correct: Repository for aggregate root (Order) +public interface IOrderRepository : IRepository { } + +// ❌ Wrong: Repository for child entity (OrderLine) +// OrderLine should only be accessed through Order aggregate +public interface IOrderLineRepository : IRepository { } // Don't do this! +``` + +## Domain Services + +Use domain services for business logic that: +- Spans multiple aggregates +- Requires repository queries to enforce rules + +```csharp +public class OrderManager : DomainService +{ + private readonly IOrderRepository _orderRepository; + private readonly IProductRepository _productRepository; + + public OrderManager( + IOrderRepository orderRepository, + IProductRepository productRepository) + { + _orderRepository = orderRepository; + _productRepository = productRepository; + } + + public async Task CreateAsync(string orderNumber, Guid customerId) + { + // Business rule: Order number must be unique + var existing = await _orderRepository.FindByOrderNumberAsync(orderNumber); + if (existing != null) + { + throw new BusinessException("Orders:OrderNumberAlreadyExists") + .WithData("OrderNumber", orderNumber); + } + + return new Order(GuidGenerator.Create(), orderNumber, customerId); + } + + public async Task AddProductAsync(Order order, Guid productId, int count) + { + var product = await _productRepository.GetAsync(productId); + order.AddLine(productId, count, product.Price); + } +} +``` + +### Domain Service Best Practices +- Use `*Manager` suffix naming +- No interface by default (create only if needed) +- Accept/return domain objects, not DTOs +- Don't depend on authenticated user - pass values from application layer +- Use base class properties (`GuidGenerator`, `Clock`) instead of injecting these services + +## Domain Events + +### Local Events +```csharp +// In aggregate +AddLocalEvent(new OrderCompletedEvent(Id)); + +// Handler +public class OrderCompletedEventHandler : ILocalEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(OrderCompletedEvent eventData) + { + // Handle within same transaction + } +} +``` + +### Distributed Events (ETO) +For inter-module/microservice communication: +```csharp +// In Domain.Shared +[EventName("Orders.OrderCompleted")] +public class OrderCompletedEto +{ + public Guid OrderId { get; set; } + public string OrderNumber { get; set; } +} +``` + +## Specifications + +Reusable query conditions: +```csharp +public class CompletedOrdersSpec : Specification +{ + public override Expression> ToExpression() + { + return o => o.Status == OrderStatus.Completed; + } +} + +// Usage +var orders = await _orderRepository.GetListAsync(new CompletedOrdersSpec()); +``` diff --git a/ai-rules/common/dependency-rules.mdc b/ai-rules/common/dependency-rules.mdc new file mode 100644 index 0000000000..3210b7436a --- /dev/null +++ b/ai-rules/common/dependency-rules.mdc @@ -0,0 +1,151 @@ +--- +description: "ABP layer dependency rules and project structure guardrails" +globs: "**/*.csproj,**/*Module*.cs" +alwaysApply: false +--- + +# ABP Dependency Rules + +## Core Principles (All Templates) + +These principles apply regardless of solution structure: + +1. **Domain logic never depends on infrastructure** (no DbContext in domain/application) +2. **Use abstractions** (interfaces) for dependencies +3. **Higher layers depend on lower layers**, never the reverse +4. **Data access through repositories**, not direct DbContext + +## Layered Template Structure + +> **Note**: This section applies to layered templates (app, module). Single-layer and microservice templates have different structures. + +``` +Domain.Shared → Constants, enums, localization keys + ↑ + Domain → Entities, repository interfaces, domain services + ↑ +Application.Contracts → App service interfaces, DTOs + ↑ + Application → App service implementations + ↑ + HttpApi → REST controllers (optional) + ↑ + Host → Final application with DI and middleware +``` + +### Layered Dependency Direction + +| Project | Can Reference | Referenced By | +|---------|---------------|---------------| +| Domain.Shared | Nothing | All | +| Domain | Domain.Shared | Application, Data layer | +| Application.Contracts | Domain.Shared | Application, HttpApi, Clients | +| Application | Domain, Contracts | Host | +| EntityFrameworkCore/MongoDB | Domain | Host only | +| HttpApi | Contracts only | Host | + +## Critical Rules + +### ❌ Never Do +```csharp +// Application layer accessing DbContext directly +public class BookAppService : ApplicationService +{ + private readonly MyDbContext _dbContext; // ❌ WRONG +} + +// Domain depending on application layer +public class BookManager : DomainService +{ + private readonly IBookAppService _appService; // ❌ WRONG +} + +// HttpApi depending on Application implementation +public class BookController : AbpController +{ + private readonly BookAppService _bookAppService; // ❌ WRONG - Use interface +} +``` + +### ✅ Always Do +```csharp +// Application layer using repository abstraction +public class BookAppService : ApplicationService +{ + private readonly IBookRepository _bookRepository; // ✅ CORRECT +} + +// Domain service using domain abstractions +public class BookManager : DomainService +{ + private readonly IBookRepository _bookRepository; // ✅ CORRECT +} + +// HttpApi depending on contracts only +public class BookController : AbpController +{ + private readonly IBookAppService _bookAppService; // ✅ CORRECT +} +``` + +## Repository Pattern Enforcement + +### Interface Location +```csharp +// In Domain project +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); +} +``` + +### Implementation Location +```csharp +// In EntityFrameworkCore project +public class BookRepository : EfCoreRepository, IBookRepository +{ + // Implementation +} + +// In MongoDB project +public class BookRepository : MongoDbRepository, IBookRepository +{ + // Implementation +} +``` + +## Multi-Application Scenarios + +When you have multiple applications (e.g., Admin + Public API): + +### Vertical Separation +``` +MyProject.Admin.Application - Admin-specific services +MyProject.Public.Application - Public-specific services +MyProject.Domain - Shared domain (both reference this) +``` + +### Rules +- Admin and Public application layers **MUST NOT** reference each other +- Share domain logic, not application logic +- Each vertical can have its own DTOs even if similar + +## Enforcement Checklist (Layered Templates) + +When adding a new feature: +1. **Entity changes?** → Domain project +2. **Constants/enums?** → Domain.Shared project +3. **Repository interface?** → Domain project (only if custom queries needed) +4. **Repository implementation?** → EntityFrameworkCore/MongoDB project +5. **DTOs and service interface?** → Application.Contracts project +6. **Service implementation?** → Application project +7. **API endpoint?** → HttpApi project (if not using auto API controllers) + +## Common Violations to Watch + +| Violation | Impact | Fix | +|-----------|--------|-----| +| DbContext in Application | Breaks DB independence | Use repository | +| Entity in DTO | Exposes internals | Map to DTO | +| IQueryable in interface | Breaks abstraction | Return concrete types | +| Cross-module app service call | Tight coupling | Use events or domain | diff --git a/ai-rules/common/development-flow.mdc b/ai-rules/common/development-flow.mdc new file mode 100644 index 0000000000..22ee69687a --- /dev/null +++ b/ai-rules/common/development-flow.mdc @@ -0,0 +1,291 @@ +--- +description: "ABP development workflow - adding features, entities, and migrations" +globs: "**/*AppService*.cs,**/*Application*/**/*.cs,**/*Application.Contracts*/**/*.cs,**/*Dto*.cs,**/*DbContext*.cs,**/*.EntityFrameworkCore/**/*.cs,**/*.MongoDB/**/*.cs,**/*Permission*.cs" +alwaysApply: false +--- + +# ABP Development Workflow + +> **Tutorials**: https://abp.io/docs/latest/tutorials + +## Adding a New Entity (Full Flow) + +### 1. Domain Layer +Create entity (location varies by template: `*.Domain/Entities/` for layered, `Entities/` for single-layer/microservice): + +```csharp +public class Book : AggregateRoot +{ + public string Name { get; private set; } + public decimal Price { get; private set; } + public Guid AuthorId { get; private set; } + + protected Book() { } + + public Book(Guid id, string name, decimal price, Guid authorId) : base(id) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name)); + SetPrice(price); + AuthorId = authorId; + } + + public void SetPrice(decimal price) + { + Price = Check.Range(price, nameof(price), 0, 9999); + } +} +``` + +### 2. Domain.Shared +Add constants and enums in `*.Domain.Shared/`: + +```csharp +public static class BookConsts +{ + public const int MaxNameLength = 128; +} + +public enum BookType +{ + Novel, + Science, + Biography +} +``` + +### 3. Repository Interface (Optional) +Define custom repository in `*.Domain/` only if you need custom query methods. For simple CRUD, use generic `IRepository` directly: + +```csharp +// Only if custom queries are needed +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); +} +``` + +### 4. EF Core Configuration +In `*.EntityFrameworkCore/`: + +**DbContext:** +```csharp +public DbSet Books { get; set; } +``` + +**OnModelCreating:** +```csharp +builder.Entity(b => +{ + b.ToTable(MyProjectConsts.DbTablePrefix + "Books", MyProjectConsts.DbSchema); + b.ConfigureByConvention(); + b.Property(x => x.Name).IsRequired().HasMaxLength(BookConsts.MaxNameLength); + b.HasIndex(x => x.Name); +}); +``` + +**Repository Implementation (only if custom interface defined):** +```csharp +public class BookRepository : EfCoreRepository, IBookRepository +{ + public BookRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync(string name) + { + return await (await GetDbSetAsync()) + .FirstOrDefaultAsync(b => b.Name == name); + } +} +``` + +### 5. Run Migration +```bash +cd src/MyProject.EntityFrameworkCore + +# Add migration +dotnet ef migrations add Added_Book + +# Apply migration (choose one): +dotnet run --project ../MyProject.DbMigrator # Recommended - also seeds data +# OR +dotnet ef database update # EF Core command only +``` + +### 6. Application.Contracts +Create DTOs and service interface: + +```csharp +// DTOs +public class BookDto : EntityDto +{ + public string Name { get; set; } + public decimal Price { get; set; } + public Guid AuthorId { get; set; } +} + +public class CreateBookDto +{ + [Required] + [StringLength(BookConsts.MaxNameLength)] + public string Name { get; set; } + + [Range(0, 9999)] + public decimal Price { get; set; } + + [Required] + public Guid AuthorId { get; set; } +} + +// Service Interface +public interface IBookAppService : IApplicationService +{ + Task GetAsync(Guid id); + Task> GetListAsync(PagedAndSortedResultRequestDto input); + Task CreateAsync(CreateBookDto input); +} +``` + +### 7. Object Mapping (Mapperly / AutoMapper) +ABP supports both Mapperly and AutoMapper. Prefer the provider already used in the solution. + +If the solution uses **Mapperly**, create a mapper in the Application project: + +```csharp +[Mapper] +public partial class BookMapper +{ + public partial BookDto MapToDto(Book book); + public partial List MapToDtoList(List books); +} +``` + +Register in module: +```csharp +context.Services.AddSingleton(); +``` + +### 8. Application Service +Implement service (using generic repository - use `IBookRepository` if you defined custom interface in step 3): + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + private readonly IRepository _bookRepository; // Or IBookRepository + private readonly BookMapper _bookMapper; + + public BookAppService( + IRepository bookRepository, + BookMapper bookMapper) + { + _bookRepository = bookRepository; + _bookMapper = bookMapper; + } + + public async Task GetAsync(Guid id) + { + var book = await _bookRepository.GetAsync(id); + return _bookMapper.MapToDto(book); + } + + [Authorize(MyProjectPermissions.Books.Create)] + public async Task CreateAsync(CreateBookDto input) + { + var book = new Book( + GuidGenerator.Create(), + input.Name, + input.Price, + input.AuthorId + ); + + await _bookRepository.InsertAsync(book); + return _bookMapper.MapToDto(book); + } +} +``` + +### 9. Add Localization +In `*.Domain.Shared/Localization/*/en.json`: + +```json +{ + "Book": "Book", + "Books": "Books", + "BookName": "Name", + "BookPrice": "Price" +} +``` + +### 10. Add Permissions (if needed) +```csharp +public static class MyProjectPermissions +{ + public static class Books + { + public const string Default = "MyProject.Books"; + public const string Create = Default + ".Create"; + } +} +``` + +### 11. Add Tests +```csharp +public class BookAppService_Tests : MyProjectApplicationTestBase +{ + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Create_Book() + { + var result = await _bookAppService.CreateAsync(new CreateBookDto + { + Name = "Test Book", + Price = 19.99m + }); + + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("Test Book"); + } +} +``` + +## Quick Reference Commands + +### Build Solution +```bash +dotnet build +``` + +### Run Migrations +```bash +cd src/MyProject.EntityFrameworkCore +dotnet ef migrations add MigrationName +dotnet run --project ../MyProject.DbMigrator # Apply migration + seed data +``` + +### Generate Angular Proxies +```bash +abp generate-proxy -t ng +``` + +## Checklist for New Features + +- [ ] Entity created with proper constructors +- [ ] Constants in Domain.Shared +- [ ] Custom repository interface in Domain (only if custom queries needed) +- [ ] EF Core configuration added +- [ ] Custom repository implementation (only if interface defined) +- [ ] Migration generated and applied (use DbMigrator) +- [ ] Mapperly mapper created and registered +- [ ] DTOs created in Application.Contracts +- [ ] Service interface defined +- [ ] Service implementation with authorization +- [ ] Localization keys added +- [ ] Permissions defined (if applicable) +- [ ] Tests written diff --git a/ai-rules/common/infrastructure.mdc b/ai-rules/common/infrastructure.mdc new file mode 100644 index 0000000000..1a2c555581 --- /dev/null +++ b/ai-rules/common/infrastructure.mdc @@ -0,0 +1,244 @@ +--- +description: "ABP infrastructure services - Settings, Features, Caching, Events, Background Jobs" +globs: "**/*Setting*.cs,**/*Feature*.cs,**/*Cache*.cs,**/*Event*.cs,**/*Job*.cs" +alwaysApply: false +--- + +# ABP Infrastructure Services + +> **Docs**: https://abp.io/docs/latest/framework/infrastructure + +## Settings + +### Define Settings +```csharp +public class MySettingDefinitionProvider : SettingDefinitionProvider +{ + public override void Define(ISettingDefinitionContext context) + { + context.Add( + new SettingDefinition("MyApp.MaxItemCount", "10"), + new SettingDefinition("MyApp.EnableFeature", "false"), + new SettingDefinition("MyApp.SecretKey", isEncrypted: true) + ); + } +} +``` + +### Read Settings +```csharp +public class MyService : ITransientDependency +{ + private readonly ISettingProvider _settingProvider; + + public async Task DoSomethingAsync() + { + var maxCount = await _settingProvider.GetAsync("MyApp.MaxItemCount"); + var isEnabled = await _settingProvider.IsTrueAsync("MyApp.EnableFeature"); + } +} +``` + +### Setting Value Providers (Priority Order) +1. User settings (highest) +2. Tenant settings +3. Global settings +4. Configuration (appsettings.json) +5. Default value (lowest) + +## Features + +### Define Features +```csharp +public class MyFeatureDefinitionProvider : FeatureDefinitionProvider +{ + public override void Define(IFeatureDefinitionContext context) + { + var myGroup = context.AddGroup("MyApp"); + + myGroup.AddFeature( + "MyApp.PdfReporting", + defaultValue: "false", + valueType: new ToggleStringValueType() + ); + + myGroup.AddFeature( + "MyApp.MaxProductCount", + defaultValue: "10", + valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1000)) + ); + } +} +``` + +### Check Features +```csharp +[RequiresFeature("MyApp.PdfReporting")] +public async Task GetPdfReportAsync() +{ + // Only executes if feature is enabled +} + +// Or programmatically +if (await _featureChecker.IsEnabledAsync("MyApp.PdfReporting")) +{ + // Feature is enabled for current tenant +} + +var maxCount = await _featureChecker.GetAsync("MyApp.MaxProductCount"); +``` + +## Distributed Caching + +### Typed Cache +```csharp +public class BookService : ITransientDependency +{ + private readonly IDistributedCache _cache; + private readonly IClock _clock; + + public BookService(IDistributedCache cache, IClock clock) + { + _cache = cache; + _clock = clock; + } + + public async Task GetAsync(Guid bookId) + { + return await _cache.GetOrAddAsync( + bookId.ToString(), + async () => await GetBookFromDatabaseAsync(bookId), + () => new DistributedCacheEntryOptions + { + AbsoluteExpiration = _clock.Now.AddHours(1) + } + ); + } +} + +[CacheName("Books")] +public class BookCacheItem +{ + public string Name { get; set; } + public decimal Price { get; set; } +} +``` + +## Event Bus + +### Local Events (Same Process) +```csharp +// Event class +public class OrderCreatedEvent +{ + public Order Order { get; set; } +} + +// Handler +public class OrderCreatedEventHandler : ILocalEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(OrderCreatedEvent eventData) + { + // Handle within same transaction + } +} + +// Publish +await _localEventBus.PublishAsync(new OrderCreatedEvent { Order = order }); +``` + +### Distributed Events (Cross-Service) +```csharp +// Event Transfer Object (in Domain.Shared) +[EventName("MyApp.Order.Created")] +public class OrderCreatedEto +{ + public Guid OrderId { get; set; } + public string OrderNumber { get; set; } +} + +// Handler +public class OrderCreatedEtoHandler : IDistributedEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(OrderCreatedEto eventData) + { + // Handle distributed event + } +} + +// Publish +await _distributedEventBus.PublishAsync(new OrderCreatedEto { ... }); +``` + +### When to Use Which +- **Local**: Within same module/bounded context +- **Distributed**: Cross-module or microservice communication + +## Background Jobs + +### Define Job +```csharp +public class EmailSendingArgs +{ + public string EmailAddress { get; set; } + public string Subject { get; set; } + public string Body { get; set; } +} + +public class EmailSendingJob : AsyncBackgroundJob, ITransientDependency +{ + private readonly IEmailSender _emailSender; + + public EmailSendingJob(IEmailSender emailSender) + { + _emailSender = emailSender; + } + + public override async Task ExecuteAsync(EmailSendingArgs args) + { + await _emailSender.SendAsync(args.EmailAddress, args.Subject, args.Body); + } +} +``` + +### Enqueue Job +```csharp +await _backgroundJobManager.EnqueueAsync( + new EmailSendingArgs + { + EmailAddress = "user@example.com", + Subject = "Hello", + Body = "..." + }, + delay: TimeSpan.FromMinutes(5) // Optional delay +); +``` + +## Localization + +### Define Resource +```csharp +[LocalizationResourceName("MyModule")] +public class MyModuleResource { } +``` + +### JSON Structure +```json +{ + "culture": "en", + "texts": { + "HelloWorld": "Hello World!", + "Menu:Books": "Books" + } +} +``` + +### Usage +- In `ApplicationService`: Use `L["Key"]` property (already available from base class) +- In other services: Inject `IStringLocalizer` + +> **Tip**: ABP base classes already provide commonly used services as properties. Check before injecting: +> - `StringLocalizer` (L), `Clock`, `CurrentUser`, `CurrentTenant`, `GuidGenerator` +> - `AuthorizationService`, `FeatureChecker`, `DataFilter` +> - `LoggerFactory`, `Logger` +> - Methods like `CheckPolicyAsync()` for authorization checks diff --git a/ai-rules/common/multi-tenancy.mdc b/ai-rules/common/multi-tenancy.mdc new file mode 100644 index 0000000000..54574a1c31 --- /dev/null +++ b/ai-rules/common/multi-tenancy.mdc @@ -0,0 +1,162 @@ +--- +description: "ABP Multi-Tenancy patterns - tenant-aware entities, data isolation, and tenant switching" +globs: "**/*Tenant*.cs,**/*MultiTenant*.cs,**/Entities/**/*.cs" +alwaysApply: false +--- + +# ABP Multi-Tenancy + +> **Docs**: https://abp.io/docs/latest/framework/architecture/multi-tenancy + +## Making Entities Multi-Tenant + +Implement `IMultiTenant` interface to make entities tenant-aware: + +```csharp +public class Product : AggregateRoot, IMultiTenant +{ + public Guid? TenantId { get; set; } // Required by IMultiTenant + + public string Name { get; private set; } + public decimal Price { get; private set; } + + protected Product() { } + + public Product(Guid id, string name, decimal price) : base(id) + { + Name = name; + Price = price; + // TenantId is automatically set from CurrentTenant.Id + } +} +``` + +**Key points:** +- `TenantId` is **nullable** - `null` means entity belongs to Host +- ABP **automatically filters** queries by current tenant +- ABP **automatically sets** `TenantId` when creating entities + +## Accessing Current Tenant + +Use `CurrentTenant` property (available in base classes) or inject `ICurrentTenant`: + +```csharp +public class ProductAppService : ApplicationService +{ + public async Task DoSomethingAsync() + { + // Available from base class + var tenantId = CurrentTenant.Id; // Guid? - null for host + var tenantName = CurrentTenant.Name; // string? + var isAvailable = CurrentTenant.IsAvailable; // true if Id is not null + } +} + +// In other services +public class MyService : ITransientDependency +{ + private readonly ICurrentTenant _currentTenant; + public MyService(ICurrentTenant currentTenant) => _currentTenant = currentTenant; +} +``` + +## Switching Tenant Context + +Use `CurrentTenant.Change()` to temporarily switch tenant (useful in host context): + +```csharp +public class ProductManager : DomainService +{ + private readonly IRepository _productRepository; + + public async Task GetProductCountAsync(Guid? tenantId) + { + // Switch to specific tenant + using (CurrentTenant.Change(tenantId)) + { + return await _productRepository.GetCountAsync(); + } + // Automatically restored to previous tenant after using block + } + + public async Task DoHostOperationAsync() + { + // Switch to host context + using (CurrentTenant.Change(null)) + { + // Operations here are in host context + } + } +} +``` + +> **Important**: Always use `Change()` with a `using` statement. + +## Disabling Multi-Tenant Filter + +To query all tenants' data (only works with single database): + +```csharp +public class ProductManager : DomainService +{ + public async Task GetAllProductCountAsync() + { + // DataFilter is available from base class + using (DataFilter.Disable()) + { + return await _productRepository.GetCountAsync(); + // Returns count from ALL tenants + } + } +} +``` + +> **Note**: This doesn't work with separate databases per tenant. + +## Database Architecture Options + +| Approach | Description | Use Case | +|----------|-------------|----------| +| Single Database | All tenants share one database | Simple, cost-effective | +| Database per Tenant | Each tenant has dedicated database | Data isolation, compliance | +| Hybrid | Mix of shared and dedicated | Flexible, premium tenants | + +Connection strings are configured per tenant in Tenant Management module. + +## Best Practices + +1. **Always implement `IMultiTenant`** for tenant-specific entities +2. **Never manually filter by `TenantId`** - ABP does it automatically +3. **Don't change `TenantId` after creation** - it moves entity between tenants +4. **Use `Change()` scope carefully** - nested scopes are supported +5. **Test both host and tenant contexts** - ensure proper data isolation +6. **Consider nullable `TenantId`** - entity may be host-only or shared + +## Enabling Multi-Tenancy + +```csharp +Configure(options => +{ + options.IsEnabled = true; // Enabled by default in ABP templates +}); +``` + +Check `MultiTenancyConsts.IsEnabled` in your solution for centralized control. + +## Tenant Resolution + +ABP resolves current tenant from (in order): +1. Current user's claims +2. Query string (`?__tenant=...`) +3. Route (`/{__tenant}/...`) +4. HTTP header (`__tenant`) +5. Cookie (`__tenant`) +6. Domain/subdomain (if configured) + +For subdomain-based resolution: +```csharp +Configure(options => +{ + options.AddDomainTenantResolver("{0}.mydomain.com"); +}); +``` diff --git a/ai-rules/data/ef-core.mdc b/ai-rules/data/ef-core.mdc new file mode 100644 index 0000000000..6ca6a423d2 --- /dev/null +++ b/ai-rules/data/ef-core.mdc @@ -0,0 +1,254 @@ +--- +description: "ABP Entity Framework Core patterns - DbContext, migrations, repositories" +globs: "**/*.EntityFrameworkCore/**/*.cs,**/EntityFrameworkCore/**/*.cs,**/*DbContext*.cs" +alwaysApply: false +--- + +# ABP Entity Framework Core + +> **Docs**: https://abp.io/docs/latest/framework/data/entity-framework-core + +## DbContext Configuration + +```csharp +[ConnectionStringName("Default")] +public class MyProjectDbContext : AbpDbContext +{ + public DbSet Books { get; set; } + public DbSet Authors { get; set; } + + public MyProjectDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + // Configure all entities + builder.ConfigureMyProject(); + } +} +``` + +## Entity Configuration + +```csharp +public static class MyProjectDbContextModelCreatingExtensions +{ + public static void ConfigureMyProject(this ModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + builder.Entity(b => + { + b.ToTable(MyProjectConsts.DbTablePrefix + "Books", MyProjectConsts.DbSchema); + b.ConfigureByConvention(); // ABP conventions (audit, soft-delete, etc.) + + // Property configurations + b.Property(x => x.Name) + .IsRequired() + .HasMaxLength(BookConsts.MaxNameLength); + + b.Property(x => x.Price) + .HasColumnType("decimal(18,2)"); + + // Indexes + b.HasIndex(x => x.Name); + + // Relationships + b.HasOne() + .WithMany() + .HasForeignKey(x => x.AuthorId) + .OnDelete(DeleteBehavior.Restrict); + }); + } +} +``` + +## Repository Implementation + +```csharp +public class BookRepository : EfCoreRepository, IBookRepository +{ + public BookRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync( + string name, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + var dbSet = await GetDbSetAsync(); + + return await dbSet + .IncludeDetails(includeDetails) + .FirstOrDefaultAsync( + b => b.Name == name, + GetCancellationToken(cancellationToken)); + } + + public async Task> GetListByAuthorAsync( + Guid authorId, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var dbSet = await GetDbSetAsync(); + + return await dbSet + .IncludeDetails(includeDetails) + .Where(b => b.AuthorId == authorId) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public override async Task> WithDetailsAsync() + { + return (await GetQueryableAsync()) + .Include(b => b.Reviews); + } +} +``` + +## Extension Method for Include +```csharp +public static class BookEfCoreQueryableExtensions +{ + public static IQueryable IncludeDetails( + this IQueryable queryable, + bool include = true) + { + if (!include) + { + return queryable; + } + + return queryable + .Include(b => b.Reviews); + } +} +``` + +## Migration Commands + +```bash +# Navigate to EF Core project +cd src/MyProject.EntityFrameworkCore + +# Add migration +dotnet ef migrations add MigrationName + +# Apply migration (choose one): +dotnet run --project ../MyProject.DbMigrator # Recommended - also seeds data +dotnet ef database update # EF Core command only + +# Remove last migration (if not applied) +dotnet ef migrations remove + +# Generate SQL script +dotnet ef migrations script +``` + +> **Note**: ABP templates include `IDesignTimeDbContextFactory` in the EF Core project, so `-s` (startup project) parameter is not needed. + +## Module Configuration + +```csharp +[DependsOn(typeof(AbpEntityFrameworkCoreModule))] +public class MyProjectEntityFrameworkCoreModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAbpDbContext(options => + { + // Add default repositories for aggregate roots only (DDD best practice) + options.AddDefaultRepositories(); + // ⚠️ Avoid includeAllEntities: true - it creates repositories for child entities, + // allowing them to be modified without going through the aggregate root, + // which breaks data consistency + }); + + Configure(options => + { + options.UseSqlServer(); // or UseNpgsql(), UseMySql(), etc. + }); + } +} +``` + +## Best Practices + +### Repositories for Aggregate Roots Only +Don't use `includeAllEntities: true` in `AddDefaultRepositories()`. This creates repositories for child entities, allowing direct modification without going through the aggregate root - breaking DDD data consistency rules. + +```csharp +// ✅ Correct - Only aggregate roots get repositories +options.AddDefaultRepositories(); + +// ❌ Avoid - Creates repositories for ALL entities including child entities +options.AddDefaultRepositories(includeAllEntities: true); +``` + +### Always Call ConfigureByConvention +```csharp +builder.Entity(b => +{ + b.ConfigureByConvention(); // Don't forget this! + // Other configurations... +}); +``` + +### Use Table Prefix +```csharp +public static class MyProjectConsts +{ + public const string DbTablePrefix = "App"; + public const string DbSchema = null; // Or "myschema" +} +``` + +### Performance Tips +- Add explicit indexes for frequently queried fields +- Use `AsNoTracking()` for read-only queries +- Avoid N+1 queries with `.Include()` or specifications +- ABP handles cancellation automatically; use `GetCancellationToken(cancellationToken)` only in custom repository methods +- Consider query splitting for complex queries with multiple collections + +### Accessing Raw DbContext +```csharp +public async Task CustomOperationAsync() +{ + var dbContext = await GetDbContextAsync(); + + // Raw SQL + await dbContext.Database.ExecuteSqlRawAsync( + "UPDATE Books SET IsPublished = 1 WHERE AuthorId = {0}", + authorId + ); +} +``` + +## Data Seeding + +```csharp +public class MyProjectDataSeedContributor : IDataSeedContributor, ITransientDependency +{ + private readonly IRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public async Task SeedAsync(DataSeedContext context) + { + if (await _bookRepository.GetCountAsync() > 0) + { + return; + } + + await _bookRepository.InsertAsync( + new Book(_guidGenerator.Create(), "Sample Book", 19.99m, Guid.Empty), + autoSave: true + ); + } +} +``` diff --git a/ai-rules/data/mongodb.mdc b/ai-rules/data/mongodb.mdc new file mode 100644 index 0000000000..671d2052c4 --- /dev/null +++ b/ai-rules/data/mongodb.mdc @@ -0,0 +1,203 @@ +--- +description: "ABP MongoDB patterns - MongoDbContext and repositories" +globs: "**/*.MongoDB/**/*.cs,**/MongoDB/**/*.cs,**/*MongoDb*.cs" +alwaysApply: false +--- + +# ABP MongoDB + +> **Docs**: https://abp.io/docs/latest/framework/data/mongodb + +## MongoDbContext Configuration + +```csharp +[ConnectionStringName("Default")] +public class MyProjectMongoDbContext : AbpMongoDbContext +{ + public IMongoCollection Books => Collection(); + public IMongoCollection Authors => Collection(); + + protected override void CreateModel(IMongoModelBuilder modelBuilder) + { + base.CreateModel(modelBuilder); + + modelBuilder.ConfigureMyProject(); + } +} +``` + +## Entity Configuration + +```csharp +public static class MyProjectMongoDbContextExtensions +{ + public static void ConfigureMyProject(this IMongoModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + builder.Entity(b => + { + b.CollectionName = MyProjectConsts.DbTablePrefix + "Books"; + }); + + builder.Entity(b => + { + b.CollectionName = MyProjectConsts.DbTablePrefix + "Authors"; + }); + } +} +``` + +## Repository Implementation + +```csharp +public class BookRepository : MongoDbRepository, IBookRepository +{ + public BookRepository(IMongoDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync( + string name, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .FirstOrDefaultAsync( + b => b.Name == name, + GetCancellationToken(cancellationToken)); + } + + public async Task> GetListByAuthorAsync( + Guid authorId, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(b => b.AuthorId == authorId) + .ToListAsync(GetCancellationToken(cancellationToken)); + } +} +``` + +## Module Configuration + +```csharp +[DependsOn(typeof(AbpMongoDbModule))] +public class MyProjectMongoDbModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddMongoDbContext(options => + { + // Add default repositories for aggregate roots only (DDD best practice) + options.AddDefaultRepositories(); + // ⚠️ Avoid includeAllEntities: true - breaks DDD data consistency + }); + } +} +``` + +## Connection String + +In `appsettings.json`: +```json +{ + "ConnectionStrings": { + "Default": "mongodb://localhost:27017/MyProjectDb" + } +} +``` + +## Key Differences from EF Core + +### No Migrations +MongoDB is schema-less; no migrations needed. Changes to entity structure are handled automatically. + +### includeDetails Parameter +Often ignored in MongoDB because documents typically embed related data: + +```csharp +public async Task> GetListAsync( + bool includeDetails = false, // Usually ignored + CancellationToken cancellationToken = default) +{ + // MongoDB documents already include nested data + return await (await GetQueryableAsync()) + .ToListAsync(GetCancellationToken(cancellationToken)); +} +``` + +### Embedded Documents vs References +```csharp +// Embedded (stored in same document) +public class Order : AggregateRoot +{ + public List Lines { get; set; } // Embedded +} + +// Reference (separate collection, store ID only) +public class Order : AggregateRoot +{ + public Guid CustomerId { get; set; } // Reference by ID +} +``` + +### No Change Tracking +MongoDB doesn't track entity changes automatically: + +```csharp +public async Task UpdateBookAsync(Guid id, string newName) +{ + var book = await _bookRepository.GetAsync(id); + book.SetName(newName); + + // Must explicitly update + await _bookRepository.UpdateAsync(book); +} +``` + +## Direct Collection Access + +```csharp +public async Task CustomOperationAsync() +{ + var collection = await GetCollectionAsync(); + + // Use MongoDB driver directly + var filter = Builders.Filter.Eq(b => b.AuthorId, authorId); + var update = Builders.Update.Set(b => b.IsPublished, true); + + await collection.UpdateManyAsync(filter, update); +} +``` + +## Indexing + +Configure indexes in repository or via MongoDB driver: + +```csharp +public class BookRepository : MongoDbRepository, IBookRepository +{ + public override async Task> GetQueryableAsync() + { + var collection = await GetCollectionAsync(); + + // Ensure index exists + var indexKeys = Builders.IndexKeys.Ascending(b => b.Name); + await collection.Indexes.CreateOneAsync(new CreateIndexModel(indexKeys)); + + return await base.GetQueryableAsync(); + } +} +``` + +## Best Practices + +- Design documents for query patterns (denormalize when needed) +- Use references for frequently changing data +- Use embedding for data that's always accessed together +- Add indexes for frequently queried fields +- Use `GetCancellationToken(cancellationToken)` for proper cancellation +- Remember: ABP data filters (soft-delete, multi-tenancy) work with MongoDB too diff --git a/ai-rules/template-specific/app-nolayers.mdc b/ai-rules/template-specific/app-nolayers.mdc new file mode 100644 index 0000000000..4d9a00458f --- /dev/null +++ b/ai-rules/template-specific/app-nolayers.mdc @@ -0,0 +1,79 @@ +--- +description: "ABP Single-Layer (No-Layers) application template specific patterns" +globs: "**/src/*/*Module.cs,**/src/*/Entities/**/*.cs,**/src/*/Services/**/*.cs,**/src/*/Data/**/*.cs" +alwaysApply: false +--- + +# ABP Single-Layer Application Template + +> **Docs**: https://abp.io/docs/latest/solution-templates/single-layer-web-application + +## Solution Structure + +Single project containing everything: + +``` +MyProject/ +├── src/ +│ └── MyProject/ +│ ├── Data/ # DbContext, migrations +│ ├── Entities/ # Domain entities +│ ├── Services/ # Application services + DTOs +│ ├── Pages/ # Razor pages / Blazor components +│ └── MyProjectModule.cs +└── test/ + └── MyProject.Tests/ +``` + +## Key Differences from Layered + +| Layered Template | Single-Layer Template | +|------------------|----------------------| +| DTOs in Application.Contracts | DTOs in Services folder (same project) | +| Repository interfaces in Domain | Use generic `IRepository` directly | +| Separate Domain.Shared for constants | Constants in same project | +| Multiple module classes | Single module class | + +## File Organization + +Group related files by feature: + +``` +Services/ +├── Books/ +│ ├── BookAppService.cs +│ ├── BookDto.cs +│ ├── CreateBookDto.cs +│ └── IBookAppService.cs +└── Authors/ + ├── AuthorAppService.cs + └── ... +``` + +## Simplified Entity (Still keep invariants) + +Single-layer templates are structurally simpler, but you may still have real business invariants. + +- For **trivial CRUD** entities, public setters can be acceptable. +- For **non-trivial business rules**, still prefer encapsulation (private setters + methods) to prevent invalid states. + +```csharp +public class Book : AuditedAggregateRoot +{ + public string Name { get; set; } // OK for trivial CRUD only + public decimal Price { get; set; } +} +``` + +## No Custom Repository Needed + +Use generic repository directly - no need to define custom interfaces: + +```csharp +public class BookAppService : ApplicationService +{ + private readonly IRepository _bookRepository; + + // Generic repository is sufficient for single-layer apps +} +``` diff --git a/ai-rules/template-specific/microservice.mdc b/ai-rules/template-specific/microservice.mdc new file mode 100644 index 0000000000..749dfca572 --- /dev/null +++ b/ai-rules/template-specific/microservice.mdc @@ -0,0 +1,209 @@ +--- +description: "ABP Microservice solution template specific patterns" +alwaysApply: false +--- + +# ABP Microservice Solution Template + +> **Docs**: https://abp.io/docs/latest/solution-templates/microservice + +## Solution Structure + +``` +MyMicroservice/ +├── apps/ # UI applications +│ ├── web/ # Web application +│ ├── public-web/ # Public website +│ └── auth-server/ # Authentication server (OpenIddict) +├── gateways/ # BFF pattern - one gateway per UI +│ └── web-gateway/ # YARP reverse proxy +├── services/ # Microservices +│ ├── administration/ # Permissions, settings, features +│ ├── identity/ # Users, roles +│ └── [your-services]/ # Your business services +└── etc/ + ├── docker/ # Docker compose for local infra + └── helm/ # Kubernetes deployment +``` + +## Microservice Structure (NOT Layered!) + +Each microservice has simplified structure - everything in one project: + +``` +services/ordering/ +├── OrderingService/ # Main project +│ ├── Entities/ +│ ├── Services/ +│ ├── IntegrationServices/ # For inter-service communication +│ ├── Data/ # DbContext (implements IHasEventInbox, IHasEventOutbox) +│ └── OrderingServiceModule.cs +├── OrderingService.Contracts/ # Interfaces, DTOs, ETOs (shared) +└── OrderingService.Tests/ +``` + +## Inter-Service Communication + +### 1. Integration Services (Synchronous HTTP) + +For synchronous calls, use **Integration Services** - NOT regular application services. + +#### Step 1: Provider Service - Create Integration Service + +```csharp +// In CatalogService.Contracts project +[IntegrationService] +public interface IProductIntegrationService : IApplicationService +{ + Task> GetProductsByIdsAsync(List ids); +} + +// In CatalogService project +[IntegrationService] +public class ProductIntegrationService : ApplicationService, IProductIntegrationService +{ + public async Task> GetProductsByIdsAsync(List ids) + { + var products = await _productRepository.GetListAsync(p => ids.Contains(p.Id)); + return ObjectMapper.Map, List>(products); + } +} +``` + +#### Step 2: Provider Service - Expose Integration Services + +```csharp +// In CatalogServiceModule.cs +Configure(options => +{ + options.ExposeIntegrationServices = true; +}); +``` + +#### Step 3: Consumer Service - Add Package Reference + +Add reference to provider's Contracts project (via ABP Studio or manually): +- Right-click OrderingService → Add Package Reference → Select `CatalogService.Contracts` + +#### Step 4: Consumer Service - Generate Proxies + +```bash +# Run ABP CLI in consumer service folder +abp generate-proxy -t csharp -u http://localhost:44361 -m catalog --without-contracts +``` + +Or use ABP Studio: Right-click service → ABP CLI → Generate Proxy → C# + +#### Step 5: Consumer Service - Register HTTP Client Proxies + +```csharp +// In OrderingServiceModule.cs +[DependsOn(typeof(CatalogServiceContractsModule))] // Add module dependency +public class OrderingServiceModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Register static HTTP client proxies + context.Services.AddStaticHttpClientProxies( + typeof(CatalogServiceContractsModule).Assembly, + "CatalogService"); + } +} +``` + +#### Step 6: Consumer Service - Configure Remote Service URL + +```json +// appsettings.json +"RemoteServices": { + "CatalogService": { + "BaseUrl": "http://localhost:44361" + } +} +``` + +#### Step 7: Use Integration Service + +```csharp +public class OrderAppService : ApplicationService +{ + private readonly IProductIntegrationService _productIntegrationService; + + public async Task> GetListAsync() + { + var orders = await _orderRepository.GetListAsync(); + var productIds = orders.Select(o => o.ProductId).Distinct().ToList(); + + // Call remote service via generated proxy + var products = await _productIntegrationService.GetProductsByIdsAsync(productIds); + // ... + } +} +``` + +> **Why Integration Services?** Application services are for UI - they have different authorization, validation, and optimization needs. Integration services are designed specifically for inter-service communication. + +**When to use:** Need immediate response, data required to complete current operation (e.g., get product details to display in order list). + +### 2. Distributed Events (Asynchronous) + +Use RabbitMQ-based events for loose coupling. + +**When to use:** +- Notifying other services about state changes (e.g., "order placed", "stock updated") +- Operations that don't need immediate response +- When services should remain independent and decoupled + +```csharp +// Define ETO in Contracts project +[EventName("Product.StockChanged")] +public class StockCountChangedEto +{ + public Guid ProductId { get; set; } + public int NewCount { get; set; } +} + +// Publish +await _distributedEventBus.PublishAsync(new StockCountChangedEto { ... }); + +// Subscribe in another service +public class StockChangedHandler : IDistributedEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(StockCountChangedEto eventData) { ... } +} +``` + +DbContext must implement `IHasEventInbox`, `IHasEventOutbox` for Outbox/Inbox pattern. + +## Performance: Entity Cache + +For frequently accessed data from other services, use Entity Cache: + +```csharp +// Register +context.Services.AddEntityCache(); + +// Use - auto-invalidates on entity changes +private readonly IEntityCache _productCache; + +public async Task GetProductAsync(Guid id) +{ + return await _productCache.GetAsync(id); +} +``` + +## Pre-Configured Infrastructure + +- **RabbitMQ** - Distributed events with Outbox/Inbox +- **Redis** - Distributed cache and locking +- **YARP** - API Gateway +- **OpenIddict** - Auth server + +## Best Practices + +- **Choose communication wisely** - Synchronous for queries needing immediate data, asynchronous for notifications and state changes +- **Use Integration Services** - Not application services for inter-service calls +- **Cache remote data** - Use Entity Cache or IDistributedCache for frequently accessed data +- **Share only Contracts** - Never share implementations +- **Idempotent handlers** - Events may be delivered multiple times +- **Database per service** - Each service owns its database diff --git a/ai-rules/template-specific/module.mdc b/ai-rules/template-specific/module.mdc new file mode 100644 index 0000000000..c60f54239e --- /dev/null +++ b/ai-rules/template-specific/module.mdc @@ -0,0 +1,234 @@ +--- +description: "ABP Module solution template specific patterns" +alwaysApply: false +--- + +# ABP Module Solution Template + +> **Docs**: https://abp.io/docs/latest/solution-templates/application-module + +This template is for developing reusable ABP modules. Key requirement: **extensibility** - consumers must be able to override and customize module behavior. + +## Solution Structure + +``` +MyModule/ +├── src/ +│ ├── MyModule.Domain.Shared/ # Constants, enums, localization +│ ├── MyModule.Domain/ # Entities, repository interfaces, domain services +│ ├── MyModule.Application.Contracts/ # DTOs, service interfaces +│ ├── MyModule.Application/ # Service implementations +│ ├── MyModule.EntityFrameworkCore/ # EF Core implementation +│ ├── MyModule.MongoDB/ # MongoDB implementation +│ ├── MyModule.HttpApi/ # REST controllers +│ ├── MyModule.HttpApi.Client/ # Client proxies +│ ├── MyModule.Web/ # MVC/Razor Pages UI +│ └── MyModule.Blazor/ # Blazor UI +├── test/ +│ └── MyModule.Tests/ +└── host/ + └── MyModule.HttpApi.Host/ # Test host application +``` + +## Database Independence + +Support both EF Core and MongoDB: + +### Repository Interface (Domain) +```csharp +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); + Task> GetListByAuthorAsync(Guid authorId); +} +``` + +### EF Core Implementation +```csharp +public class BookRepository : EfCoreRepository, IBookRepository +{ + public async Task FindByNameAsync(string name) + { + var dbSet = await GetDbSetAsync(); + return await dbSet.FirstOrDefaultAsync(b => b.Name == name); + } +} +``` + +### MongoDB Implementation +```csharp +public class BookRepository : MongoDbRepository, IBookRepository +{ + public async Task FindByNameAsync(string name) + { + var queryable = await GetQueryableAsync(); + return await queryable.FirstOrDefaultAsync(b => b.Name == name); + } +} +``` + +## Table/Collection Prefix + +Allow customization to avoid naming conflicts: + +```csharp +// Domain.Shared +public static class MyModuleDbProperties +{ + public static string DbTablePrefix { get; set; } = "MyModule"; + public static string DbSchema { get; set; } = null; + + public const string ConnectionStringName = "MyModule"; +} +``` + +Usage: +```csharp +builder.Entity(b => +{ + b.ToTable(MyModuleDbProperties.DbTablePrefix + "Books", MyModuleDbProperties.DbSchema); +}); +``` + +## Module Options + +Provide configuration options: + +```csharp +// Domain +public class MyModuleOptions +{ + public bool EnableFeatureX { get; set; } = true; + public int MaxItemCount { get; set; } = 100; +} +``` + +Usage in module: +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + options.EnableFeatureX = true; + }); +} +``` + +Usage in service: +```csharp +public class MyService : ITransientDependency +{ + private readonly MyModuleOptions _options; + + public MyService(IOptions options) + { + _options = options.Value; + } +} +``` + +## Extensibility Points + +### Virtual Methods (Critical for Modules!) +When developing a reusable module, **all public and protected methods must be virtual** to allow consumers to override behavior: + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + // ✅ Public methods MUST be virtual + public virtual async Task CreateAsync(CreateBookDto input) + { + var book = await CreateBookEntityAsync(input); + await _bookRepository.InsertAsync(book); + return _bookMapper.MapToDto(book); + } + + // ✅ Use protected virtual for helper methods (not private) + protected virtual Task CreateBookEntityAsync(CreateBookDto input) + { + return Task.FromResult(new Book( + GuidGenerator.Create(), + input.Name, + input.Price + )); + } + + // ❌ WRONG for modules - private methods cannot be overridden + // private Book CreateBook(CreateBookDto input) { ... } +} +``` + +This allows module consumers to: +- Override specific methods without copying entire class +- Extend functionality while preserving base behavior +- Customize module behavior for their needs + +### Entity Extension +Support object extension system: +```csharp +public class MyModuleModuleExtensionConfigurator +{ + public static void Configure() + { + OneTimeRunner.Run(() => + { + ObjectExtensionManager.Instance.Modules() + .ConfigureMyModule(module => + { + module.ConfigureBook(book => + { + book.AddOrUpdateProperty("CustomProperty"); + }); + }); + }); + } +} +``` + +## Localization + +```csharp +// Domain.Shared +[LocalizationResourceName("MyModule")] +public class MyModuleResource +{ +} + +// Module configuration +Configure(options => +{ + options.Resources + .Add("en") + .AddVirtualJson("/Localization/MyModule"); +}); +``` + +## Permission Definition + +```csharp +public class MyModulePermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var myGroup = context.AddGroup( + MyModulePermissions.GroupName, + L("Permission:MyModule")); + + myGroup.AddPermission( + MyModulePermissions.Books.Default, + L("Permission:Books")); + } +} +``` + +## Best Practices + +1. **Virtual methods** - All public/protected methods must be `virtual` for extensibility +2. **Protected virtual helpers** - Use `protected virtual` instead of `private` for helper methods +3. **Database agnostic** - Support both EF Core and MongoDB +4. **Configurable** - Use options pattern for customization +5. **Localizable** - Use localization for all user-facing text +6. **Table prefix** - Allow customization to avoid conflicts +7. **Separate connection string** - Support dedicated database +8. **No dependencies on host** - Module should be self-contained +9. **Test with host app** - Include a host application for testing diff --git a/ai-rules/testing/patterns.mdc b/ai-rules/testing/patterns.mdc new file mode 100644 index 0000000000..07c9307448 --- /dev/null +++ b/ai-rules/testing/patterns.mdc @@ -0,0 +1,270 @@ +--- +description: "ABP testing patterns - unit tests and integration tests" +globs: "test/**/*.cs,tests/**/*.cs,**/*Tests*/**/*.cs,**/*Test*.cs" +alwaysApply: false +--- + +# ABP Testing Patterns + +> **Docs**: https://abp.io/docs/latest/testing + +## Test Project Structure + +| Project | Purpose | Base Class | +|---------|---------|------------| +| `*.Domain.Tests` | Domain logic, entities, domain services | `*DomainTestBase` | +| `*.Application.Tests` | Application services | `*ApplicationTestBase` | +| `*.EntityFrameworkCore.Tests` | Repository implementations | `*EntityFrameworkCoreTestBase` | + +## Integration Test Approach + +ABP recommends integration tests over unit tests: +- Tests run with real services and database (SQLite in-memory) +- No mocking of internal services +- Each test gets a fresh database instance + +## Application Service Test + +```csharp +public class BookAppService_Tests : MyProjectApplicationTestBase +{ + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Books() + { + // Act + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + + // Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "Test Book"); + } + + [Fact] + public async Task Should_Create_Book() + { + // Arrange + var input = new CreateBookDto + { + Name = "New Book", + Price = 19.99m + }; + + // Act + var result = await _bookAppService.CreateAsync(input); + + // Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New Book"); + result.Price.ShouldBe(19.99m); + } + + [Fact] + public async Task Should_Not_Create_Book_With_Invalid_Name() + { + // Arrange + var input = new CreateBookDto + { + Name = "", // Invalid + Price = 10m + }; + + // Act & Assert + await Should.ThrowAsync(async () => + { + await _bookAppService.CreateAsync(input); + }); + } +} +``` + +## Domain Service Test + +```csharp +public class BookManager_Tests : MyProjectDomainTestBase +{ + private readonly BookManager _bookManager; + private readonly IBookRepository _bookRepository; + + public BookManager_Tests() + { + _bookManager = GetRequiredService(); + _bookRepository = GetRequiredService(); + } + + [Fact] + public async Task Should_Create_Book() + { + // Act + var book = await _bookManager.CreateAsync("Test Book", 29.99m); + + // Assert + book.ShouldNotBeNull(); + book.Name.ShouldBe("Test Book"); + book.Price.ShouldBe(29.99m); + } + + [Fact] + public async Task Should_Not_Allow_Duplicate_Book_Name() + { + // Arrange + await _bookManager.CreateAsync("Existing Book", 10m); + + // Act & Assert + var exception = await Should.ThrowAsync(async () => + { + await _bookManager.CreateAsync("Existing Book", 20m); + }); + + exception.Code.ShouldBe("MyProject:BookNameAlreadyExists"); + } +} +``` + +## Test Naming Convention + +Use descriptive names: +```csharp +// Pattern: Should_ExpectedBehavior_When_Condition +public async Task Should_Create_Book_When_Input_Is_Valid() +public async Task Should_Throw_BusinessException_When_Name_Already_Exists() +public async Task Should_Return_Empty_List_When_No_Books_Exist() +``` + +## Arrange-Act-Assert (AAA) + +```csharp +[Fact] +public async Task Should_Update_Book_Price() +{ + // Arrange + var bookId = await CreateTestBookAsync(); + var newPrice = 39.99m; + + // Act + var result = await _bookAppService.UpdateAsync(bookId, new UpdateBookDto + { + Price = newPrice + }); + + // Assert + result.Price.ShouldBe(newPrice); +} +``` + +## Assertions with Shouldly + +ABP uses Shouldly library: +```csharp +result.ShouldNotBeNull(); +result.Name.ShouldBe("Expected Name"); +result.Price.ShouldBeGreaterThan(0); +result.Items.ShouldContain(x => x.Id == expectedId); +result.Items.ShouldBeEmpty(); +result.Items.Count.ShouldBe(5); + +// Exception assertions +await Should.ThrowAsync(async () => +{ + await _service.DoSomethingAsync(); +}); + +var ex = await Should.ThrowAsync(async () => +{ + await _service.DoSomethingAsync(); +}); +ex.Code.ShouldBe("MyProject:ErrorCode"); +``` + +## Test Data Seeding + +```csharp +public class MyProjectTestDataSeedContributor : IDataSeedContributor, ITransientDependency +{ + public static readonly Guid TestBookId = Guid.Parse("..."); + + private readonly IBookRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public async Task SeedAsync(DataSeedContext context) + { + await _bookRepository.InsertAsync( + new Book(TestBookId, "Test Book", 19.99m, Guid.Empty), + autoSave: true + ); + } +} +``` + +## Disabling Authorization in Tests + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddAlwaysAllowAuthorization(); +} +``` + +## Mocking External Services + +Use NSubstitute when needed: +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var emailSender = Substitute.For(); + emailSender.SendAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + context.Services.AddSingleton(emailSender); +} +``` + +## Testing with Specific User + +```csharp +[Fact] +public async Task Should_Get_Current_User_Books() +{ + // Login as specific user + await WithUnitOfWorkAsync(async () => + { + using (CurrentUser.Change(TestData.UserId)) + { + var result = await _bookAppService.GetMyBooksAsync(); + result.Items.ShouldAllBe(b => b.CreatorId == TestData.UserId); + } + }); +} +``` + +## Testing Multi-Tenancy + +```csharp +[Fact] +public async Task Should_Filter_Books_By_Tenant() +{ + using (CurrentTenant.Change(TestData.TenantId)) + { + var result = await _bookAppService.GetListAsync(new GetBookListDto()); + // Results should be filtered by tenant + } +} +``` + +## Best Practices + +- Each test should be independent +- Don't share state between tests +- Use meaningful test data +- Test edge cases and error conditions +- Keep tests focused on single behavior +- Use test data seeders for common data +- Avoid testing framework internals diff --git a/ai-rules/ui/angular.mdc b/ai-rules/ui/angular.mdc new file mode 100644 index 0000000000..e61881fb28 --- /dev/null +++ b/ai-rules/ui/angular.mdc @@ -0,0 +1,221 @@ +--- +description: "ABP Angular UI patterns and best practices" +globs: "**/angular/**/*.ts,**/angular/**/*.html,**/*.component.ts" +alwaysApply: false +--- + +# ABP Angular UI + +> **Docs**: https://abp.io/docs/latest/framework/ui/angular/overview + +## Project Structure +``` +src/app/ +├── proxy/ # Auto-generated service proxies +├── shared/ # Shared components, pipes, directives +├── book/ # Feature module +│ ├── book.module.ts +│ ├── book-routing.module.ts +│ ├── book-list/ +│ │ ├── book-list.component.ts +│ │ ├── book-list.component.html +│ │ └── book-list.component.scss +│ └── book-detail/ +``` + +## Generate Service Proxies +```bash +abp generate-proxy -t ng +``` + +This generates typed service classes in `src/app/proxy/`. + +## List Component Pattern +```typescript +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html' +}) +export class BookListComponent implements OnInit { + books = { items: [], totalCount: 0 } as PagedResultDto; + + constructor( + public readonly list: ListService, + private bookService: BookService, + private confirmation: ConfirmationService + ) {} + + ngOnInit(): void { + this.hookToQuery(); + } + + private hookToQuery(): void { + this.list.hookToQuery(query => + this.bookService.getList(query) + ).subscribe(response => { + this.books = response; + }); + } + + create(): void { + // Open create modal + } + + delete(book: BookDto): void { + this.confirmation + .warn('::AreYouSureToDelete', '::AreYouSure') + .subscribe(status => { + if (status === Confirmation.Status.confirm) { + this.bookService.delete(book.id).subscribe(() => this.list.get()); + } + }); + } +} +``` + +## Localization +```typescript +// In component +constructor(private localizationService: LocalizationService) {} + +getText(): string { + return this.localizationService.instant('::Books'); +} +``` + +```html + +

{{ '::Books' | abpLocalization }}

+ + +

{{ '::WelcomeMessage' | abpLocalization: userName }}

+``` + +## Authorization + +### Permission Directive +```html + +``` + +### Permission Guard +```typescript +const routes: Routes = [ + { + path: '', + component: BookListComponent, + canActivate: [PermissionGuard], + data: { + requiredPolicy: 'BookStore.Books' + } + } +]; +``` + +### Programmatic Check +```typescript +constructor(private permissionService: PermissionService) {} + +canCreate(): boolean { + return this.permissionService.getGrantedPolicy('BookStore.Books.Create'); +} +``` + +## Forms with Validation +```typescript +@Component({...}) +export class BookFormComponent { + form: FormGroup; + + constructor(private fb: FormBuilder) { + this.buildForm(); + } + + buildForm(): void { + this.form = this.fb.group({ + name: ['', [Validators.required, Validators.maxLength(128)]], + price: [0, [Validators.required, Validators.min(0)]] + }); + } + + save(): void { + if (this.form.invalid) return; + + this.bookService.create(this.form.value).subscribe(() => { + // Handle success + }); + } +} +``` + +```html +
+
+ + +
+ + +
+``` + +## Configuration API +```typescript +constructor(private configService: ConfigStateService) {} + +getCurrentUser(): CurrentUserDto { + return this.configService.getOne('currentUser'); +} + +getSettings(): void { + const setting = this.configService.getSetting('MyApp.MaxItemCount'); +} +``` + +## Modal Service +```typescript +constructor(private modalService: ModalService) {} + +openCreateModal(): void { + const modalRef = this.modalService.open(BookFormComponent, { + size: 'lg' + }); + + modalRef.result.then(result => { + if (result) { + this.list.get(); + } + }); +} +``` + +## Toast Notifications +```typescript +constructor(private toaster: ToasterService) {} + +showSuccess(): void { + this.toaster.success('::BookCreatedSuccessfully', '::Success'); +} + +showError(error: string): void { + this.toaster.error(error, '::Error'); +} +``` + +## Lazy Loading Modules +```typescript +// app-routing.module.ts +const routes: Routes = [ + { + path: 'books', + loadChildren: () => import('./book/book.module').then(m => m.BookModule) + } +]; +``` + +## Theme & Styling +- Use Bootstrap classes +- ABP provides theme variables via CSS custom properties +- Component-specific styles in `.component.scss` diff --git a/ai-rules/ui/blazor.mdc b/ai-rules/ui/blazor.mdc new file mode 100644 index 0000000000..68b8051109 --- /dev/null +++ b/ai-rules/ui/blazor.mdc @@ -0,0 +1,207 @@ +--- +description: "ABP Blazor UI patterns and components" +globs: "**/*.razor,**/Blazor/**/*.cs,**/*.Blazor*/**/*.cs" +alwaysApply: false +--- + +# ABP Blazor UI + +> **Docs**: https://abp.io/docs/latest/framework/ui/blazor/overall + +## Component Base Classes + +### Basic Component +```razor +@inherits AbpComponentBase + +

@L["Books"]

+``` + +### CRUD Page +```razor +@page "/books" +@inherits AbpCrudPageBase + + + + + +

@L["Books"]

+
+ + @if (HasCreatePermission) + { + + } + +
+
+ + + + + + + + + + + + + + + + +
+``` + +## Localization +```razor +@* Using L property from base class *@ +

@L["PageTitle"]

+ +@* With parameters *@ +

@L["WelcomeMessage", CurrentUser.UserName]

+``` + +## Authorization +```razor +@* Check permission before rendering *@ +@if (await AuthorizationService.IsGrantedAsync("MyPermission")) +{ + +} + +@* Using policy-based authorization *@ + + +

You have access!

+
+
+``` + +## Navigation & Menu +Configure in `*MenuContributor.cs`: + +```csharp +public class MyMenuContributor : IMenuContributor +{ + public async Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + var bookMenu = new ApplicationMenuItem( + "Books", + l["Menu:Books"], + "/books", + icon: "fa fa-book" + ); + + if (await context.IsGrantedAsync(MyPermissions.Books.Default)) + { + context.Menu.AddItem(bookMenu); + } + } + } +} +``` + +## Notifications & Messages +```csharp +// Success message +await Message.Success(L["BookCreatedSuccessfully"]); + +// Confirmation dialog +if (await Message.Confirm(L["AreYouSure"])) +{ + // User confirmed +} + +// Toast notification +await Notify.Success(L["OperationCompleted"]); +``` + +## Forms & Validation +```razor +
+ + + + @L["Name"] + + + + + + + + +
+``` + +## JavaScript Interop +```csharp +@inject IJSRuntime JsRuntime + +@code { + private async Task CallJavaScript() + { + await JsRuntime.InvokeVoidAsync("myFunction", arg1, arg2); + var result = await JsRuntime.InvokeAsync("myFunctionWithReturn"); + } +} +``` + +## State Management +```csharp +// Inject service proxy from HttpApi.Client +@inject IBookAppService BookAppService + +@code { + private List Books { get; set; } + + protected override async Task OnInitializedAsync() + { + var result = await BookAppService.GetListAsync(new PagedAndSortedResultRequestDto()); + Books = result.Items.ToList(); + } +} +``` + +## Code-Behind Pattern +**Books.razor:** +```razor +@page "/books" +@inherits BooksBase +``` + +**Books.razor.cs:** +```csharp +public partial class Books : BooksBase +{ + // Component logic here +} +``` + +**BooksBase.cs:** +```csharp +public abstract class BooksBase : AbpComponentBase +{ + [Inject] + protected IBookAppService BookAppService { get; set; } +} +``` diff --git a/ai-rules/ui/mvc.mdc b/ai-rules/ui/mvc.mdc new file mode 100644 index 0000000000..1bc423010d --- /dev/null +++ b/ai-rules/ui/mvc.mdc @@ -0,0 +1,258 @@ +--- +description: "ABP MVC and Razor Pages UI patterns" +globs: "**/*.cshtml,**/Pages/**/*.cs,**/Views/**/*.cs,**/Controllers/**/*.cs" +alwaysApply: false +--- + +# ABP MVC / Razor Pages UI + +> **Docs**: https://abp.io/docs/latest/framework/ui/mvc-razor-pages/overall + +## Razor Page Model +```csharp +public class IndexModel : AbpPageModel +{ + private readonly IBookAppService _bookAppService; + + public List Books { get; set; } + + public IndexModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnGetAsync() + { + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + Books = result.Items.ToList(); + } +} +``` + +## Razor Page View +```html +@page +@model IndexModel + + + + + +

@L["Books"]

+
+ + + +
+
+ + + + + @L["Name"] + @L["Price"] + @L["Actions"] + + + + @foreach (var book in Model.Books) + { + + @book.Name + @book.Price + + + + + } + + + +
+``` + +## ABP Tag Helpers + +### Cards +```html + + Header + Content + Footer + +``` + +### Buttons +```html + + +``` + +### Forms +```html + + + + + + + + + +``` + +### Tables +```html + + + +``` + +## Localization +```html +@* In Razor views/pages *@ +

@L["Books"]

+ +@* With parameters *@ +

@L["WelcomeMessage", Model.UserName]

+``` + +## JavaScript API +```javascript +// Localization +var text = abp.localization.getResource('BookStore')('Books'); + +// Authorization +if (abp.auth.isGranted('BookStore.Books.Create')) { + // Show create button +} + +// Settings +var maxCount = abp.setting.get('BookStore.MaxItemCount'); + +// Ajax with automatic error handling +abp.ajax({ + url: '/api/app/book', + type: 'POST', + data: JSON.stringify(bookData) +}).then(function(result) { + // Success +}); + +// Notifications +abp.notify.success('Book created successfully!'); +abp.notify.error('An error occurred!'); + +// Confirmation +abp.message.confirm('Are you sure?').then(function(confirmed) { + if (confirmed) { + // User confirmed + } +}); +``` + +## DataTables Integration +```javascript +var dataTable = $('#BooksTable').DataTable( + abp.libs.datatables.normalizeConfiguration({ + serverSide: true, + paging: true, + ajax: abp.libs.datatables.createAjax(bookService.getList), + columnDefs: [ + { + title: l('Name'), + data: 'name' + }, + { + title: l('Price'), + data: 'price', + render: function(data) { + return data.toFixed(2); + } + }, + { + title: l('Actions'), + rowAction: { + items: [ + { + text: l('Edit'), + visible: abp.auth.isGranted('BookStore.Books.Edit'), + action: function(data) { + editModal.open({ id: data.record.id }); + } + }, + { + text: l('Delete'), + visible: abp.auth.isGranted('BookStore.Books.Delete'), + confirmMessage: function(data) { + return l('BookDeletionConfirmationMessage', data.record.name); + }, + action: function(data) { + bookService.delete(data.record.id).then(function() { + abp.notify.success(l('SuccessfullyDeleted')); + dataTable.ajax.reload(); + }); + } + } + ] + } + } + ] + }) +); +``` + +## Modal Pages +**CreateModal.cshtml:** +```html +@page +@model CreateModalModel + + + + + + + + + + +``` + +**CreateModal.cshtml.cs:** +```csharp +public class CreateModalModel : AbpPageModel +{ + [BindProperty] + public CreateBookDto Book { get; set; } + + private readonly IBookAppService _bookAppService; + + public CreateModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnPostAsync() + { + await _bookAppService.CreateAsync(Book); + return NoContent(); + } +} +``` + +## Bundle & Minification +```csharp +Configure(options => +{ + options.StyleBundles.Configure( + StandardBundles.Styles.Global, + bundle => bundle.AddFiles("/styles/my-styles.css") + ); +}); +``` diff --git a/docs/en/Community-Articles/2026-01-16-meet-abio-at-ndc-london/post.md b/docs/en/Community-Articles/2026-01-16-meet-abio-at-ndc-london/post.md new file mode 100644 index 0000000000..4890b42174 --- /dev/null +++ b/docs/en/Community-Articles/2026-01-16-meet-abio-at-ndc-london/post.md @@ -0,0 +1,17 @@ +We are thrilled to announce that **ABP.IO will be sponsoring [NDC London 2026](https://ndclondon.com/),** making the start of 2026 a very exciting time for us\! + +NDC London is going to take place from **26th-30th January 2026 at Queen Elizabeth II Center.** This 5-Day event for software developers will have over 90 speakers and 100 sessions. We are excited to be a part of this amazing event once more as devoted supporters of the software development community\! + +## Conference Tracks, Topics, and What Developers Can Expect + +Developers attending **NDC London 2026** can expect five focused tracks packed with practical, real-world sessions. The conference covers the modern development stack, including **.NET, JavaScript, Cloud, DevOps, Security, Testing, UX, Web**, and emerging technologies, delivered by industry experts with actionable insights developers can apply immediately. + +## Discover Previous NDC Events + +We have shared **our takeaways from past NDC events** and other conferences [**here**](https://abp.io/community/events/sponsored#gsc.tab=0). You can check them out to learn what we discovered along the way\! + +## Stop By Our Booth and Say Hello + +We can’t wait to meet fellow developers at NDC London 2026, have meaningful conversations and connect in person. **If you are stopping by our booth, don’t miss our raffle\!** We will be giving away a nice surprise during the event\! + +We are looking forward to meeting you there and sharing a few great days focused on software development. See you there\! diff --git a/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/PuppeteerSharp.png b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/PuppeteerSharp.png new file mode 100644 index 0000000000..0a2b72ab1c Binary files /dev/null and b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/PuppeteerSharp.png differ diff --git a/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/QuestPDF.png b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/QuestPDF.png new file mode 100644 index 0000000000..ef93db3d48 Binary files /dev/null and b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/QuestPDF.png differ diff --git a/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/article.md b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/article.md new file mode 100644 index 0000000000..ff613b0aed --- /dev/null +++ b/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.* + diff --git a/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/cover.png b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/cover.png new file mode 100644 index 0000000000..bd40d5d2d8 Binary files /dev/null and b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/cover.png differ diff --git a/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/itext.jpg b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/itext.jpg new file mode 100644 index 0000000000..f0b2df04e2 Binary files /dev/null and b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/itext.jpg differ diff --git a/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/pdfsharp.png b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/pdfsharp.png new file mode 100644 index 0000000000..b9f1c0c203 Binary files /dev/null and b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/pdfsharp.png differ diff --git a/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/playwright.png b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/playwright.png new file mode 100644 index 0000000000..71a50d9e82 Binary files /dev/null and b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/playwright.png differ diff --git a/docs/en/framework/ui/angular/data-table-column-extensions.md b/docs/en/framework/ui/angular/data-table-column-extensions.md index 0365336d81..81312cdd75 100644 --- a/docs/en/framework/ui/angular/data-table-column-extensions.md +++ b/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) diff --git a/docs/en/framework/ui/angular/extensible-table-row-detail.md b/docs/en/framework/ui/angular/extensible-table-row-detail.md new file mode 100644 index 0000000000..0ae24e69e1 --- /dev/null +++ b/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 `` component allows you to add expandable row details to any ``. When users click the expand icon, additional content is revealed below the row. + +Extensible Table Row Detail Example + +## 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 `` inside `` with an `ng-template`: + +```html + + + +
+
{%{{{ row.name }}}%}
+

ID: {%{{{ row.id }}}%}

+

Status: {%{{{ row.isActive ? 'Active' : 'Inactive' }}}%}

+
+
+
+
+``` + +An expand/collapse chevron icon will automatically appear in the first column of each row. + +## API + +### ExtensibleTableRowDetailComponent + +| Input | Type | Default | Description | +|-------|------|---------|-------------| +| `rowHeight` | `string` | `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 + + + +
+ Details for: {%{{{ row.name }}}%} +
{%{{{ row | json }}}%}
+
+
+
+
+``` + +### With Custom Row Height + +Specify a fixed height for the detail area: + +```html + + +
Fixed 200px height content
+
+
+``` + +### Using Expanded State + +Apply conditional styling based on expansion state: + +```html + + +
+

This row is {%{{{ expanded ? 'expanded' : 'collapsed' }}}%}

+
+
+
+``` + +### With Badges and Localization + +```html + + +
+
+
+

{%{{{ 'MyModule::Name' | abpLocalization }}}%}

+

{%{{{ row.name }}}%}

+
+
+

{%{{{ 'MyModule::Status' | abpLocalization }}}%}

+

+ @if (row.isActive) { + {%{{{ 'AbpUi::Yes' | abpLocalization }}}%} + } @else { + {%{{{ 'AbpUi::No' | abpLocalization }}}%} + } +

+
+
+
+
+
+``` + +## Alternative: Direct Template Input + +For simpler use cases, you can use the `rowDetailTemplate` input on `` directly: + +```html + + + +
{%{{{ row.name }}}%}
+
+``` + +## Events + +### rowDetailToggle + +The `rowDetailToggle` output emits when a row is expanded or collapsed: + +```html + + + ... + + +``` + +```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) diff --git a/docs/en/framework/ui/angular/images/row-detail-image.png b/docs/en/framework/ui/angular/images/row-detail-image.png new file mode 100644 index 0000000000..c99ff98ca0 Binary files /dev/null and b/docs/en/framework/ui/angular/images/row-detail-image.png differ diff --git a/docs/en/get-started/images/abp-studio-background-tasks.png b/docs/en/get-started/images/abp-studio-background-tasks.png index cf63394600..c3d020a90f 100644 Binary files a/docs/en/get-started/images/abp-studio-background-tasks.png and b/docs/en/get-started/images/abp-studio-background-tasks.png differ diff --git a/docs/en/get-started/images/abp-studio-created-microservice-solution-explorer.png b/docs/en/get-started/images/abp-studio-created-microservice-solution-explorer.png index 97936d018e..398be32048 100644 Binary files a/docs/en/get-started/images/abp-studio-created-microservice-solution-explorer.png and b/docs/en/get-started/images/abp-studio-created-microservice-solution-explorer.png differ diff --git a/docs/en/get-started/images/abp-studio-created-new-microservice-solution.png b/docs/en/get-started/images/abp-studio-created-new-microservice-solution.png index 7b13736509..28fa473185 100644 Binary files a/docs/en/get-started/images/abp-studio-created-new-microservice-solution.png and b/docs/en/get-started/images/abp-studio-created-new-microservice-solution.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-kubernetes-build-docker-images.png b/docs/en/get-started/images/abp-studio-microservice-kubernetes-build-docker-images.png index b2c3115f4b..a4730d1f61 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-kubernetes-build-docker-images.png and b/docs/en/get-started/images/abp-studio-microservice-kubernetes-build-docker-images.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-kubernetes-install-helm-chart.png b/docs/en/get-started/images/abp-studio-microservice-kubernetes-install-helm-chart.png index c4e4f6787f..a4f65d9214 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-kubernetes-install-helm-chart.png and b/docs/en/get-started/images/abp-studio-microservice-kubernetes-install-helm-chart.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-kubernetes-tab.png b/docs/en/get-started/images/abp-studio-microservice-kubernetes-tab.png index 16ff718210..400a4d1789 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-kubernetes-tab.png and b/docs/en/get-started/images/abp-studio-microservice-kubernetes-tab.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-applications.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-applications.png index 2bd1a2fe40..497ad5d065 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-applications.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-applications.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse-microservice.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse-microservice.png index 6b15ce18b3..49abf55666 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse-microservice.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse.png index da82711582..b633385243 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-1.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-1.png index 1d953a9552..aba03aedf4 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-1.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-1.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-2.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-2.png index 53152f1243..d3275453ab 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-2.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-2.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-external-service.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-external-service.png index 35fc51899e..d02ac37d39 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-external-service.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-external-service.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-watch-enabled-icon.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-watch-enabled-icon.png index 796d183ddc..d3539efffb 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-watch-enabled-icon.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-watch-enabled-icon.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner.png index 3e06813a46..4a856a62e5 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner.png differ diff --git a/docs/en/get-started/images/abp-studio-new-microservice-helm-charts.png b/docs/en/get-started/images/abp-studio-new-microservice-helm-charts.png index 42009900b3..de4476a9f1 100644 Binary files a/docs/en/get-started/images/abp-studio-new-microservice-helm-charts.png and b/docs/en/get-started/images/abp-studio-new-microservice-helm-charts.png differ diff --git a/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-optional-modules.png b/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-optional-modules.png index 3b1a4abe68..0ecc98f3ee 100644 Binary files a/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-optional-modules.png and b/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-optional-modules.png differ diff --git a/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-properties.png b/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-properties.png index 5f2a9b7938..13d0e679a8 100644 Binary files a/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-properties.png and b/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-properties.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options-microservice.png index cb85c3a2b8..ae89059631 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-services.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-services.png index 6bbf3fb22a..40f2037ac5 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-services.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-services.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-admin-password.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-admin-password.png new file mode 100644 index 0000000000..8040b491a3 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-admin-password.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-aspire-configuration-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-aspire-configuration-microservice.png index 74cd5066bb..507fdf0a7f 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-aspire-configuration-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-aspire-configuration-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-microservice.png index e41580ad1e..12f0003333 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-microservice.png index ed52bac62c..97255f2464 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-dynamic-localization.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-dynamic-localization.png index 455a6bd574..3564685c5c 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-dynamic-localization.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-dynamic-localization.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-languages-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-languages-microservice.png index 705fee9325..1797a4553c 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-languages-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-languages-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-microservice.png index 9b9210b269..d1ec05e4c8 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework-microservice.png index 04e1af7ebe..004a473579 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy.png index 3c06ae001d..3311e76a76 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-public-web-site.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-public-web-site.png index 40325a5358..edad8a63db 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-public-web-site.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-public-web-site.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-microservice.png index 27e39f3776..08e30001e6 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme-microservice.png index 53692d03de..8755073d1d 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-open-module-folder.png b/docs/en/get-started/images/abp-studio-open-module-folder.png index b32f046d94..d945377143 100644 Binary files a/docs/en/get-started/images/abp-studio-open-module-folder.png and b/docs/en/get-started/images/abp-studio-open-module-folder.png differ diff --git a/docs/en/get-started/images/abp-studio-welcome-screen.png b/docs/en/get-started/images/abp-studio-welcome-screen.png index 938be4a010..0607762677 100644 Binary files a/docs/en/get-started/images/abp-studio-welcome-screen.png and b/docs/en/get-started/images/abp-studio-welcome-screen.png differ diff --git a/docs/en/get-started/microservice.md b/docs/en/get-started/microservice.md index 56f275c258..d30e3926fc 100644 --- a/docs/en/get-started/microservice.md +++ b/docs/en/get-started/microservice.md @@ -126,6 +126,12 @@ Click the Next button to see *Additional Services* screen: On that screen, allows you to include extra microservices in your ABP solution during the creation process. This feature lets you extend your solution with business-specific services right from the start. +Click the Next button to see *Admin Password* screen: + +![abp-studio-new-solution-dialog-admin-password](images/abp-studio-new-solution-dialog-admin-password.png) + +Here, you can set the initial password for the `admin` user of your application. By default, it is set to `1q2w3E*`, but you can change it to a more secure password of your choice. + Now, we are ready to allow ABP Studio to create our solution. Just click the *Create* button and let the ABP Studio do the rest for you. After clicking the *Create* button, the dialog is closed and your solution is loaded into ABP Studio: ![abp-studio-created-new-microservice-solution](images/abp-studio-created-new-microservice-solution.png) diff --git a/docs/en/suite/generating-crud-page.md b/docs/en/suite/generating-crud-page.md index afc2536709..160f89216f 100644 --- a/docs/en/suite/generating-crud-page.md +++ b/docs/en/suite/generating-crud-page.md @@ -270,6 +270,29 @@ In the example above, the `IdentityUser` entity is selected as the navigation pr > **Note:** Ensure that your solution is built properly before establishing relationship between your own entity and a module entity because ABP Suite scans assemblies and finds which ABP modules you are using and lists their entities in the navigation property model if you have checked the **Include entities from ABP modules** checkbox. +#### Extending with Custom Module Entities + +If you want to extend ABP Suite's system to list entities from your own custom modules (not just ABP's built-in modules), you can configure the `module-entity-extension.json` file. This file is located in the `.suite` folder at the root of your solution (`/.suite/module-entity-extension.json`). + +Here is the default sample file content: + +```json +{ + "Modules": [ + { + "DomainProjectDllFileName": "MySampleModule.MyProject.Domain.dll" + } + ] +} +``` + +By defining the `DomainProjectDllFileName` property, ABP Suite will scan the specified module's **.dll** and list its entities in the navigation property model. This allows you to create navigation properties that reference entities from your custom modules. + +> **Important:** When extending with custom module entities, ensure that: +> - Your current solution properly depends on the related module. +> - All module references are correctly configured. +> - The solution is built successfully before attempting to establish relationships. + #### Adding An Existing Entity as a Navigation Property Alternatively, you can add `IdentityUser` entity (or any other entity) as a navigation property to an entity by manually entering the required information. See the screenshot below: diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs index 65b7e1b390..159b72ad66 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs @@ -47,6 +47,7 @@ public class AbpAuthorizationModule : AbpModule options.ResourceValueProviders.Add(); options.ResourceValueProviders.Add(); + options.ResourceValueProviders.Add(); }); Configure(options => diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/ClientPermissionValueProvider.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/ClientPermissionValueProvider.cs index 13c7981063..d3ac6870d3 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/ClientPermissionValueProvider.cs +++ b/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)) diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ClientResourcePermissionValueProvider.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ClientResourcePermissionValueProvider.cs new file mode 100644 index 0000000000..2a73292528 --- /dev/null +++ b/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 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 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); + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/AbpHangfirePeriodicBackgroundWorkerAdapterOptions.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/AbpHangfirePeriodicBackgroundWorkerAdapterOptions.cs new file mode 100644 index 0000000000..2df335abd8 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/AbpHangfirePeriodicBackgroundWorkerAdapterOptions.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.BackgroundWorkers.Hangfire; + +public class AbpHangfirePeriodicBackgroundWorkerAdapterOptions +{ + public TimeZoneInfo TimeZone { get; set; } = TimeZoneInfo.Utc; + + public string Queue { get; set; } = default!; +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs index fe9a8ad983..64a4a1be64 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs @@ -5,7 +5,9 @@ using System.Threading; using System.Threading.Tasks; using Hangfire; using Hangfire.Common; +using Hangfire.Storage; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Abp.DynamicProxy; @@ -30,8 +32,9 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet BackgroundJobServer = ServiceProvider.GetRequiredService(); } - public async override Task AddAsync(IBackgroundWorker worker, CancellationToken cancellationToken = default) + public override async Task AddAsync(IBackgroundWorker worker, CancellationToken cancellationToken = default) { + var logger = ServiceProvider.GetRequiredService>(); var abpHangfireOptions = ServiceProvider.GetRequiredService>().Value; var defaultQueuePrefix = abpHangfireOptions.DefaultQueuePrefix; var defaultQueue = abpHangfireOptions.DefaultQueue; @@ -42,54 +45,90 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet { var unProxyWorker = ProxyHelper.UnProxy(hangfireBackgroundWorker); - RecurringJob.AddOrUpdate( - hangfireBackgroundWorker.RecurringJobId, - hangfireBackgroundWorker.Queue.IsNullOrWhiteSpace() ? defaultQueue : defaultQueuePrefix + hangfireBackgroundWorker.Queue, - () => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(cancellationToken), - hangfireBackgroundWorker.CronExpression, - new RecurringJobOptions - { - TimeZone = hangfireBackgroundWorker.TimeZone - }); + var queueName = hangfireBackgroundWorker.Queue.IsNullOrWhiteSpace() ? defaultQueue : defaultQueuePrefix + hangfireBackgroundWorker.Queue; + if (!JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty)) + { + logger.LogError($"Current storage doesn't support specifying queues({queueName}) directly for a specific job. Please use the QueueAttribute instead."); + RecurringJob.AddOrUpdate( + hangfireBackgroundWorker.RecurringJobId, + () => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(cancellationToken), + hangfireBackgroundWorker.CronExpression, + new RecurringJobOptions + { + TimeZone = hangfireBackgroundWorker.TimeZone + }); + } + else + { + RecurringJob.AddOrUpdate( + hangfireBackgroundWorker.RecurringJobId, + queueName, + () => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(cancellationToken), + hangfireBackgroundWorker.CronExpression, + new RecurringJobOptions + { + TimeZone = hangfireBackgroundWorker.TimeZone + }); + } break; } case AsyncPeriodicBackgroundWorkerBase or PeriodicBackgroundWorkerBase: { int? period = null; - string? CronExpression = null; + string? cronExpression = null; - if (worker is AsyncPeriodicBackgroundWorkerBase asyncPeriodicBackgroundWorkerBase) + switch (worker) { + case AsyncPeriodicBackgroundWorkerBase asyncPeriodicBackgroundWorkerBase: period = asyncPeriodicBackgroundWorkerBase.Period; - CronExpression = asyncPeriodicBackgroundWorkerBase.CronExpression; - } - else if (worker is PeriodicBackgroundWorkerBase periodicBackgroundWorkerBase) - { + cronExpression = asyncPeriodicBackgroundWorkerBase.CronExpression; + break; + case PeriodicBackgroundWorkerBase periodicBackgroundWorkerBase: period = periodicBackgroundWorkerBase.Period; - CronExpression = periodicBackgroundWorkerBase.CronExpression; + cronExpression = periodicBackgroundWorkerBase.CronExpression; + break; } - if (period == null && CronExpression.IsNullOrWhiteSpace()) + if (period == null && cronExpression.IsNullOrWhiteSpace()) { + logger.LogError( + $"Cannot add periodic background worker {worker.GetType().FullName} to Hangfire scheduler, because both Period and CronExpression are not set. " + + "You can either set Period or CronExpression property of the worker." + ); return; } - var adapterType = typeof(HangfirePeriodicBackgroundWorkerAdapter<>).MakeGenericType(ProxyHelper.GetUnProxiedType(worker)); - var workerAdapter = (Activator.CreateInstance(adapterType) as IHangfireBackgroundWorker)!; - + var workerAdapter = (ServiceProvider.GetRequiredService(typeof(HangfirePeriodicBackgroundWorkerAdapter<>).MakeGenericType(ProxyHelper.GetUnProxiedType(worker))) as IHangfireBackgroundWorker)!; Expression> methodCall = () => workerAdapter.DoWorkAsync(cancellationToken); var recurringJobId = !workerAdapter.RecurringJobId.IsNullOrWhiteSpace() ? workerAdapter.RecurringJobId : GetRecurringJobId(worker, methodCall); - RecurringJob.AddOrUpdate( - recurringJobId, - workerAdapter.Queue.IsNullOrWhiteSpace() ? defaultQueue : defaultQueuePrefix + workerAdapter.Queue, - methodCall, - CronExpression ?? GetCron(period!.Value), - new RecurringJobOptions - { - TimeZone = workerAdapter.TimeZone - }); + var queueName = workerAdapter.Queue.IsNullOrWhiteSpace() ? defaultQueue : defaultQueuePrefix + workerAdapter.Queue; + if (!JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty)) + { + logger.LogError($"Current storage doesn't support specifying queues({queueName}) directly for a specific job. Please use the QueueAttribute instead."); + RecurringJob.AddOrUpdate( + recurringJobId, + methodCall, + cronExpression ?? GetCron(period!.Value), + new RecurringJobOptions + { + TimeZone = workerAdapter.TimeZone + }); + } + else + { + RecurringJob.AddOrUpdate( + recurringJobId, + queueName, + methodCall, + cronExpression ?? GetCron(period!.Value), + new RecurringJobOptions + { + TimeZone = workerAdapter.TimeZone + }); + } + break; } default: @@ -98,7 +137,7 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet } } - private readonly static MethodInfo? GetRecurringJobIdMethodInfo = typeof(RecurringJob).GetMethod("GetRecurringJobId", BindingFlags.NonPublic | BindingFlags.Static); + private static readonly MethodInfo? GetRecurringJobIdMethodInfo = typeof(RecurringJob).GetMethod("GetRecurringJobId", BindingFlags.NonPublic | BindingFlags.Static); protected virtual string? GetRecurringJobId(IBackgroundWorker worker, Expression> methodCall) { string? recurringJobId = null; diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfirePeriodicBackgroundWorkerAdapter.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfirePeriodicBackgroundWorkerAdapter.cs index 43e9b4a95c..cf5945aaf1 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfirePeriodicBackgroundWorkerAdapter.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfirePeriodicBackgroundWorkerAdapter.cs @@ -1,7 +1,9 @@ -using System.Reflection; +using System; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Volo.Abp.BackgroundWorkers.Hangfire; @@ -11,14 +13,17 @@ public class HangfirePeriodicBackgroundWorkerAdapter : HangfireBackgrou private readonly MethodInfo _doWorkAsyncMethod; private readonly MethodInfo _doWorkMethod; - public HangfirePeriodicBackgroundWorkerAdapter() + public HangfirePeriodicBackgroundWorkerAdapter(IOptions options) { + TimeZone = options.Value.TimeZone; + Queue = options.Value.Queue; + RecurringJobId = BackgroundWorkerNameAttribute.GetNameOrNull(); + _doWorkAsyncMethod = typeof(TWorker).GetMethod("DoWorkAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; _doWorkMethod = typeof(TWorker).GetMethod("DoWork", BindingFlags.Instance | BindingFlags.NonPublic)!; - RecurringJobId = BackgroundWorkerNameAttribute.GetNameOrNull(); } - public async override Task DoWorkAsync(CancellationToken cancellationToken = default) + public override async Task DoWorkAsync(CancellationToken cancellationToken = default) { var workerContext = new PeriodicBackgroundWorkerContext(ServiceProvider, cancellationToken); var worker = ServiceProvider.GetRequiredService(); @@ -26,13 +31,11 @@ public class HangfirePeriodicBackgroundWorkerAdapter : HangfireBackgrou switch (worker) { case AsyncPeriodicBackgroundWorkerBase asyncPeriodicBackgroundWorker: - await (Task)(_doWorkAsyncMethod.Invoke(asyncPeriodicBackgroundWorker, new object[] { workerContext })!); + await (Task)(_doWorkAsyncMethod.Invoke(asyncPeriodicBackgroundWorker, [workerContext])!); break; case PeriodicBackgroundWorkerBase periodicBackgroundWorker: - _doWorkMethod.Invoke(periodicBackgroundWorker, new object[] { workerContext }); + _doWorkMethod.Invoke(periodicBackgroundWorker, [workerContext]); break; } } - - } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Internal/RecreateInitialMigrationCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Internal/RecreateInitialMigrationCommand.cs index 2b65b9d00b..c3641cac15 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Internal/RecreateInitialMigrationCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Internal/RecreateInitialMigrationCommand.cs @@ -54,6 +54,14 @@ public class RecreateInitialMigrationCommand : IConsoleCommand, ITransientDepend Directory.Delete(Path.Combine(projectDir, "TenantMigrations"), true); separateDbContext = true; } + + CmdHelper.RunCmd("dotnet build", workingDirectory: projectDir, exitCode: out var exitCode); + if (exitCode != 0) + { + Logger.LogError("Build failed for project {Project}. Skipping migration recreation.", csprojFile); + continue; + } + if (!separateDbContext) { CmdHelper.RunCmd($"dotnet ef migrations add Initial", workingDirectory: projectDir); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/InitialMigrationCreator.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/InitialMigrationCreator.cs index 860fcef60f..2288eeccf7 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/InitialMigrationCreator.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/InitialMigrationCreator.cs @@ -14,7 +14,7 @@ public class InitialMigrationCreator : ITransientDependency public ICmdHelper CmdHelper { get; } public DotnetEfToolManager DotnetEfToolManager { get; } public ILogger Logger { get; set; } - + public InitialMigrationCreator(ICmdHelper cmdHelper, DotnetEfToolManager dotnetEfToolManager) { CmdHelper = cmdHelper; @@ -30,11 +30,11 @@ public class InitialMigrationCreator : ITransientDependency Logger.LogError($"This path doesn't exist: {targetProjectFolder}"); return false; } - + Logger.LogInformation("Creating initial migrations..."); await DotnetEfToolManager.BeSureInstalledAsync(); - + var tenantDbContextName = FindTenantDbContextName(targetProjectFolder); var dbContextName = tenantDbContextName != null ? FindDbContextName(targetProjectFolder) @@ -60,7 +60,7 @@ public class InitialMigrationCreator : ITransientDependency return migrationSuccess; } - + private string FindTenantDbContextName(string projectFolder) { var tenantDbContext = Directory.GetFiles(projectFolder, "*TenantMigrationsDbContext.cs", SearchOption.AllDirectories) @@ -93,6 +93,12 @@ public class InitialMigrationCreator : ITransientDependency private string AddMigrationAndGetOutput(string dbMigrationsFolder, string dbContext, string outputDirectory) { + var output = CmdHelper.RunCmdAndGetOutput("dotnet build", out int buildExitCode, dbMigrationsFolder); + if (buildExitCode != 0) + { + return output; + } + var dbContextOption = string.IsNullOrWhiteSpace(dbContext) ? string.Empty : $"--context {dbContext}"; @@ -108,4 +114,4 @@ public class InitialMigrationCreator : ITransientDependency output.Contains("To undo this action") && output.Contains("ef migrations remove")); } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/EfCoreMigrationManager.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/EfCoreMigrationManager.cs index ffa6a50edf..c4f1181fa3 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/EfCoreMigrationManager.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/EfCoreMigrationManager.cs @@ -44,13 +44,20 @@ public class EfCoreMigrationManager : ITransientDependency string dbContext, string outputDirectory) { + CmdHelper.RunCmd($"dotnet build", workingDirectory: dbMigrationsProjectFolder, exitCode: out var buildExitCode); + if (buildExitCode != 0) + { + Logger.LogWarning("Dotnet build failed for project folder {ProjectFolder}. Skipping EF Core migration command.", dbMigrationsProjectFolder); + return; + } + var dbContextOption = string.IsNullOrWhiteSpace(dbContext) ? string.Empty : $"--context {dbContext}"; CmdHelper.RunCmd($"dotnet ef migrations add {migrationName}" + $" --output-dir {outputDirectory}" + - $" {dbContextOption}", + $" {dbContextOption}", workingDirectory: dbMigrationsProjectFolder); } diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/DemoAppHangfireModule.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/DemoAppHangfireModule.cs index cd74c10f82..61a034c0a0 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/DemoAppHangfireModule.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/DemoAppHangfireModule.cs @@ -1,17 +1,23 @@ -using Hangfire; +using System; +using System.Threading.Tasks; +using Hangfire; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Autofac; using Volo.Abp.BackgroundJobs.DemoApp.Shared; using Volo.Abp.Modularity; using Microsoft.Extensions.Configuration; using Volo.Abp.BackgroundJobs.Hangfire; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.BackgroundWorkers.Hangfire; +using Volo.Abp.Hangfire; namespace Volo.Abp.BackgroundJobs.DemoApp.HangFire; [DependsOn( typeof(DemoAppSharedModule), typeof(AbpAutofacModule), - typeof(AbpBackgroundJobsHangfireModule) + typeof(AbpBackgroundJobsHangfireModule), + typeof(AbpBackgroundWorkersHangfireModule) )] public class DemoAppHangfireModule : AbpModule { @@ -24,4 +30,27 @@ public class DemoAppHangfireModule : AbpModule hangfireConfiguration.UseSqlServerStorage(configuration.GetConnectionString("Default")); }); } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.ServerOptions = new BackgroundJobServerOptions + { + Queues = new []{ "default", "my-default" } + }; + }); + + Configure(options => + { + options.TimeZone = TimeZoneInfo.Local; + options.Queue = "my-default"; + }); + } + + public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context) + { + var backgroundWorkerManager = context.ServiceProvider.GetRequiredService(); + await backgroundWorkerManager.AddAsync(context.ServiceProvider.GetRequiredService()); + } } diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/TestWorker.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/TestWorker.cs new file mode 100644 index 0000000000..60d7f7d365 --- /dev/null +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/TestWorker.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; +using Hangfire; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.Threading; + +namespace Volo.Abp.BackgroundJobs.DemoApp.HangFire; + +public class TestWorker : AsyncPeriodicBackgroundWorkerBase +{ + public TestWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory) + : base(timer, serviceScopeFactory) + { + CronExpression = Cron.Minutely(); + } + + protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) + { + Console.WriteLine($"[{DateTime.Now}] TestWorker executed."); + } +} diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/Volo.Abp.BackgroundJobs.DemoApp.HangFire.csproj b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/Volo.Abp.BackgroundJobs.DemoApp.HangFire.csproj index 3d145c93ef..c59e3afac9 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/Volo.Abp.BackgroundJobs.DemoApp.HangFire.csproj +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/Volo.Abp.BackgroundJobs.DemoApp.HangFire.csproj @@ -7,8 +7,10 @@ + + diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20201013055401_Initial.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20201013055401_Initial.cs deleted file mode 100644 index aa50d3f32a..0000000000 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20201013055401_Initial.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations; - -public partial class Initial : Migration -{ - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "AbpBackgroundJobs", - columns: table => new { - Id = table.Column(nullable: false), - ExtraProperties = table.Column(nullable: true), - ConcurrencyStamp = table.Column(maxLength: 40, nullable: true), - JobName = table.Column(maxLength: 128, nullable: false), - JobArgs = table.Column(maxLength: 1048576, nullable: false), - TryCount = table.Column(nullable: false, defaultValue: (short)0), - CreationTime = table.Column(nullable: false), - NextTryTime = table.Column(nullable: false), - LastTryTime = table.Column(nullable: true), - IsAbandoned = table.Column(nullable: false, defaultValue: false), - Priority = table.Column(nullable: false, defaultValue: (byte)15) - }, - constraints: table => - { - table.PrimaryKey("PK_AbpBackgroundJobs", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_AbpBackgroundJobs_IsAbandoned_NextTryTime", - table: "AbpBackgroundJobs", - columns: new[] { "IsAbandoned", "NextTryTime" }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AbpBackgroundJobs"); - } -} diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20201013055401_Initial.Designer.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.Designer.cs similarity index 73% rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20201013055401_Initial.Designer.cs rename to modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.Designer.cs index 35129e8877..3225815926 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20201013055401_Initial.Designer.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.Designer.cs @@ -8,20 +8,24 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Volo.Abp.BackgroundJobs.DemoApp.Db; using Volo.Abp.EntityFrameworkCore; +#nullable disable + namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations { [DbContext(typeof(DemoAppDbContext))] - [Migration("20201013055401_Initial")] + [Migration("20260119064307_Initial")] partial class Initial { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("ProductVersion", "3.1.8") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); modelBuilder.Entity("Volo.Abp.BackgroundJobs.BackgroundJobRecord", b => { @@ -29,19 +33,25 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("nvarchar(96)"); + b.Property("ConcurrencyStamp") .IsConcurrencyToken() - .HasColumnName("ConcurrencyStamp") + .IsRequired() + .HasMaxLength(40) .HasColumnType("nvarchar(40)") - .HasMaxLength(40); + .HasColumnName("ConcurrencyStamp"); b.Property("CreationTime") - .HasColumnName("CreationTime") - .HasColumnType("datetime2"); + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); b.Property("ExtraProperties") - .HasColumnName("ExtraProperties") - .HasColumnType("nvarchar(max)"); + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); b.Property("IsAbandoned") .ValueGeneratedOnAdd() @@ -50,13 +60,13 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations b.Property("JobArgs") .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(1048576); + .HasMaxLength(1048576) + .HasColumnType("nvarchar(max)"); b.Property("JobName") .IsRequired() - .HasColumnType("nvarchar(128)") - .HasMaxLength(128); + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); b.Property("LastTryTime") .HasColumnType("datetime2"); @@ -78,7 +88,7 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations b.HasIndex("IsAbandoned", "NextTryTime"); - b.ToTable("AbpBackgroundJobs"); + b.ToTable("AbpBackgroundJobs", (string)null); }); #pragma warning restore 612, 618 } diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.cs new file mode 100644 index 0000000000..ab0f0d7f37 --- /dev/null +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AbpBackgroundJobs", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ApplicationName = table.Column(type: "nvarchar(96)", maxLength: 96, nullable: true), + JobName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + JobArgs = table.Column(type: "nvarchar(max)", maxLength: 1048576, nullable: false), + TryCount = table.Column(type: "smallint", nullable: false, defaultValue: (short)0), + CreationTime = table.Column(type: "datetime2", nullable: false), + NextTryTime = table.Column(type: "datetime2", nullable: false), + LastTryTime = table.Column(type: "datetime2", nullable: true), + IsAbandoned = table.Column(type: "bit", nullable: false, defaultValue: false), + Priority = table.Column(type: "tinyint", nullable: false, defaultValue: (byte)15), + ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), + ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpBackgroundJobs", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AbpBackgroundJobs_IsAbandoned_NextTryTime", + table: "AbpBackgroundJobs", + columns: new[] { "IsAbandoned", "NextTryTime" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AbpBackgroundJobs"); + } + } +} diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/DemoAppDbContextModelSnapshot.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/DemoAppDbContextModelSnapshot.cs index 47ee56f4bb..ab91bc354c 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/DemoAppDbContextModelSnapshot.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/DemoAppDbContextModelSnapshot.cs @@ -7,6 +7,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Volo.Abp.BackgroundJobs.DemoApp.Db; using Volo.Abp.EntityFrameworkCore; +#nullable disable + namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations { [DbContext(typeof(DemoAppDbContext))] @@ -17,9 +19,10 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("ProductVersion", "3.1.8") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); modelBuilder.Entity("Volo.Abp.BackgroundJobs.BackgroundJobRecord", b => { @@ -27,19 +30,25 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("nvarchar(96)"); + b.Property("ConcurrencyStamp") .IsConcurrencyToken() - .HasColumnName("ConcurrencyStamp") + .IsRequired() + .HasMaxLength(40) .HasColumnType("nvarchar(40)") - .HasMaxLength(40); + .HasColumnName("ConcurrencyStamp"); b.Property("CreationTime") - .HasColumnName("CreationTime") - .HasColumnType("datetime2"); + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); b.Property("ExtraProperties") - .HasColumnName("ExtraProperties") - .HasColumnType("nvarchar(max)"); + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); b.Property("IsAbandoned") .ValueGeneratedOnAdd() @@ -48,13 +57,13 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations b.Property("JobArgs") .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(1048576); + .HasMaxLength(1048576) + .HasColumnType("nvarchar(max)"); b.Property("JobName") .IsRequired() - .HasColumnType("nvarchar(128)") - .HasMaxLength(128); + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); b.Property("LastTryTime") .HasColumnType("datetime2"); @@ -76,7 +85,7 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations b.HasIndex("IsAbandoned", "NextTryTime"); - b.ToTable("AbpBackgroundJobs"); + b.ToTable("AbpBackgroundJobs", (string)null); }); #pragma warning restore 612, 618 } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/UserRoleFinder.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/UserRoleFinder.cs index 48d8b01a73..633bbd1ed9 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/UserRoleFinder.cs +++ b/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(); } } diff --git a/modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/RoleResourcePermissionProviderKeyLookupService.cs b/modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/RoleResourcePermissionProviderKeyLookupService.cs index fa9fc31f79..ab1824e13d 100644 --- a/modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/RoleResourcePermissionProviderKeyLookupService.cs +++ b/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> SearchAsync(string[] keys, CancellationToken cancellationToken = default) + public virtual Task> 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()); } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientFinderResult.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientFinderResult.cs new file mode 100644 index 0000000000..d0c42968c7 --- /dev/null +++ b/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; } +} diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/IClientFinder.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/IClientFinder.cs new file mode 100644 index 0000000000..be96379902 --- /dev/null +++ b/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> SearchAsync(string filter, int page = 1); +} diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/FR.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/FR.json index 1d62307724..27edc5d80a 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/FR.json +++ b/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" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ar.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ar.json index 3329275161..ee9fd11913 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ar.json +++ b/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": "العميل" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/cs.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/cs.json index 1c5cb2bbf6..f77e12665b 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/cs.json +++ b/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" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/de.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/de.json index b0fc6bd640..3738380e70 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/de.json +++ b/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" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/el.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/el.json index 246db314c7..9528ee45de 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/el.json +++ b/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": "Πελάτης" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en-GB.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en-GB.json index 173379249c..aaf48fefee 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en-GB.json +++ b/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" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en.json index 82d4be83b0..71ee543544 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en.json +++ b/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" } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/es.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/es.json index 4c2ecd92ee..9cd39dea54 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/es.json +++ b/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" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fa.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fa.json index 8713511f43..ec7f8d355b 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fa.json +++ b/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": "کلاینت" } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fi.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fi.json index 94779f34b3..fe3f0747a9 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fi.json +++ b/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" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hi.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hi.json index 2df86f55bc..c1e2dbe4b5 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hi.json +++ b/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": "क्लाइंट" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hr.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hr.json index da79db308c..693bd3513e 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hr.json +++ b/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" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hu.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hu.json index e41b61c736..505e034724 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hu.json +++ b/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" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/is.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/is.json index 8f97265691..fc4b11af18 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/is.json +++ b/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" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/it.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/it.json index 1795289fa3..eb0e87c346 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/it.json +++ b/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" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/nl.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/nl.json index e8a7a5c0b1..baaca72b61 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/nl.json +++ b/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" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pl-PL.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pl-PL.json index a231a6ec3f..59a831a878 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pl-PL.json +++ b/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" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pt-BR.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pt-BR.json index db5d2fb00e..35ab5aa711 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pt-BR.json +++ b/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" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ro-RO.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ro-RO.json index a1af796373..9ab41e64bc 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ro-RO.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ro-RO.json @@ -11,6 +11,7 @@ "InvalidUsername": "Nume de utilizator sau parolă invalidă!", "InvalidAuthenticatorCode": "Cod de autentificare invalid!", "InvalidRecoveryCode": "Cod de recuperare nevalid!", - "TheTargetUserIsNotLinkedToYou": "Utilizatorul ţintă nu este conectat la dumneavoastră!" + "TheTargetUserIsNotLinkedToYou": "Utilizatorul ţintă nu este conectat la dumneavoastră!", + "ClientResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ru.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ru.json index 0c8a0f9fe5..90db053cd6 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ru.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ru.json @@ -11,6 +11,7 @@ "InvalidUsername": "Неверное имя пользователя или пароль!", "InvalidAuthenticatorCode": "Неверный код аутентификатора!", "InvalidRecoveryCode": "Неверный код восстановления!", - "TheTargetUserIsNotLinkedToYou": "Целевой пользователь не связан с вами!" + "TheTargetUserIsNotLinkedToYou": "Целевой пользователь не связан с вами!", + "ClientResourcePermissionProviderKeyLookupService": "Клиент" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sk.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sk.json index e0e31056fd..181ae21fa2 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sk.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sk.json @@ -11,6 +11,7 @@ "InvalidUsername": "Nesprávne používateľské meno alebo heslo!", "InvalidAuthenticatorCode": "Neplatný overovací kód!", "InvalidRecoveryCode": "Neplatný kód na obnovenie!", - "TheTargetUserIsNotLinkedToYou": "Cieľový používateľ nie je s vami prepojený!" + "TheTargetUserIsNotLinkedToYou": "Cieľový používateľ nie je s vami prepojený!", + "ClientResourcePermissionProviderKeyLookupService": "Klient" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sl.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sl.json index 527b11b35f..b6d653d406 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sl.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sl.json @@ -11,6 +11,7 @@ "InvalidUsername": "Napačno uporabniško ime ali geslo!", "InvalidAuthenticatorCode": "Neveljavna koda za preverjanje pristnosti!", "InvalidRecoveryCode": "Neveljavna obnovitvena koda!", - "TheTargetUserIsNotLinkedToYou": "Ciljni uporabnik ni povezan z vami!" + "TheTargetUserIsNotLinkedToYou": "Ciljni uporabnik ni povezan z vami!", + "ClientResourcePermissionProviderKeyLookupService": "Odjemalec" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sv.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sv.json index cdbf5d0dff..f0a1921bdf 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sv.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sv.json @@ -11,6 +11,7 @@ "InvalidUsername": "Ogiltigt användarnamn eller lösenord!", "InvalidAuthenticatorCode": "Ogiltig autentiseringskod!", "InvalidRecoveryCode": "Ogiltig återställningskod!", - "TheTargetUserIsNotLinkedToYou": "Målanvändaren är inte kopplad till dig!" + "TheTargetUserIsNotLinkedToYou": "Målanvändaren är inte kopplad till dig!", + "ClientResourcePermissionProviderKeyLookupService": "Klient" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/tr.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/tr.json index ef893f60ef..1d1e9e7c36 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/tr.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/tr.json @@ -11,6 +11,7 @@ "InvalidUsername": "Kullanıcı adı ya da şifre geçersiz!", "InvalidAuthenticatorCode": "Geçersiz kimlik doğrulama kodu!", "InvalidRecoveryCode": "Geçersiz kurtarma kodu!", - "TheTargetUserIsNotLinkedToYou": "Hedef kullanıcı sizinle bağlantılı değil!" + "TheTargetUserIsNotLinkedToYou": "Hedef kullanıcı sizinle bağlantılı değil!", + "ClientResourcePermissionProviderKeyLookupService": "İstemci" } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/vi.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/vi.json index 4d62d91611..6eeb3b68cf 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/vi.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/vi.json @@ -11,6 +11,7 @@ "InvalidUsername": "Sai username hoặc password!", "InvalidAuthenticatorCode": "Mã xác thực không hợp lệ!", "InvalidRecoveryCode": "Mã khôi phục không hợp lệ!", - "TheTargetUserIsNotLinkedToYou": "Người dùng mục tiêu không được liên kết với bạn!" + "TheTargetUserIsNotLinkedToYou": "Người dùng mục tiêu không được liên kết với bạn!", + "ClientResourcePermissionProviderKeyLookupService": "Máy khách" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hans.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hans.json index afb5824356..bbe4d7773d 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hans.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hans.json @@ -11,6 +11,7 @@ "InvalidUsername": "用户名或密码错误!", "InvalidAuthenticatorCode": "验证码无效!", "InvalidRecoveryCode": "恢复代码无效!", - "TheTargetUserIsNotLinkedToYou": "目标用户与您没有关联!" + "TheTargetUserIsNotLinkedToYou": "目标用户与您没有关联!", + "ClientResourcePermissionProviderKeyLookupService": "客户端" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hant.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hant.json index 3678d6b572..5e592e1409 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hant.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hant.json @@ -11,6 +11,7 @@ "InvalidUsername": "用戶名或密碼錯誤!", "InvalidAuthenticatorCode": "驗證碼無效!", "InvalidRecoveryCode": "恢復碼無效!", - "TheTargetUserIsNotLinkedToYou": "目標用戶與您無關!" + "TheTargetUserIsNotLinkedToYou": "目標用戶與您無關!", + "ClientResourcePermissionProviderKeyLookupService": "用戶端" } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/Clients/ClientFinder.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/Clients/ClientFinder.cs new file mode 100644 index 0000000000..d64215a047 --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/Clients/ClientFinder.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.IdentityServer.Clients; + +public class ClientFinder : IClientFinder, ITransientDependency +{ + protected IClientRepository ClientRepository { get; } + + public ClientFinder(IClientRepository clientRepository) + { + ClientRepository = clientRepository; + } + + public virtual async Task> SearchAsync(string filter, int page = 1) + { + using (ClientRepository.DisableTracking()) + { + page = page < 1 ? 1 : page; + var clients = await ClientRepository.GetListAsync(nameof(Client.ClientName), filter: filter, skipCount: (page - 1) * 10, maxResultCount: 10); + return clients.Select(x => new ClientFinderResult + { + Id = x.Id, + ClientId = x.ClientId + }).ToList(); + } + } +} diff --git a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/ClientResourcePermissionManagerExtensions.cs b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/ClientResourcePermissionManagerExtensions.cs new file mode 100644 index 0000000000..16e686a2aa --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/ClientResourcePermissionManagerExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Volo.Abp.Authorization.Permissions; + +namespace Volo.Abp.PermissionManagement; + +public static class ClientResourcePermissionManagerExtensions +{ + public static Task GetForClientAsync([NotNull] this IResourcePermissionManager resourcePermissionManager, string resourceName, string resourceKey, string clientId, string permissionName) + { + Check.NotNull(resourcePermissionManager, nameof(resourcePermissionManager)); + + return resourcePermissionManager.GetAsync(permissionName, resourceName, resourceKey, ClientPermissionValueProvider.ProviderName, clientId); + } + + public static Task> GetAllForClientAsync([NotNull] this IResourcePermissionManager resourcePermissionManager, string resourceName, string resourceKey, string clientId) + { + Check.NotNull(resourcePermissionManager, nameof(resourcePermissionManager)); + + return resourcePermissionManager.GetAllAsync(resourceName, resourceKey, ClientPermissionValueProvider.ProviderName, clientId); + } + + public static Task SetForClientAsync([NotNull] this IResourcePermissionManager resourcePermissionManager, string resourceName, string resourceKey, string clientId, [NotNull] string permissionName, bool isGranted) + { + Check.NotNull(resourcePermissionManager, nameof(resourcePermissionManager)); + + return resourcePermissionManager.SetAsync(permissionName, resourceName, resourceKey, ClientPermissionValueProvider.ProviderName, clientId, isGranted); + } +} diff --git a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/AbpPermissionManagementDomainIdentityServerModule.cs b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/AbpPermissionManagementDomainIdentityServerModule.cs index 3d2d42c28f..2525ffb3d3 100644 --- a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/AbpPermissionManagementDomainIdentityServerModule.cs +++ b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/AbpPermissionManagementDomainIdentityServerModule.cs @@ -1,5 +1,8 @@ -using Volo.Abp.Authorization.Permissions; +using System; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Authorization.Permissions; using Volo.Abp.IdentityServer; +using Volo.Abp.IdentityServer.Clients; using Volo.Abp.Modularity; namespace Volo.Abp.PermissionManagement.IdentityServer; @@ -18,5 +21,17 @@ public class AbpPermissionManagementDomainIdentityServerModule : AbpModule options.ProviderPolicies[ClientPermissionValueProvider.ProviderName] = "IdentityServer.Client.ManagePermissions"; }); + + context.Services.AddAbpOptions().PostConfigure((options, serviceProvider) => + { + // The IClientFinder implementation in identity Server Pro module for tiered application. + if (serviceProvider.GetService() == null) + { + return; + } + + options.ResourceManagementProviders.Add(); + options.ResourcePermissionProviderKeyLookupServices.Add(); + }); } } diff --git a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientDeletedEventHandler.cs b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientDeletedEventHandler.cs new file mode 100644 index 0000000000..2a5ebbfb71 --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientDeletedEventHandler.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.IdentityServer.Clients; +using Volo.Abp.Uow; + +namespace Volo.Abp.PermissionManagement.IdentityServer; + +public class ClientDeletedEventHandler : + IDistributedEventHandler>, + ITransientDependency +{ + protected IPermissionManager PermissionManager { get; } + protected IResourcePermissionManager ResourcePermissionManager { get; } + + public ClientDeletedEventHandler(IPermissionManager permissionManager, IResourcePermissionManager resourcePermissionManager) + { + PermissionManager = permissionManager; + ResourcePermissionManager = resourcePermissionManager; + } + + [UnitOfWork] + public virtual async Task HandleEventAsync(EntityDeletedEto eventData) + { + await PermissionManager.DeleteAsync(ClientPermissionValueProvider.ProviderName, eventData.Entity.ClientId); + await ResourcePermissionManager.DeleteAsync(ClientResourcePermissionValueProvider.ProviderName, eventData.Entity.ClientId); + } +} diff --git a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientPermissionManagementProvider.cs b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientPermissionManagementProvider.cs index dd25c6d5c0..618588bdf4 100644 --- a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientPermissionManagementProvider.cs +++ b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientPermissionManagementProvider.cs @@ -18,7 +18,6 @@ public class ClientPermissionManagementProvider : PermissionManagementProvider guidGenerator, currentTenant) { - } public override Task CheckAsync(string name, string providerName, string providerKey) @@ -29,6 +28,14 @@ public class ClientPermissionManagementProvider : PermissionManagementProvider } } + public override Task CheckAsync(string[] names, string providerName, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.CheckAsync(names, providerName, providerKey); + } + } + protected override Task GrantAsync(string name, string providerKey) { using (CurrentTenant.Change(null)) diff --git a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionManagementProvider.cs b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionManagementProvider.cs new file mode 100644 index 0000000000..9285742131 --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionManagementProvider.cs @@ -0,0 +1,62 @@ +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.PermissionManagement.IdentityServer; + +public class ClientResourcePermissionManagementProvider : ResourcePermissionManagementProvider +{ + public override string Name => ClientResourcePermissionValueProvider.ProviderName; + + public ClientResourcePermissionManagementProvider( + IResourcePermissionGrantRepository permissionGrantRepository, + IGuidGenerator guidGenerator, + ICurrentTenant currentTenant) + : base( + permissionGrantRepository, + guidGenerator, + currentTenant) + { + } + + public override Task CheckAsync(string name, string resourceName, string resourceKey, string providerName, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.CheckAsync(name, resourceName, resourceKey, providerName, providerKey); + } + } + + public override Task CheckAsync(string[] names, string resourceName, string resourceKey, string providerName, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.CheckAsync(names, resourceName, resourceKey, providerName, providerKey); + } + } + + public override Task SetAsync(string name, string resourceName, string resourceKey, string providerKey, bool isGranted) + { + using (CurrentTenant.Change(null)) + { + return base.SetAsync(name, resourceName, resourceKey, providerKey, isGranted); + } + } + + protected override async Task GrantAsync(string name, string resourceName, string resourceKey, string providerKey) + { + using (CurrentTenant.Change(null)) + { + await base.GrantAsync(name, resourceName, resourceKey, providerKey); + } + } + + protected override Task RevokeAsync(string name, string resourceName, string resourceKey, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.RevokeAsync(name, resourceName, resourceKey, providerKey); + } + } +} diff --git a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionProviderKeyLookupService.cs b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionProviderKeyLookupService.cs new file mode 100644 index 0000000000..7fc813dabb --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionProviderKeyLookupService.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.DependencyInjection; +using Volo.Abp.IdentityServer.Clients; +using Volo.Abp.IdentityServer.Localization; +using Volo.Abp.Localization; + +namespace Volo.Abp.PermissionManagement.IdentityServer; + +public class ClientResourcePermissionProviderKeyLookupService : IResourcePermissionProviderKeyLookupService, ITransientDependency +{ + public string Name => ClientResourcePermissionValueProvider.ProviderName; + + public ILocalizableString DisplayName { get; } + + protected IClientFinder ClientFinder { get; } + + public ClientResourcePermissionProviderKeyLookupService(IClientFinder clientFinder) + { + ClientFinder = clientFinder; + DisplayName = LocalizableString.Create(nameof(ClientResourcePermissionProviderKeyLookupService)); + } + + public virtual async Task> SearchAsync(string filter = null, int page = 1, CancellationToken cancellationToken = default) + { + var clients = await ClientFinder.SearchAsync(filter, page); + return clients.Select(x => new ResourcePermissionProviderKeyInfo(x.ClientId, x.ClientId)).ToList(); + } + + public virtual Task> SearchAsync(string[] keys, CancellationToken cancellationToken = default) + { + // Keys are ClientIds + return Task.FromResult(keys.Select(x => new ResourcePermissionProviderKeyInfo(x, x)).ToList()); + } +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/ApplicationFinderResult.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/ApplicationFinderResult.cs new file mode 100644 index 0000000000..5ff4d5041b --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/ApplicationFinderResult.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.OpenIddict.Applications; + +public class ApplicationFinderResult +{ + public Guid Id { get; set; } + + public string ClientId { get; set; } +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/IApplicationFinder.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/IApplicationFinder.cs new file mode 100644 index 0000000000..0585df8815 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/IApplicationFinder.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Volo.Abp.OpenIddict.Applications; + +public interface IApplicationFinder +{ + Task> SearchAsync(string filter, int page = 1); +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationClientIdChangedEto.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationClientIdChangedEto.cs new file mode 100644 index 0000000000..d80e5f35d6 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationClientIdChangedEto.cs @@ -0,0 +1,13 @@ +using System; + +namespace Volo.Abp.OpenIddict.Applications; + +[Serializable] +public class OpenIddictApplicationClientIdChangedEto +{ + public Guid Id { get; set; } + + public string ClientId { get; set; } + + public string OldClientId { get; set; } +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationEto.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationEto.cs new file mode 100644 index 0000000000..ff853d2c4a --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationEto.cs @@ -0,0 +1,43 @@ +using System; + +namespace Volo.Abp.OpenIddict.Applications; + +[Serializable] +public class OpenIddictApplicationEto +{ + public Guid Id { get; set; } + + public string ApplicationType { get; set; } + + public string ClientId { get; set; } + + public string ClientSecret { get; set; } + + public string ClientType { get; set; } + + public string ConsentType { get; set; } + + public string DisplayName { get; set; } + + public string DisplayNames { get; set; } + + public string JsonWebKeySet { get; set; } + + public string Permissions { get; set; } + + public string PostLogoutRedirectUris { get; set; } + + public string Properties { get; set; } + + public string RedirectUris { get; set; } + + public string Requirements { get; set; } + + public string Settings { get; set; } + + public string FrontChannelLogoutUri { get; set; } + + public string ClientUri { get; set; } + + public string LogoUri { get; set; } +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ar.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ar.json index 61c52f63c5..c22bdea508 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ar.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ar.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "هل تريد منح {0} حق الوصول إلى بياناتك؟", "ScopesRequested": "النطاقات المطلوبة", "Accept": "قبول", - "Deny": "رفض" + "Deny": "رفض", + "ApplicationResourcePermissionProviderKeyLookupService": "العميل" } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/cs.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/cs.json index c219b5dc8f..fda735746b 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/cs.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/cs.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Chcete uživateli {0} udělit přístup ke svým datům?", "ScopesRequested": "Požadované rozsahy", "Accept": "Akceptovat", - "Deny": "Odmítnout" + "Deny": "Odmítnout", + "ApplicationResourcePermissionProviderKeyLookupService": "Klient" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/de.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/de.json index 4c6438de37..affff9da29 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/de.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/de.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Möchten Sie {0} Zugriff auf Ihre Daten gewähren?", "ScopesRequested": "Umfänge angefordert", "Accept": "Akzeptieren", - "Deny": "Leugnen" + "Deny": "Leugnen", + "ApplicationResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/el.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/el.json index c10f34fb1d..493c73b2a7 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/el.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/el.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Θέλετε να παραχωρήσετε στον χρήστη {0} πρόσβαση στα δεδομένα σας;", "ScopesRequested": "Ζητούνται πεδία εφαρμογής", "Accept": "Αποδοχή", - "Deny": "Άρνηση" + "Deny": "Άρνηση", + "ApplicationResourcePermissionProviderKeyLookupService": "Πελάτης" } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/en.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/en.json index b6bd02bc15..d742176bd5 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/en.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/en.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Do you want to grant {0} access to your data?", "ScopesRequested": "Scopes requested", "Accept": "Accept", - "Deny": "Deny" + "Deny": "Deny", + "ApplicationResourcePermissionProviderKeyLookupService": "Client" } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/es.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/es.json index 3bec447a24..ad5a974972 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/es.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/es.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "¿Quieres otorgarle a {0} acceso a tus datos?", "ScopesRequested": "Alcances solicitados", "Accept": "Aceptar", - "Deny": "Denegar" + "Deny": "Denegar", + "ApplicationResourcePermissionProviderKeyLookupService": "Cliente" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fa.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fa.json index f1649533a9..c0c2d82340 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fa.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fa.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "آیا می خواهید به {0} اجازه دسترسی به داده های خود را بدهید؟", "ScopesRequested": "محدوده های درخواستی", "Accept": "پذیرش", - "Deny": "رد" + "Deny": "رد", + "ApplicationResourcePermissionProviderKeyLookupService": "کلاینت" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fi.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fi.json index 43dbfb17ac..ce0bb20bfe 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fi.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fi.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Haluatko myöntää käyttäjälle {0} pääsyn tietoihisi?", "ScopesRequested": "Laajuudet pyydetty", "Accept": "Hyväksy", - "Deny": "Kiellä" + "Deny": "Kiellä", + "ApplicationResourcePermissionProviderKeyLookupService": "Asiakas" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fr.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fr.json index 188116b53c..c11964d21d 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fr.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fr.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Voulez-vous accorder à {0} l'accès à vos données ?", "ScopesRequested": "Périmètres demandés", "Accept": "Accepter", - "Deny": "Refuser" + "Deny": "Refuser", + "ApplicationResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hi.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hi.json index 39b3370ca7..7f2678d7a0 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hi.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hi.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "क्या आप {0} को अपने डेटा तक पहुंच प्रदान करना चाहते हैं?", "ScopesRequested": "दायरे का अनुरोध किया गया", "Accept": "स्वीकार करना", - "Deny": "अस्वीकार करना" + "Deny": "अस्वीकार करना", + "ApplicationResourcePermissionProviderKeyLookupService": "क्लाइंट" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hr.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hr.json index 2dfc3b3a9d..ee16d2b1e0 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hr.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hr.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Želite li {0} odobriti pristup vašim podacima?", "ScopesRequested": "Traženi dometi", "Accept": "Prihvatiti", - "Deny": "poreći" + "Deny": "poreći", + "ApplicationResourcePermissionProviderKeyLookupService": "Klijent" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hu.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hu.json index f55ea2ac74..7a5e7b1956 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hu.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hu.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Hozzáférést szeretne adni a(z) {0} számára az adataihoz?", "ScopesRequested": "Kért hatókörök", "Accept": "Elfogad", - "Deny": "Tiltás" + "Deny": "Tiltás", + "ApplicationResourcePermissionProviderKeyLookupService": "Kliens" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/is.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/is.json index b9fc6a43ce..b6c5bafca6 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/is.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/is.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Viltu veita {0} aðgang að gögnunum þínum?", "ScopesRequested": "Umfang óskað", "Accept": "Samþykkja", - "Deny": "Neita" + "Deny": "Neita", + "ApplicationResourcePermissionProviderKeyLookupService": "Biðlari" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/it.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/it.json index 22eb3bbafe..e5b67df57a 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/it.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/it.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Vuoi concedere a {0} l'accesso ai tuoi dati?", "ScopesRequested": "Ambiti richiesti", "Accept": "Accettare", - "Deny": "Negare" + "Deny": "Negare", + "ApplicationResourcePermissionProviderKeyLookupService": "Cliente" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/nl.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/nl.json index b3b35c6acb..7eb593fd2d 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/nl.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/nl.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Wilt u {0} toegang verlenen tot uw gegevens?", "ScopesRequested": "Scopes gevraagd", "Accept": "Aanvaarden", - "Deny": "Ontkennen" + "Deny": "Ontkennen", + "ApplicationResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pl-PL.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pl-PL.json index 2e5e19eae7..bff402519d 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pl-PL.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pl-PL.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Czy chcesz przyznać firmie {0} dostęp do swoich danych?", "ScopesRequested": "Poproszono o zakresy", "Accept": "Zaakceptować", - "Deny": "Zaprzeczyć" + "Deny": "Zaprzeczyć", + "ApplicationResourcePermissionProviderKeyLookupService": "Klient" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pt-BR.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pt-BR.json index 60c50e9e3e..97f52a7f01 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pt-BR.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pt-BR.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Deseja permitir {0} acessar seus dados?", "ScopesRequested": "Escopo solicitado", "Accept": "Aceitar", - "Deny": "Negar" + "Deny": "Negar", + "ApplicationResourcePermissionProviderKeyLookupService": "Cliente" } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ro-RO.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ro-RO.json index a9d9eee22e..579df65787 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ro-RO.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ro-RO.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Doriți să acordați acces {0} la datele dvs.?", "ScopesRequested": "Domenii de aplicare solicitate", "Accept": "Accept", - "Deny": "Negați" + "Deny": "Negați", + "ApplicationResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ru.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ru.json index 4f5b1d0e21..2f22d22da5 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ru.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ru.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Вы хотите предоставить пользователю {0} доступ к вашим данным?", "ScopesRequested": "Запрошенные объемы", "Accept": "Принимать", - "Deny": "Отрицать" + "Deny": "Отрицать", + "ApplicationResourcePermissionProviderKeyLookupService": "Клиент" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sk.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sk.json index 13e0853b2d..cb017569cc 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sk.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sk.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Chcete používateľovi {0} udeliť prístup k svojim údajom?", "ScopesRequested": "Požadované rozsahy", "Accept": "súhlasiť", - "Deny": "Odmietnuť" + "Deny": "Odmietnuť", + "ApplicationResourcePermissionProviderKeyLookupService": "Klient" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sl.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sl.json index c8157ac509..49d1bd148b 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sl.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sl.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Ali želite aplikaciji {0} omogočiti dostop do vaših podatkov?", "ScopesRequested": "Zahtevani obsegi", "Accept": "Sprejmi", - "Deny": "Zanikati" + "Deny": "Zanikati", + "ApplicationResourcePermissionProviderKeyLookupService": "Odjemalec" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sv.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sv.json index 3e2a58578e..063727a745 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sv.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sv.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Vill du ge {0} tillgång till dina data?", "ScopesRequested": "Begärda omfattningar", "Accept": "Acceptera", - "Deny": "Förneka" + "Deny": "Förneka", + "ApplicationResourcePermissionProviderKeyLookupService": "Klient" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/tr.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/tr.json index 907612a6c4..847eb5f482 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/tr.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/tr.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Do you want to grant {0} access to your data?", "ScopesRequested": "İstenen kapsamlar", "Accept": "Kabul etmek", - "Deny": "Reddetmek" + "Deny": "Reddetmek", + "ApplicationResourcePermissionProviderKeyLookupService": "İstemci" } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/vi.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/vi.json index c89d882a39..4435c32298 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/vi.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/vi.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Bạn có muốn cấp cho {0} quyền truy cập vào dữ liệu của mình không?", "ScopesRequested": "Phạm vi được yêu cầu", "Accept": "Chấp nhận", - "Deny": "Từ chối" + "Deny": "Từ chối", + "ApplicationResourcePermissionProviderKeyLookupService": "Máy khách" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hans.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hans.json index f00a7c9e37..ee7b4a7b0f 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hans.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hans.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "是否要授予 {0} 访问你的数据的权限?", "ScopesRequested": "要求的Scope", "Accept": "接受", - "Deny": "拒绝" + "Deny": "拒绝", + "ApplicationResourcePermissionProviderKeyLookupService": "客户端" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hant.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hant.json index 83aa0c35d1..ca1ceaed22 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hant.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hant.json @@ -11,6 +11,7 @@ "DoYouWantToGrantAccessToYourData": "是否要授予 {0} 訪問你的數據的權限?", "ScopesRequested": "要求的Scope", "Accept": "接受", - "Deny": "拒絕" + "Deny": "拒絕", + "ApplicationResourcePermissionProviderKeyLookupService": "客戶端" } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainMappers.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainMappers.cs new file mode 100644 index 0000000000..ef750fc3a6 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainMappers.cs @@ -0,0 +1,13 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Abp.OpenIddict.Applications; + +namespace Volo.Abp.OpenIddict; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class OpenIddictApplicationToOpenIddictApplicationEtoMapper : MapperBase +{ + public override partial OpenIddictApplicationEto Map(OpenIddictApplication source); + + public override partial void Map(OpenIddictApplication source, OpenIddictApplicationEto destination); +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainModule.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainModule.cs index fd20f71012..e235e16893 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainModule.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainModule.cs @@ -8,6 +8,7 @@ using Volo.Abp.BackgroundWorkers; using Volo.Abp.Caching; using Volo.Abp.DistributedLocking; using Volo.Abp.Domain; +using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.Guids; using Volo.Abp.Identity; using Volo.Abp.Modularity; @@ -18,6 +19,7 @@ using Volo.Abp.OpenIddict.Authorizations; using Volo.Abp.OpenIddict.Scopes; using Volo.Abp.OpenIddict.Tokens; using Volo.Abp.Threading; +using Volo.Abp.Users; namespace Volo.Abp.OpenIddict; @@ -36,6 +38,15 @@ public class AbpOpenIddictDomainModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { AddOpenIddictCore(context.Services); + + context.Services.AddMapperlyObjectMapper(); + + Configure(options => + { + options.EtoMappings.Add(typeof(AbpOpenIddictDomainModule)); + + options.AutoEventSelectors.Add(); + }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationFinder.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationFinder.cs new file mode 100644 index 0000000000..56c6f9af99 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationFinder.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.OpenIddict.Applications; + +public class AbpApplicationFinder : IApplicationFinder, ITransientDependency +{ + protected IOpenIddictApplicationRepository ApplicationRepository { get; } + + public AbpApplicationFinder(IOpenIddictApplicationRepository applicationRepository) + { + ApplicationRepository = applicationRepository; + } + + public virtual async Task> SearchAsync(string filter, int page = 1) + { + using (ApplicationRepository.DisableTracking()) + { + page = page < 1 ? 1 : page; + var applications = await ApplicationRepository.GetListAsync(nameof(OpenIddictApplication.CreationTime), filter: filter, skipCount: (page - 1) * 10, maxResultCount: 10); + return applications.Select(x => new ApplicationFinderResult + { + Id = x.Id, + ClientId = x.ClientId + }).ToList(); + } + } +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationManager.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationManager.cs index ea2283ea25..478ccb68f1 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationManager.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationManager.cs @@ -6,29 +6,35 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.Core; +using Volo.Abp.EventBus.Distributed; namespace Volo.Abp.OpenIddict.Applications; public class AbpApplicationManager : OpenIddictApplicationManager, IAbpApplicationManager { protected AbpOpenIddictIdentifierConverter IdentifierConverter { get; } + protected IDistributedEventBus DistributedEventBus { get; } public AbpApplicationManager( [NotNull] IOpenIddictApplicationCache cache, [NotNull] ILogger logger, [NotNull] IOptionsMonitor options, [NotNull] IOpenIddictApplicationStore resolver, - AbpOpenIddictIdentifierConverter identifierConverter) + AbpOpenIddictIdentifierConverter identifierConverter, + IDistributedEventBus distributedEventBus) : base(cache, logger, options, resolver) { IdentifierConverter = identifierConverter; + DistributedEventBus = distributedEventBus; } - public async override ValueTask UpdateAsync(OpenIddictApplicationModel application, CancellationToken cancellationToken = default) + public override async ValueTask UpdateAsync(OpenIddictApplicationModel application, CancellationToken cancellationToken = default) { + var entity = await Store.FindByIdAsync(IdentifierConverter.ToString(application.Id), cancellationToken); + var oldClientId = entity?.ClientId; + if (!Options.CurrentValue.DisableEntityCaching) { - var entity = await Store.FindByIdAsync(IdentifierConverter.ToString(application.Id), cancellationToken); if (entity != null) { await Cache.RemoveAsync(entity, cancellationToken); @@ -36,9 +42,21 @@ public class AbpApplicationManager : OpenIddictApplicationManager> GetListAsync(string sorting, int skipCount, int maxResultCount, string filter = null, CancellationToken cancellationToken = default); Task GetCountAsync(string filter = null, CancellationToken cancellationToken = default); - + Task FindByClientIdAsync(string clientId, CancellationToken cancellationToken = default); Task> FindByPostLogoutRedirectUriAsync(string address, CancellationToken cancellationToken = default); diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.MongoDB/Volo/Abp/OpenIddict/Applications/MongoOpenIddictApplicationRepository.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.MongoDB/Volo/Abp/OpenIddict/Applications/MongoOpenIddictApplicationRepository.cs index 867bbe71c5..7b3b0ba826 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.MongoDB/Volo/Abp/OpenIddict/Applications/MongoOpenIddictApplicationRepository.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.MongoDB/Volo/Abp/OpenIddict/Applications/MongoOpenIddictApplicationRepository.cs @@ -17,7 +17,7 @@ public class MongoOpenIddictApplicationRepository : MongoDbRepository dbContextProvider) : base(dbContextProvider) { } - + public virtual async Task> GetListAsync(string sorting, int skipCount, int maxResultCount, string filter = null, CancellationToken cancellationToken = default) { diff --git a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/ClientResourcePermissionManagerExtensions.cs b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/ClientResourcePermissionManagerExtensions.cs new file mode 100644 index 0000000000..16e686a2aa --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/ClientResourcePermissionManagerExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Volo.Abp.Authorization.Permissions; + +namespace Volo.Abp.PermissionManagement; + +public static class ClientResourcePermissionManagerExtensions +{ + public static Task GetForClientAsync([NotNull] this IResourcePermissionManager resourcePermissionManager, string resourceName, string resourceKey, string clientId, string permissionName) + { + Check.NotNull(resourcePermissionManager, nameof(resourcePermissionManager)); + + return resourcePermissionManager.GetAsync(permissionName, resourceName, resourceKey, ClientPermissionValueProvider.ProviderName, clientId); + } + + public static Task> GetAllForClientAsync([NotNull] this IResourcePermissionManager resourcePermissionManager, string resourceName, string resourceKey, string clientId) + { + Check.NotNull(resourcePermissionManager, nameof(resourcePermissionManager)); + + return resourcePermissionManager.GetAllAsync(resourceName, resourceKey, ClientPermissionValueProvider.ProviderName, clientId); + } + + public static Task SetForClientAsync([NotNull] this IResourcePermissionManager resourcePermissionManager, string resourceName, string resourceKey, string clientId, [NotNull] string permissionName, bool isGranted) + { + Check.NotNull(resourcePermissionManager, nameof(resourcePermissionManager)); + + return resourcePermissionManager.SetAsync(permissionName, resourceName, resourceKey, ClientPermissionValueProvider.ProviderName, clientId, isGranted); + } +} diff --git a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/AbpPermissionManagementDomainOpenIddictModule.cs b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/AbpPermissionManagementDomainOpenIddictModule.cs index 1bf2bf140d..2cad25a283 100644 --- a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/AbpPermissionManagementDomainOpenIddictModule.cs +++ b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/AbpPermissionManagementDomainOpenIddictModule.cs @@ -1,6 +1,9 @@ -using Volo.Abp.Authorization.Permissions; +using System; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Authorization.Permissions; using Volo.Abp.Modularity; using Volo.Abp.OpenIddict; +using Volo.Abp.OpenIddict.Applications; namespace Volo.Abp.PermissionManagement.OpenIddict; @@ -17,5 +20,17 @@ public class AbpPermissionManagementDomainOpenIddictModule : AbpModule options.ManagementProviders.Add(); options.ProviderPolicies[ClientPermissionValueProvider.ProviderName] = "OpenIddictPro.Application.ManagePermissions"; }); + + context.Services.AddAbpOptions().PostConfigure((options, serviceProvider) => + { + // The IApplicationFinder implementation in OpenIddict Pro module for tiered application. + if (serviceProvider.GetService() == null) + { + return; + } + + options.ResourceManagementProviders.Add(); + options.ResourcePermissionProviderKeyLookupServices.Add(); + }); } } diff --git a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationPermissionManagementProvider.cs b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationPermissionManagementProvider.cs index 4c4d6fe398..6d07a728e4 100644 --- a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationPermissionManagementProvider.cs +++ b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationPermissionManagementProvider.cs @@ -18,7 +18,6 @@ public class ApplicationPermissionManagementProvider : PermissionManagementProvi guidGenerator, currentTenant) { - } public override Task CheckAsync(string name, string providerName, string providerKey) @@ -29,6 +28,14 @@ public class ApplicationPermissionManagementProvider : PermissionManagementProvi } } + public override Task CheckAsync(string[] names, string providerName, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.CheckAsync(names, providerName, providerKey); + } + } + protected override Task GrantAsync(string name, string providerKey) { using (CurrentTenant.Change(null)) diff --git a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionManagementProvider.cs b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionManagementProvider.cs new file mode 100644 index 0000000000..e12fa63d03 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionManagementProvider.cs @@ -0,0 +1,59 @@ +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.PermissionManagement.OpenIddict; + +public class ApplicationResourcePermissionManagementProvider : ResourcePermissionManagementProvider +{ + public override string Name => ClientResourcePermissionValueProvider.ProviderName; + + public ApplicationResourcePermissionManagementProvider( + IResourcePermissionGrantRepository resourcePermissionGrantRepository, + IGuidGenerator guidGenerator, + ICurrentTenant currentTenant) + : base(resourcePermissionGrantRepository, guidGenerator, currentTenant) + { + } + + public override Task CheckAsync(string name, string resourceName, string resourceKey, string providerName, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.CheckAsync(name, resourceName, resourceKey, providerName, providerKey); + } + } + + public override Task CheckAsync(string[] names, string resourceName, string resourceKey, string providerName, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.CheckAsync(names, resourceName, resourceKey, providerName, providerKey); + } + } + + public override Task SetAsync(string name, string resourceName, string resourceKey, string providerKey, bool isGranted) + { + using (CurrentTenant.Change(null)) + { + return base.SetAsync(name, resourceName, resourceKey, providerKey, isGranted); + } + } + + protected override async Task GrantAsync(string name, string resourceName, string resourceKey, string providerKey) + { + using (CurrentTenant.Change(null)) + { + await base.GrantAsync(name, resourceName, resourceKey, providerKey); + } + } + + protected override Task RevokeAsync(string name, string resourceName, string resourceKey, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.RevokeAsync(name, resourceName, resourceKey, providerKey); + } + } +} diff --git a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionProviderKeyLookupService.cs b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionProviderKeyLookupService.cs new file mode 100644 index 0000000000..1423ca9768 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionProviderKeyLookupService.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Localization; +using Volo.Abp.OpenIddict.Applications; +using Volo.Abp.OpenIddict.Localization; + +namespace Volo.Abp.PermissionManagement.OpenIddict; + +public class ApplicationResourcePermissionProviderKeyLookupService : IResourcePermissionProviderKeyLookupService, ITransientDependency +{ + public string Name => ClientResourcePermissionValueProvider.ProviderName; + + public ILocalizableString DisplayName { get; } + + protected IApplicationFinder ApplicationFinder { get; } + + public ApplicationResourcePermissionProviderKeyLookupService(IApplicationFinder applicationFinder) + { + ApplicationFinder = applicationFinder; + DisplayName = LocalizableString.Create(nameof(ApplicationResourcePermissionProviderKeyLookupService)); + } + + public virtual async Task> SearchAsync(string filter = null, int page = 1, CancellationToken cancellationToken = default) + { + var applications = await ApplicationFinder.SearchAsync(filter, page); + return applications.Select(x => new ResourcePermissionProviderKeyInfo(x.ClientId, x.ClientId)).ToList(); + } + + public virtual Task> SearchAsync(string[] keys, CancellationToken cancellationToken = default) + { + // Keys are ClientIds + return Task.FromResult(keys.Select(x => new ResourcePermissionProviderKeyInfo(x, x)).ToList()); + } +} diff --git a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/OpenIddictApplicationClientIdChangedHandler.cs b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/OpenIddictApplicationClientIdChangedHandler.cs new file mode 100644 index 0000000000..8a22d1b46d --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/OpenIddictApplicationClientIdChangedHandler.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.OpenIddict.Applications; + +namespace Volo.Abp.PermissionManagement.OpenIddict; + +public class OpenIddictApplicationClientIdChangedHandler : + IDistributedEventHandler, + ITransientDependency +{ + protected IPermissionManager PermissionManager { get; } + protected IPermissionGrantRepository PermissionGrantRepository { get; } + protected IResourcePermissionManager ResourcePermissionManager { get; } + protected IResourcePermissionGrantRepository ResourcePermissionGrantRepository { get; } + + public OpenIddictApplicationClientIdChangedHandler( + IPermissionManager permissionManager, + IPermissionGrantRepository permissionGrantRepository, + IResourcePermissionManager resourcePermissionManager, + IResourcePermissionGrantRepository resourcePermissionGrantRepository) + { + PermissionManager = permissionManager; + PermissionGrantRepository = permissionGrantRepository; + ResourcePermissionManager = resourcePermissionManager; + ResourcePermissionGrantRepository = resourcePermissionGrantRepository; + } + + public async Task HandleEventAsync(OpenIddictApplicationClientIdChangedEto eventData) + { + var permissionGrantsInRole = await PermissionGrantRepository.GetListAsync(ClientPermissionValueProvider.ProviderName, eventData.OldClientId); + foreach (var permissionGrant in permissionGrantsInRole) + { + await PermissionManager.UpdateProviderKeyAsync(permissionGrant, eventData.ClientId); + } + + var resourcePermissionGrantsInRole = await ResourcePermissionGrantRepository.GetListAsync(ClientResourcePermissionValueProvider.ProviderName, eventData.OldClientId); + foreach (var resourcePermissionGrant in resourcePermissionGrantsInRole) + { + await ResourcePermissionManager.UpdateProviderKeyAsync(resourcePermissionGrant, eventData.ClientId); + } + } +} diff --git a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/OpenIddictApplicationDeletedEventHandler.cs b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/OpenIddictApplicationDeletedEventHandler.cs new file mode 100644 index 0000000000..ac02065e63 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/OpenIddictApplicationDeletedEventHandler.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.OpenIddict.Applications; +using Volo.Abp.Uow; + +namespace Volo.Abp.PermissionManagement.OpenIddict; + +public class OpenIddictApplicationDeletedEventHandler : + IDistributedEventHandler>, + ITransientDependency +{ + protected IPermissionManager PermissionManager { get; } + protected IResourcePermissionManager ResourcePermissionManager { get; } + + public OpenIddictApplicationDeletedEventHandler(IPermissionManager permissionManager, IResourcePermissionManager resourcePermissionManager) + { + PermissionManager = permissionManager; + ResourcePermissionManager = resourcePermissionManager; + } + + [UnitOfWork] + public virtual async Task HandleEventAsync(EntityDeletedEto eventData) + { + await PermissionManager.DeleteAsync(ClientPermissionValueProvider.ProviderName, eventData.Entity.ClientId); + await ResourcePermissionManager.DeleteAsync(ClientResourcePermissionValueProvider.ProviderName, eventData.Entity.ClientId); + } +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider.cs index 90d4e176d3..9682965c67 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider.cs @@ -26,7 +26,7 @@ public abstract class ResourcePermissionManagementProvider : IResourcePermission CurrentTenant = currentTenant; } - public virtual async Task CheckAsync(string name, string resourceName,string resourceKey, string providerName, string providerKey) + public virtual async Task CheckAsync(string name, string resourceName, string resourceKey, string providerName, string providerKey) { var multiplePermissionValueProviderGrantInfo = await CheckAsync(new[] { name }, resourceName, resourceKey, providerName, providerKey); @@ -55,7 +55,7 @@ public abstract class ResourcePermissionManagementProvider : IResourcePermission } } - public virtual Task SetAsync(string name, string resourceName,string resourceKey, string providerKey, bool isGranted) + public virtual Task SetAsync(string name, string resourceName, string resourceKey, string providerKey, bool isGranted) { return isGranted ? GrantAsync(name, resourceName, resourceKey, providerKey) diff --git a/npm/ng-packs/package.json b/npm/ng-packs/package.json index c5c3b698c7..7367046944 100644 --- a/npm/ng-packs/package.json +++ b/npm/ng-packs/package.json @@ -56,6 +56,7 @@ "@angular-eslint/eslint-plugin-template": "~21.0.0", "@angular-eslint/template-parser": "~21.0.0", "@angular/animations": "21.0.0", + "@angular/aria": "21.0.0", "@angular/build": "~21.0.0", "@angular/cli": "~21.0.0", "@angular/common": "~21.0.0", @@ -147,4 +148,4 @@ "dependencies": { "openid-client": "^6.6.4" } -} +} \ No newline at end of file diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/extensible-table-row-detail.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/extensible-table-row-detail.component.ts new file mode 100644 index 0000000000..aabbd2a255 --- /dev/null +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/extensible-table-row-detail.component.ts @@ -0,0 +1,10 @@ +import { Component, contentChild, input, TemplateRef } from '@angular/core'; + +@Component({ + selector: 'abp-extensible-table-row-detail', + template: '', +}) +export class ExtensibleTableRowDetailComponent { + readonly rowHeight = input('100%'); + readonly template = contentChild(TemplateRef<{ row: R; expanded: boolean }>); +} diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/index.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/index.ts new file mode 100644 index 0000000000..e0666787b4 --- /dev/null +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/index.ts @@ -0,0 +1 @@ +export * from './extensible-table-row-detail.component'; diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html index 8581f6c4d4..c5373aacff 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html @@ -1,21 +1,30 @@ @if (isBrowser) { - -@if(selectable) { - + + @if (effectiveRowDetailTemplate) { + + + + + + + + + + + + } + @if(selectable) { + @@ -44,97 +53,67 @@ } @if (actionsTemplate || (actionList.length && hasAtLeastOnePermittedAction)) { - - - - - @if (isVisibleActions(row)) { - - } - + + + + + @if (isVisibleActions(row)) { + + } - + + } @for (prop of propList; track prop.name; let i = $index) { - - - @if (prop.tooltip) { - - {{ column.name }} - - } @else { - - {{ column.name }} - - } - - - - - @if (!row['_' + prop.name].component) { - @if (prop.type === 'datetime' || prop.type === 'date' || prop.type === 'time') { -
+ + @if (prop.tooltip) { + + {{ column.name }} + + } @else { + + {{ column.name }} + + } + + + + + @if (!row['_' + prop.name].component) { + @if (prop.type === 'datetime' || prop.type === 'date' || prop.type === 'time') { +
- } @else { -
+ } @else { +
- } - } @else { -
+ } + } @else { + - } -
+ ">
+ } -
-
+ + +
}
-} +} \ No newline at end of file diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts index c70d2b39e7..75253b1f02 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts @@ -4,6 +4,7 @@ import { ChangeDetectorRef, Component, computed, + ContentChild, EventEmitter, inject, Injector, @@ -17,13 +18,14 @@ import { SimpleChanges, TemplateRef, TrackByFunction, + ViewChild, } from '@angular/core'; import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; import { Observable, filter, map, Subject, debounceTime, distinctUntilChanged } from 'rxjs'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; -import { NgxDatatableModule, SelectionType } from '@swimlane/ngx-datatable'; +import { NgxDatatableModule, SelectionType, DatatableComponent } from '@swimlane/ngx-datatable'; import { ABP, @@ -53,6 +55,8 @@ import { ROW_RECORD, } from '../../tokens/extensions.token'; import { GridActionsComponent } from '../grid-actions/grid-actions.component'; +import { ExtensibleTableRowDetailComponent } from './extensible-table-row-detail'; +import { RowDetailContext } from '../../models/row-detail'; const DEFAULT_ACTIONS_COLUMN_WIDTH = 150; @@ -75,6 +79,12 @@ const DEFAULT_ACTIONS_COLUMN_WIDTH = 150; ], templateUrl: './extensible-table.component.html', changeDetection: ChangeDetectionStrategy.OnPush, + styles: [` + :host ::ng-deep .ngx-datatable.material .datatable-body .datatable-row-detail { + background: none; + padding: 0; + } + `], }) export class ExtensibleTableComponent implements OnChanges, AfterViewInit, OnDestroy { readonly #injector = inject(Injector); @@ -127,6 +137,23 @@ export class ExtensibleTableComponent implements OnChanges, AfterViewIn @Output() loadMore = new EventEmitter(); @Input() tableHeight: number; + @Input() rowDetailTemplate?: TemplateRef>; + @Input() rowDetailHeight: string | number = '100%'; + @Output() rowDetailToggle = new EventEmitter(); + + @ContentChild(ExtensibleTableRowDetailComponent) + rowDetailComponent?: ExtensibleTableRowDetailComponent; + + @ViewChild('table', { static: false }) table!: DatatableComponent; + + protected get effectiveRowDetailTemplate(): TemplateRef> | undefined { + return this.rowDetailComponent?.template() ?? this.rowDetailTemplate; + } + + protected get effectiveRowDetailHeight(): string | number { + return this.rowDetailComponent?.rowHeight() ?? this.rowDetailHeight; + } + hasAtLeastOnePermittedAction: boolean; readonly propList: EntityPropList; @@ -290,6 +317,13 @@ export class ExtensibleTableComponent implements OnChanges, AfterViewIn return this.tableHeight ? `${this.tableHeight}px` : 'auto'; } + toggleExpandRow(row: R): void { + if (this.table && this.table.rowDetail) { + this.table.rowDetail.toggleExpandRow(row); + } + this.rowDetailToggle.emit(row); + } + ngAfterViewInit(): void { if (!this.infiniteScroll) { this.list?.requestStatus$?.pipe(filter(status => status === 'loading')).subscribe(() => { diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/index.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/index.ts index 149043089b..ee501c875d 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/index.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/index.ts @@ -2,6 +2,7 @@ export * from './abstract-actions/abstract-actions.component'; export * from './extensible-form/extensible-form.component'; export * from './extensible-form/extensible-form-prop.component'; export * from './extensible-table/extensible-table.component'; +export * from './extensible-table/extensible-table-row-detail'; export * from './date-time-picker/extensible-date-time-picker.component'; export * from './grid-actions/grid-actions.component'; export * from './page-toolbar/page-toolbar.component'; diff --git a/npm/ng-packs/packages/components/extensible/src/lib/extensible.module.ts b/npm/ng-packs/packages/components/extensible/src/lib/extensible.module.ts index 26741c2832..9f3ebd775b 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/extensible.module.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/extensible.module.ts @@ -13,6 +13,7 @@ import { ExtensibleFormComponent, ExtensibleFormPropComponent, ExtensibleTableComponent, + ExtensibleTableRowDetailComponent, GridActionsComponent, PageToolbarComponent, ExtensibleDateTimePickerComponent, @@ -32,6 +33,7 @@ const importWithExport = [ CreateInjectorPipe, ExtensibleFormComponent, ExtensibleTableComponent, + ExtensibleTableRowDetailComponent, ExtensibleFormMultiselectComponent, ]; @@ -50,4 +52,4 @@ const importWithExport = [ ], exports: [...importWithExport], }) -export class ExtensibleModule {} +export class ExtensibleModule { } diff --git a/npm/ng-packs/packages/components/extensible/src/lib/models/row-detail.ts b/npm/ng-packs/packages/components/extensible/src/lib/models/row-detail.ts new file mode 100644 index 0000000000..c344e9add7 --- /dev/null +++ b/npm/ng-packs/packages/components/extensible/src/lib/models/row-detail.ts @@ -0,0 +1,4 @@ +export interface RowDetailContext { + row: R; + expanded: boolean; +} diff --git a/npm/ng-packs/packages/components/extensible/src/public-api.ts b/npm/ng-packs/packages/components/extensible/src/public-api.ts index e5c7883738..6d53f8ba9a 100644 --- a/npm/ng-packs/packages/components/extensible/src/public-api.ts +++ b/npm/ng-packs/packages/components/extensible/src/public-api.ts @@ -2,6 +2,7 @@ export * from './lib/components/date-time-picker/extensible-date-time-picker.com export * from './lib/components/extensible-form/extensible-form-prop.component'; export * from './lib/components/extensible-form/extensible-form.component'; export * from './lib/components/extensible-table/extensible-table.component'; +export * from './lib/components/extensible-table/extensible-table-row-detail'; export * from './lib/components/grid-actions/grid-actions.component'; export * from './lib/components/page-toolbar/page-toolbar.component'; export * from './lib/components/multi-select'; @@ -68,4 +69,5 @@ export * from './lib/utils/form-props.util'; export * from './lib/utils/props.util'; export * from './lib/utils/state.util'; export * from './lib/utils/model.utils'; +export * from './lib/models/row-detail'; export * from './lib/extensible.module'; diff --git a/npm/ng-packs/packages/setting-management/package.json b/npm/ng-packs/packages/setting-management/package.json index 579c744222..5fba6848ab 100644 --- a/npm/ng-packs/packages/setting-management/package.json +++ b/npm/ng-packs/packages/setting-management/package.json @@ -11,6 +11,9 @@ "@abp/ng.theme.shared": "~10.1.0-rc.1", "tslib": "^2.0.0" }, + "peerDependencies": { + "@angular/aria": "21.0.0" + }, "publishConfig": { "access": "public" }, @@ -28,4 +31,4 @@ "csharp", "webapp" ] -} +} \ No newline at end of file diff --git a/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.html b/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.html index 0ffdd873c8..07830f2d1f 100644 --- a/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.html +++ b/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.html @@ -2,41 +2,29 @@
-
+
- +
@if (settings.length) { -
-
- -
+ +
+
+
}
- + \ No newline at end of file diff --git a/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.ts b/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.ts index 1cec644ed6..2bf5e684e4 100644 --- a/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.ts +++ b/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.ts @@ -4,11 +4,17 @@ import { Component, inject, OnDestroy, OnInit, TrackByFunction } from '@angular/ import { Subscription } from 'rxjs'; import { NgComponentOutlet } from '@angular/common'; import { PageComponent } from '@abp/ng.components/page'; +import { Tab, Tabs, TabList, TabPanel } from '@angular/aria/tabs'; @Component({ selector: 'abp-setting-management', templateUrl: './setting-management.component.html', - imports: [NgComponentOutlet, PageComponent, LocalizationPipe, PermissionDirective, ForDirective], + imports: [NgComponentOutlet, PageComponent, LocalizationPipe, PermissionDirective, ForDirective, Tabs, TabList, Tab, TabPanel], + styles: [` + :host [ngTabPanel][inert] { + display: none; + } + `], }) export class SettingManagementComponent implements OnDestroy, OnInit { private settingTabsService = inject(SettingTabsService); diff --git a/templates/app-nolayers/angular/package.json b/templates/app-nolayers/angular/package.json index b1ea75904b..93db1446ad 100644 --- a/templates/app-nolayers/angular/package.json +++ b/templates/app-nolayers/angular/package.json @@ -22,6 +22,7 @@ "@abp/ng.theme.lepton-x": "~5.1.0-rc.1", "@abp/ng.theme.shared": "~10.1.0-rc.1", "@angular/animations": "~21.0.0", + "@angular/aria": "~21.0.0", "@angular/common": "~21.0.0", "@angular/compiler": "~21.0.0", "@angular/core": "~21.0.0", @@ -59,4 +60,4 @@ "karma-jasmine-html-reporter": "^1.7.0", "typescript": "~5.9.0" } -} +} \ No newline at end of file diff --git a/templates/app/angular/package.json b/templates/app/angular/package.json index e560238e6a..9710a598f6 100644 --- a/templates/app/angular/package.json +++ b/templates/app/angular/package.json @@ -22,6 +22,7 @@ "@abp/ng.theme.lepton-x": "~5.1.0-rc.1", "@abp/ng.theme.shared": "~10.1.0-rc.1", "@angular/animations": "~21.0.0", + "@angular/aria": "~21.0.0", "@angular/common": "~21.0.0", "@angular/compiler": "~21.0.0", "@angular/core": "~21.0.0", @@ -59,4 +60,4 @@ "karma-jasmine-html-reporter": "^1.7.0", "typescript": "~5.9.3" } -} +} \ No newline at end of file diff --git a/templates/module/angular/package.json b/templates/module/angular/package.json index aac3293e2a..b386586560 100644 --- a/templates/module/angular/package.json +++ b/templates/module/angular/package.json @@ -23,6 +23,7 @@ "@abp/ng.theme.basic": "~10.1.0-rc.1", "@abp/ng.theme.shared": "~10.1.0-rc.1", "@angular/animations": "~21.0.0", + "@angular/aria": "~21.0.0", "@angular/common": "~21.0.0", "@angular/compiler": "~21.0.0", "@angular/core": "~21.0.0", @@ -61,4 +62,4 @@ "symlink": "^2.0.0", "typescript": "~5.9.0" } -} +} \ No newline at end of file