CursorPool
← 返回首页

ABP Framework

ABP Rules and ABP MCP server packaged as a Cursor plugin.

cursor.directory·18
规则

Core ABP Framework conventions - module system, dependency injection, and base classes

Core ABP Framework conventions - module system, dependency injection, and base classes

# 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 |
规则

ABP Application Services, DTOs, validation, and error handling patterns

ABP Application Services, DTOs, validation, and error handling patterns

# 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.
规则

ABP permission system and authorization patterns

ABP permission system and authorization patterns

# 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
规则

ABP DDD patterns - Entities, Aggregate Roots, Repositories, Domain Services

ABP DDD patterns - Entities, Aggregate Roots, Repositories, Domain Services

# 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());
```
规则

ABP layer dependency rules and project structure guardrails

ABP layer dependency rules and project structure guardrails

# 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 |
规则

ABP development workflow - adding features, entities, and migrations

ABP development workflow - adding features, entities, and migrations

# 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
规则

ABP Entity Framework Core patterns - DbContext, migrations, repositories

ABP Entity Framework Core patterns - DbContext, migrations, repositories

# 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
        );
    }
}
```
规则

ABP infrastructure services - Settings, Features, Caching, Events, Background Jobs

ABP infrastructure services - Settings, Features, Caching, Events, Background Jobs

# 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
规则

ABP MongoDB patterns - MongoDbContext and repositories

ABP MongoDB patterns - MongoDbContext and repositories

# 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
规则

ABP testing patterns - unit tests and integration tests

ABP testing patterns - unit tests and integration tests

# 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
规则

ABP Angular UI patterns and best practices

ABP Angular UI patterns and best practices

# 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)
  }
];
```

## Component Replacement

ABP Angular provides a **component replacement** system via `ReplaceableComponentsService`:

```typescript
import { ReplaceableComponentsService } from '@abp/ng.core';
import { eIdentityComponents } from '@abp/ng.identity';

constructor(private replaceableComponents: ReplaceableComponentsService) {
  this.replaceableComponents.add({
    component: YourCustomComponent,
    key: eIdentityComponents.Roles,
  });
}
```

Replaceable targets include ABP default components (Roles, Users, Tenants), layouts (Application, Account, Empty), and UI elements (Logo, Routes, NavItems).

> **Docs**: https://abp.io/docs/latest/framework/ui/angular/customization-user-interface

## Modern Angular Patterns

### Standalone Components
Prefer **standalone components** (no `NgModules`). `standalone: true` is the default in modern Angular — do not set it manually.

### Signals & State
- Use **signals** for local component state
- Use `computed()` for derived state
- Avoid `mutate()` on signals — use `update()` or `set()`

### Components
- Set `changeDetection: ChangeDetectionStrategy.OnPush`
- Use `input()` and `output()` functions instead of decorators
- Use **Reactive Forms** over template-driven forms
- Prefer `[class]` bindings over `ngClass` and `[style]` over `ngStyle`

### Templates
- Use **native control flow** (`@if`, `@for`, `@switch`) instead of structural directives
- Use the **async pipe** for observable bindings

### Services
- Provide services using `providedIn: 'root'`
- Use the **`inject()` function** instead of constructor injection

### TypeScript
- Enable **strict type checking** in `tsconfig.json`
- Avoid `any`; use `unknown` or generics instead

## Theme & Styling
- Use Bootstrap classes
- ABP provides theme variables via CSS custom properties
- Component-specific styles in `.component.scss`
规则

ABP Blazor UI patterns and components

ABP Blazor UI patterns and components

# 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; }
}
```
规则

ABP MVC and Razor Pages UI patterns

ABP MVC and Razor Pages UI patterns

# 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")
    );
});
```
MCP

abp-studio

MCP server: abp-studio

{
  "command": "abp",
  "args": [
    "mcp-studio"
  ]
}
规则

ABP CLI commands: generate-proxy, install-libs, add-package-ref, new-module, install-module, update, clean, suite generate (CRUD pages)

ABP CLI commands: generate-proxy, install-libs, add-package-ref, new-module, install-module, update, clean, suite generate (CRUD pages)

# 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]` |
规则

ABP Multi-Tenancy patterns - tenant-aware entities, data isolation, and tenant switching

ABP Multi-Tenancy patterns - tenant-aware entities, data isolation, and tenant switching

# 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");
});
```
规则

ABP solution template structures - detect and follow the correct template patterns (app, app-nolayers, module, microservice)

ABP solution template structures - detect and follow the correct template patterns (app, app-nolayers, module, microservice)

# ABP Solution Templates

> **Docs**: https://abp.io/docs/latest/solution-templates

ABP provides four solution templates. Detect which one the developer is using from the solution structure, then follow that template's conventions.

## Template Detection

| Template | Key Indicators |
|----------|---------------|
| **Layered App** | Has `*.Domain/`, `*.Application/`, `*.Application.Contracts/`, `*.EntityFrameworkCore/` or `*.MongoDB/`, `*.HttpApi/`, `*.Web/` or `*.Blazor/` projects |
| **Single-Layer (No-Layers)** | Single `src/MyProject/` project with `Entities/`, `Services/`, `Data/` folders — no separate Domain or Application projects |
| **Module** | Has `host/` folder with test host app, plus `*.HttpApi.Client/` project for client proxies; designed for reuse |
| **Microservice** | Has `apps/`, `gateways/`, `services/` top-level folders; each service is a self-contained project |

---

## 1. Layered App Template

> **Docs**: https://abp.io/docs/latest/solution-templates/layered-web-application

Classic DDD-layered structure. Best for most business applications.

### Solution Structure

```
MyProject/
├── src/
│   ├── MyProject.Domain.Shared/        # Constants, enums, localization
│   ├── MyProject.Domain/               # Entities, repository interfaces, domain services
│   ├── MyProject.Application.Contracts/ # DTOs, app service interfaces
│   ├── MyProject.Application/          # App service implementations
│   ├── MyProject.EntityFrameworkCore/   # EF Core DbContext, migrations, repository impls
│   ├── MyProject.HttpApi/              # REST controllers (optional with Auto API)
│   ├── MyProject.HttpApi.Client/       # C# client proxies
│   ├── MyProject.Web/                  # MVC/Razor Pages UI (or .Blazor/)
│   └── MyProject.DbMigrator/          # Migration console app
└── test/
    ├── MyProject.Application.Tests/
    ├── MyProject.Domain.Tests/
    └── MyProject.EntityFrameworkCore.Tests/
```

### Layer Dependency Direction

```
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
```

| 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 |

### File Placement

| Artifact | Project |
|----------|---------|
| Entity | `*.Domain/Entities/` |
| Constants, enums | `*.Domain.Shared/` |
| Repository interface | `*.Domain/` (only if custom queries needed) |
| Repository implementation | `*.EntityFrameworkCore/` or `*.MongoDB/` |
| DTOs, service interface | `*.Application.Contracts/` |
| Service implementation | `*.Application/` |
| REST controller | `*.HttpApi/` (if not using Auto API Controllers) |

---

## 2. Single-Layer (No-Layers) App Template

> **Docs**: https://abp.io/docs/latest/solution-templates/single-layer-web-application

Everything in one project. Best for simple CRUD apps or rapid prototyping.

### Solution Structure

```
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

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;
}
```

---

## 3. Module Template

> **Docs**: https://abp.io/docs/latest/solution-templates/application-module

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

Modules must support both EF Core and MongoDB:

```csharp
// Repository interface (Domain)
public interface IBookRepository : IRepository<Book, Guid>
{
    Task<Book> FindByNameAsync(string name);
}

// EF Core implementation
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
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 in EF Core configuration
builder.Entity<Book>(b =>
{
    b.ToTable(MyModuleDbProperties.DbTablePrefix + "Books", MyModuleDbProperties.DbSchema);
});
```

### Virtual Methods (Critical for Modules!)

All public and protected methods **must be virtual** to allow consumers to override behavior:

```csharp
public class BookAppService : ApplicationService, IBookAppService
{
    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) { ... }
}
```

### Module Options

Provide configuration options for consumers:

```csharp
public class MyModuleOptions
{
    public bool EnableFeatureX { get; set; } = true;
    public int MaxItemCount { get; set; } = 100;
}

// Configure in module
Configure<MyModuleOptions>(options =>
{
    options.EnableFeatureX = true;
});

// Use in service
public class MyService : ITransientDependency
{
    private readonly MyModuleOptions _options;

    public MyService(IOptions<MyModuleOptions> options)
    {
        _options = options.Value;
    }
}
```

### Entity Extension

Support the object extension system so consumers can add properties:

```csharp
public class MyModuleModuleExtensionConfigurator
{
    public static void Configure()
    {
        OneTimeRunner.Run(() =>
        {
            ObjectExtensionManager.Instance.Modules()
                .ConfigureMyModule(module =>
                {
                    module.ConfigureBook(book =>
                    {
                        book.AddOrUpdateProperty<string>("CustomProperty");
                    });
                });
        });
    }
}
```

### Module Best Practices

1. **Virtual methods** — All public/protected methods must be `virtual`
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

---

## 4. Microservice 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 a 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

#### Synchronous: Integration Services

For synchronous calls, use **Integration Services** — NOT regular application services.

**Step 1: Provider — 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 — Expose Integration Services**

```csharp
// In CatalogServiceModule.cs
Configure<AbpAspNetCoreMvcOptions>(options =>
{
    options.ExposeIntegrationServices = true;
});
```

**Step 3: Consumer — 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 — Generate Proxies**

```bash
abp generate-proxy -t csharp -u http://localhost:44361 -m catalog --without-contracts
```

**Step 5: Consumer — Register HTTP Client Proxies**

```csharp
[DependsOn(typeof(CatalogServiceContractsModule))]
public class OrderingServiceModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.AddStaticHttpClientProxies(
            typeof(CatalogServiceContractsModule).Assembly,
            "CatalogService");
    }
}
```

**Step 6: Consumer — 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();
        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 synchronous:** Need immediate response, data required to complete current operation.

#### Asynchronous: Distributed Events

Use RabbitMQ-based events for loose coupling.

**When to use:** Notifying other services about state changes, operations that don't need immediate response, keeping services independent.

```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 the Outbox/Inbox pattern.

### Entity Cache

For frequently accessed data from other services:

```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

### Microservice Best Practices

1. **Choose communication wisely** — Synchronous for queries needing immediate data, asynchronous for notifications
2. **Use Integration Services** — Not application services for inter-service calls
3. **Cache remote data** — Use Entity Cache or `IDistributedCache` for frequently accessed data
4. **Share only Contracts** — Never share implementations between services
5. **Idempotent handlers** — Events may be delivered multiple times
6. **Database per service** — Each service owns its database

来源:https://github.com/abpframework/cursor-abp-plugin