mirror of https://github.com/abpframework/abp.git
committed by
GitHub
20 changed files with 8636 additions and 0 deletions
File diff suppressed because it is too large
@ -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) |
|||
@ -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<TEntity, TKey>` 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<Book, Guid> _bookRepository; // ✅ OK for simple operations |
|||
} |
|||
|
|||
// Custom queries needed - Define custom interface |
|||
public interface IBookRepository : IRepository<Book, Guid> |
|||
{ |
|||
Task<Book> 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<TKey>` | Basic entity with ID | |
|||
| `AggregateRoot<TKey>` | 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<AbpExceptionLocalizationOptions>(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<TResource>` |
|||
- 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<T>` | |
|||
| `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 | |
|||
@ -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<BookDto> GetAsync(Guid id); |
|||
Task<PagedResultDto<BookListItemDto>> GetListAsync(GetBookListInput input); |
|||
Task<BookDto> CreateAsync(CreateBookDto input); |
|||
Task<BookDto> 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<BookDto> GetAsync(Guid id) |
|||
{ |
|||
var book = await _bookRepository.GetAsync(id); |
|||
return _bookMapper.MapToDto(book); |
|||
} |
|||
|
|||
[Authorize(BookStorePermissions.Books.Create)] |
|||
public async Task<BookDto> 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<BookDto> 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<ValidationResult> 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<CreateBookDto> |
|||
{ |
|||
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<BookDto> MapToDtoList(List<Book> books); |
|||
} |
|||
``` |
|||
|
|||
Register in module: |
|||
```csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddSingleton<BookMapper>(); |
|||
} |
|||
``` |
|||
|
|||
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. |
|||
@ -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<BookStoreResource>(name); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Using Permissions |
|||
|
|||
### Declarative (Attribute) |
|||
```csharp |
|||
[Authorize(BookStorePermissions.Books.Create)] |
|||
public virtual async Task<BookDto> 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<BookDto> 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 |
|||
@ -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]` | |
|||
@ -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<Guid> |
|||
{ |
|||
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<Guid> |
|||
{ |
|||
public string OrderNumber { get; private set; } |
|||
public Guid CustomerId { get; private set; } |
|||
public OrderStatus Status { get; private set; } |
|||
public ICollection<OrderLine> 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<OrderLine>(); |
|||
} |
|||
|
|||
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<T, TKey>`): 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<Order, Guid> |
|||
{ |
|||
Task<Order> FindByOrderNumberAsync(string orderNumber, bool includeDetails = false); |
|||
Task<List<Order>> 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<Order, Guid> { } |
|||
|
|||
// ❌ Wrong: Repository for child entity (OrderLine) |
|||
// OrderLine should only be accessed through Order aggregate |
|||
public interface IOrderLineRepository : IRepository<OrderLine, Guid> { } // 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<Order> 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<OrderCompletedEvent>, 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<Order> |
|||
{ |
|||
public override Expression<Func<Order, bool>> ToExpression() |
|||
{ |
|||
return o => o.Status == OrderStatus.Completed; |
|||
} |
|||
} |
|||
|
|||
// Usage |
|||
var orders = await _orderRepository.GetListAsync(new CompletedOrdersSpec()); |
|||
``` |
|||
@ -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<Book, Guid> |
|||
{ |
|||
Task<Book> FindByNameAsync(string name); |
|||
} |
|||
``` |
|||
|
|||
### Implementation Location |
|||
```csharp |
|||
// In EntityFrameworkCore project |
|||
public class BookRepository : EfCoreRepository<MyDbContext, Book, Guid>, IBookRepository |
|||
{ |
|||
// Implementation |
|||
} |
|||
|
|||
// In MongoDB project |
|||
public class BookRepository : MongoDbRepository<MyDbContext, Book, Guid>, 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 | |
|||
@ -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<Guid> |
|||
{ |
|||
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<Book, Guid>` directly: |
|||
|
|||
```csharp |
|||
// Only if custom queries are needed |
|||
public interface IBookRepository : IRepository<Book, Guid> |
|||
{ |
|||
Task<Book> FindByNameAsync(string name); |
|||
} |
|||
``` |
|||
|
|||
### 4. EF Core Configuration |
|||
In `*.EntityFrameworkCore/`: |
|||
|
|||
**DbContext:** |
|||
```csharp |
|||
public DbSet<Book> Books { get; set; } |
|||
``` |
|||
|
|||
**OnModelCreating:** |
|||
```csharp |
|||
builder.Entity<Book>(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<MyDbContext, Book, Guid>, IBookRepository |
|||
{ |
|||
public BookRepository(IDbContextProvider<MyDbContext> dbContextProvider) |
|||
: base(dbContextProvider) |
|||
{ |
|||
} |
|||
|
|||
public async Task<Book> 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<Guid> |
|||
{ |
|||
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<BookDto> GetAsync(Guid id); |
|||
Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input); |
|||
Task<BookDto> 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<BookDto> MapToDtoList(List<Book> books); |
|||
} |
|||
``` |
|||
|
|||
Register in module: |
|||
```csharp |
|||
context.Services.AddSingleton<BookMapper>(); |
|||
``` |
|||
|
|||
### 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<Book, Guid> _bookRepository; // Or IBookRepository |
|||
private readonly BookMapper _bookMapper; |
|||
|
|||
public BookAppService( |
|||
IRepository<Book, Guid> bookRepository, |
|||
BookMapper bookMapper) |
|||
{ |
|||
_bookRepository = bookRepository; |
|||
_bookMapper = bookMapper; |
|||
} |
|||
|
|||
public async Task<BookDto> GetAsync(Guid id) |
|||
{ |
|||
var book = await _bookRepository.GetAsync(id); |
|||
return _bookMapper.MapToDto(book); |
|||
} |
|||
|
|||
[Authorize(MyProjectPermissions.Books.Create)] |
|||
public async Task<BookDto> 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<IBookAppService>(); |
|||
} |
|||
|
|||
[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 |
|||
@ -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<int>("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<PdfReportDto> 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<int>("MyApp.MaxProductCount"); |
|||
``` |
|||
|
|||
## Distributed Caching |
|||
|
|||
### Typed Cache |
|||
```csharp |
|||
public class BookService : ITransientDependency |
|||
{ |
|||
private readonly IDistributedCache<BookCacheItem> _cache; |
|||
private readonly IClock _clock; |
|||
|
|||
public BookService(IDistributedCache<BookCacheItem> cache, IClock clock) |
|||
{ |
|||
_cache = cache; |
|||
_clock = clock; |
|||
} |
|||
|
|||
public async Task<BookCacheItem> 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<OrderCreatedEvent>, 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<OrderCreatedEto>, 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<EmailSendingArgs>, 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<MyResource>` |
|||
|
|||
> **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 |
|||
@ -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<Guid>, 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<Product, Guid> _productRepository; |
|||
|
|||
public async Task<long> 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<long> GetAllProductCountAsync() |
|||
{ |
|||
// DataFilter is available from base class |
|||
using (DataFilter.Disable<IMultiTenant>()) |
|||
{ |
|||
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<AbpMultiTenancyOptions>(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<AbpTenantResolveOptions>(options => |
|||
{ |
|||
options.AddDomainTenantResolver("{0}.mydomain.com"); |
|||
}); |
|||
``` |
|||
@ -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<MyProjectDbContext> |
|||
{ |
|||
public DbSet<Book> Books { get; set; } |
|||
public DbSet<Author> Authors { get; set; } |
|||
|
|||
public MyProjectDbContext(DbContextOptions<MyProjectDbContext> 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<Book>(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<Author>() |
|||
.WithMany() |
|||
.HasForeignKey(x => x.AuthorId) |
|||
.OnDelete(DeleteBehavior.Restrict); |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Repository Implementation |
|||
|
|||
```csharp |
|||
public class BookRepository : EfCoreRepository<MyProjectDbContext, Book, Guid>, IBookRepository |
|||
{ |
|||
public BookRepository(IDbContextProvider<MyProjectDbContext> dbContextProvider) |
|||
: base(dbContextProvider) |
|||
{ |
|||
} |
|||
|
|||
public async Task<Book> 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<List<Book>> 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<IQueryable<Book>> WithDetailsAsync() |
|||
{ |
|||
return (await GetQueryableAsync()) |
|||
.Include(b => b.Reviews); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Extension Method for Include |
|||
```csharp |
|||
public static class BookEfCoreQueryableExtensions |
|||
{ |
|||
public static IQueryable<Book> IncludeDetails( |
|||
this IQueryable<Book> 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<MyProjectDbContext>(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<AbpDbContextOptions>(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<MyEntity>(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<Book, Guid> _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 |
|||
); |
|||
} |
|||
} |
|||
``` |
|||
@ -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<Book> Books => Collection<Book>(); |
|||
public IMongoCollection<Author> Authors => Collection<Author>(); |
|||
|
|||
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<Book>(b => |
|||
{ |
|||
b.CollectionName = MyProjectConsts.DbTablePrefix + "Books"; |
|||
}); |
|||
|
|||
builder.Entity<Author>(b => |
|||
{ |
|||
b.CollectionName = MyProjectConsts.DbTablePrefix + "Authors"; |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Repository Implementation |
|||
|
|||
```csharp |
|||
public class BookRepository : MongoDbRepository<MyProjectMongoDbContext, Book, Guid>, IBookRepository |
|||
{ |
|||
public BookRepository(IMongoDbContextProvider<MyProjectMongoDbContext> dbContextProvider) |
|||
: base(dbContextProvider) |
|||
{ |
|||
} |
|||
|
|||
public async Task<Book> FindByNameAsync( |
|||
string name, |
|||
bool includeDetails = true, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
return await (await GetQueryableAsync()) |
|||
.FirstOrDefaultAsync( |
|||
b => b.Name == name, |
|||
GetCancellationToken(cancellationToken)); |
|||
} |
|||
|
|||
public async Task<List<Book>> 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<MyProjectMongoDbContext>(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<List<Book>> 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<Guid> |
|||
{ |
|||
public List<OrderLine> Lines { get; set; } // Embedded |
|||
} |
|||
|
|||
// Reference (separate collection, store ID only) |
|||
public class Order : AggregateRoot<Guid> |
|||
{ |
|||
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<Book>.Filter.Eq(b => b.AuthorId, authorId); |
|||
var update = Builders<Book>.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<MyProjectMongoDbContext, Book, Guid>, IBookRepository |
|||
{ |
|||
public override async Task<IQueryable<Book>> GetQueryableAsync() |
|||
{ |
|||
var collection = await GetCollectionAsync(); |
|||
|
|||
// Ensure index exists |
|||
var indexKeys = Builders<Book>.IndexKeys.Ascending(b => b.Name); |
|||
await collection.Indexes.CreateOneAsync(new CreateIndexModel<Book>(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 |
|||
@ -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<T, TKey>` 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<Guid> |
|||
{ |
|||
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<Book, Guid> _bookRepository; |
|||
|
|||
// Generic repository is sufficient for single-layer apps |
|||
} |
|||
``` |
|||
@ -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<List<ProductDto>> GetProductsByIdsAsync(List<Guid> ids); |
|||
} |
|||
|
|||
// In CatalogService project |
|||
[IntegrationService] |
|||
public class ProductIntegrationService : ApplicationService, IProductIntegrationService |
|||
{ |
|||
public async Task<List<ProductDto>> GetProductsByIdsAsync(List<Guid> ids) |
|||
{ |
|||
var products = await _productRepository.GetListAsync(p => ids.Contains(p.Id)); |
|||
return ObjectMapper.Map<List<Product>, List<ProductDto>>(products); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
#### Step 2: Provider Service - Expose Integration Services |
|||
|
|||
```csharp |
|||
// In CatalogServiceModule.cs |
|||
Configure<AbpAspNetCoreMvcOptions>(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<List<OrderDto>> 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<StockCountChangedEto>, 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<Product, ProductDto, Guid>(); |
|||
|
|||
// Use - auto-invalidates on entity changes |
|||
private readonly IEntityCache<ProductDto, Guid> _productCache; |
|||
|
|||
public async Task<ProductDto> 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 |
|||
@ -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<Book, Guid> |
|||
{ |
|||
Task<Book> FindByNameAsync(string name); |
|||
Task<List<Book>> GetListByAuthorAsync(Guid authorId); |
|||
} |
|||
``` |
|||
|
|||
### EF Core Implementation |
|||
```csharp |
|||
public class BookRepository : EfCoreRepository<MyModuleDbContext, Book, Guid>, IBookRepository |
|||
{ |
|||
public async Task<Book> FindByNameAsync(string name) |
|||
{ |
|||
var dbSet = await GetDbSetAsync(); |
|||
return await dbSet.FirstOrDefaultAsync(b => b.Name == name); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### MongoDB Implementation |
|||
```csharp |
|||
public class BookRepository : MongoDbRepository<MyModuleMongoDbContext, Book, Guid>, IBookRepository |
|||
{ |
|||
public async Task<Book> 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<Book>(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<MyModuleOptions>(options => |
|||
{ |
|||
options.EnableFeatureX = true; |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
Usage in service: |
|||
```csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly MyModuleOptions _options; |
|||
|
|||
public MyService(IOptions<MyModuleOptions> 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<BookDto> 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<Book> 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<string>("CustomProperty"); |
|||
}); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Localization |
|||
|
|||
```csharp |
|||
// Domain.Shared |
|||
[LocalizationResourceName("MyModule")] |
|||
public class MyModuleResource |
|||
{ |
|||
} |
|||
|
|||
// Module configuration |
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Add<MyModuleResource>("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 |
|||
@ -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<IBookAppService>(); |
|||
} |
|||
|
|||
[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<AbpValidationException>(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<BookManager>(); |
|||
_bookRepository = GetRequiredService<IBookRepository>(); |
|||
} |
|||
|
|||
[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<BusinessException>(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<BusinessException>(async () => |
|||
{ |
|||
await _service.DoSomethingAsync(); |
|||
}); |
|||
|
|||
var ex = await Should.ThrowAsync<BusinessException>(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<IEmailSender>(); |
|||
emailSender.SendAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>()) |
|||
.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 |
|||
@ -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<BookDto>; |
|||
|
|||
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 |
|||
<!-- In template --> |
|||
<h1>{{ '::Books' | abpLocalization }}</h1> |
|||
|
|||
<!-- With parameters --> |
|||
<p>{{ '::WelcomeMessage' | abpLocalization: userName }}</p> |
|||
``` |
|||
|
|||
## Authorization |
|||
|
|||
### Permission Directive |
|||
```html |
|||
<button *abpPermission="'BookStore.Books.Create'">Create</button> |
|||
``` |
|||
|
|||
### 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 |
|||
<form [formGroup]="form" (ngSubmit)="save()"> |
|||
<div class="form-group"> |
|||
<label for="name">{{ '::Name' | abpLocalization }}</label> |
|||
<input type="text" id="name" formControlName="name" class="form-control" /> |
|||
</div> |
|||
|
|||
<button type="submit" class="btn btn-primary" [disabled]="form.invalid"> |
|||
{{ '::Save' | abpLocalization }} |
|||
</button> |
|||
</form> |
|||
``` |
|||
|
|||
## 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` |
|||
@ -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 |
|||
|
|||
<h1>@L["Books"]</h1> |
|||
``` |
|||
|
|||
### CRUD Page |
|||
```razor |
|||
@page "/books" |
|||
@inherits AbpCrudPageBase<IBookAppService, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto> |
|||
|
|||
<Card> |
|||
<CardHeader> |
|||
<Row> |
|||
<Column> |
|||
<h2>@L["Books"]</h2> |
|||
</Column> |
|||
<Column TextAlignment="TextAlignment.End"> |
|||
@if (HasCreatePermission) |
|||
{ |
|||
<Button Color="Color.Primary" Clicked="OpenCreateModalAsync"> |
|||
@L["NewBook"] |
|||
</Button> |
|||
} |
|||
</Column> |
|||
</Row> |
|||
</CardHeader> |
|||
<CardBody> |
|||
<DataGrid TItem="BookDto" |
|||
Data="Entities" |
|||
ReadData="OnDataGridReadAsync" |
|||
TotalItems="TotalCount" |
|||
ShowPager="true" |
|||
PageSize="PageSize"> |
|||
<DataGridColumns> |
|||
<DataGridColumn Field="@nameof(BookDto.Name)" Caption="@L["Name"]" /> |
|||
<DataGridColumn Field="@nameof(BookDto.Price)" Caption="@L["Price"]" /> |
|||
<DataGridEntityActionsColumn TItem="BookDto"> |
|||
<DisplayTemplate> |
|||
<EntityActions TItem="BookDto"> |
|||
<EntityAction TItem="BookDto" |
|||
Text="@L["Edit"]" |
|||
Visible="HasUpdatePermission" |
|||
Clicked="() => OpenEditModalAsync(context)" /> |
|||
<EntityAction TItem="BookDto" |
|||
Text="@L["Delete"]" |
|||
Visible="HasDeletePermission" |
|||
Clicked="() => DeleteEntityAsync(context)" |
|||
ConfirmationMessage="() => GetDeleteConfirmationMessage(context)" /> |
|||
</EntityActions> |
|||
</DisplayTemplate> |
|||
</DataGridEntityActionsColumn> |
|||
</DataGridColumns> |
|||
</DataGrid> |
|||
</CardBody> |
|||
</Card> |
|||
``` |
|||
|
|||
## Localization |
|||
```razor |
|||
@* Using L property from base class *@ |
|||
<h1>@L["PageTitle"]</h1> |
|||
|
|||
@* With parameters *@ |
|||
<p>@L["WelcomeMessage", CurrentUser.UserName]</p> |
|||
``` |
|||
|
|||
## Authorization |
|||
```razor |
|||
@* Check permission before rendering *@ |
|||
@if (await AuthorizationService.IsGrantedAsync("MyPermission")) |
|||
{ |
|||
<Button>Admin Action</Button> |
|||
} |
|||
|
|||
@* Using policy-based authorization *@ |
|||
<AuthorizeView Policy="MyPolicy"> |
|||
<Authorized> |
|||
<p>You have access!</p> |
|||
</Authorized> |
|||
</AuthorizeView> |
|||
``` |
|||
|
|||
## 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 |
|||
<Form @ref="CreateForm"> |
|||
<Validations @ref="CreateValidationsRef" Model="@NewEntity" ValidateOnLoad="false"> |
|||
<Validation MessageLocalizer="@LH.Localize"> |
|||
<Field> |
|||
<FieldLabel>@L["Name"]</FieldLabel> |
|||
<TextEdit @bind-Text="@NewEntity.Name"> |
|||
<Feedback> |
|||
<ValidationError /> |
|||
</Feedback> |
|||
</TextEdit> |
|||
</Field> |
|||
</Validation> |
|||
</Validations> |
|||
</Form> |
|||
``` |
|||
|
|||
## JavaScript Interop |
|||
```csharp |
|||
@inject IJSRuntime JsRuntime |
|||
|
|||
@code { |
|||
private async Task CallJavaScript() |
|||
{ |
|||
await JsRuntime.InvokeVoidAsync("myFunction", arg1, arg2); |
|||
var result = await JsRuntime.InvokeAsync<string>("myFunctionWithReturn"); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## State Management |
|||
```csharp |
|||
// Inject service proxy from HttpApi.Client |
|||
@inject IBookAppService BookAppService |
|||
|
|||
@code { |
|||
private List<BookDto> 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; } |
|||
} |
|||
``` |
|||
@ -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<BookDto> 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 |
|||
|
|||
<abp-card> |
|||
<abp-card-header> |
|||
<abp-row> |
|||
<abp-column size-md="_6"> |
|||
<h2>@L["Books"]</h2> |
|||
</abp-column> |
|||
<abp-column size-md="_6" class="text-end"> |
|||
<abp-button button-type="Primary" |
|||
id="NewBookButton" |
|||
text="@L["NewBook"].Value" /> |
|||
</abp-column> |
|||
</abp-row> |
|||
</abp-card-header> |
|||
<abp-card-body> |
|||
<abp-table striped-rows="true" id="BooksTable"> |
|||
<thead> |
|||
<tr> |
|||
<th>@L["Name"]</th> |
|||
<th>@L["Price"]</th> |
|||
<th>@L["Actions"]</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
@foreach (var book in Model.Books) |
|||
{ |
|||
<tr> |
|||
<td>@book.Name</td> |
|||
<td>@book.Price</td> |
|||
<td> |
|||
<abp-button button-type="Primary" size="Small" |
|||
text="@L["Edit"].Value" /> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</abp-table> |
|||
</abp-card-body> |
|||
</abp-card> |
|||
``` |
|||
|
|||
## ABP Tag Helpers |
|||
|
|||
### Cards |
|||
```html |
|||
<abp-card> |
|||
<abp-card-header>Header</abp-card-header> |
|||
<abp-card-body>Content</abp-card-body> |
|||
<abp-card-footer>Footer</abp-card-footer> |
|||
</abp-card> |
|||
``` |
|||
|
|||
### Buttons |
|||
```html |
|||
<abp-button button-type="Primary" text="@L["Save"].Value" /> |
|||
<abp-button button-type="Danger" icon="fa fa-trash" /> |
|||
``` |
|||
|
|||
### Forms |
|||
```html |
|||
<abp-dynamic-form abp-model="Book" asp-page="/Books/CreateModal"> |
|||
<abp-modal> |
|||
<abp-modal-header title="@L["NewBook"].Value" /> |
|||
<abp-modal-body> |
|||
<abp-form-content /> |
|||
</abp-modal-body> |
|||
<abp-modal-footer buttons="@(AbpModalButtons.Save | AbpModalButtons.Cancel)" /> |
|||
</abp-modal> |
|||
</abp-dynamic-form> |
|||
``` |
|||
|
|||
### Tables |
|||
```html |
|||
<abp-table striped-rows="true" hoverable-rows="true"> |
|||
<!-- content --> |
|||
</abp-table> |
|||
``` |
|||
|
|||
## Localization |
|||
```html |
|||
@* In Razor views/pages *@ |
|||
<h1>@L["Books"]</h1> |
|||
|
|||
@* With parameters *@ |
|||
<p>@L["WelcomeMessage", Model.UserName]</p> |
|||
``` |
|||
|
|||
## 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 |
|||
|
|||
<abp-dynamic-form abp-model="Book" asp-page="/Books/CreateModal"> |
|||
<abp-modal> |
|||
<abp-modal-header title="@L["NewBook"].Value" /> |
|||
<abp-modal-body> |
|||
<abp-form-content /> |
|||
</abp-modal-body> |
|||
<abp-modal-footer buttons="@(AbpModalButtons.Save | AbpModalButtons.Cancel)" /> |
|||
</abp-modal> |
|||
</abp-dynamic-form> |
|||
``` |
|||
|
|||
**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<IActionResult> OnPostAsync() |
|||
{ |
|||
await _bookAppService.CreateAsync(Book); |
|||
return NoContent(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Bundle & Minification |
|||
```csharp |
|||
Configure<AbpBundlingOptions>(options => |
|||
{ |
|||
options.StyleBundles.Configure( |
|||
StandardBundles.Styles.Global, |
|||
bundle => bundle.AddFiles("/styles/my-styles.css") |
|||
); |
|||
}); |
|||
``` |
|||
Loading…
Reference in new issue