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