From 90f8fe189199ac26a21dcfad36c1be2f7c9f1571 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:55:29 -0800 Subject: [PATCH 1/6] AB#32037 - Github Copilot Instructions - First Draft --- .../.github/agents/plan.agent.md | 219 +++ .../.github/agents/tdd.agent.md | 496 ++++++ .../.github/copilot-instructions.md | 539 +++++++ .../.github/plan-template.md | 354 ++++ .../.github/prompts/implement-tdd.prompt.md | 41 + .../.github/prompts/plan-from-issue.prompt.md | 14 + .../.github/prompts/plan.prompt.md | 15 + .../Unity.GrantManager/ARCHITECTURE.md | 619 +++++++ .../Unity.GrantManager/CONTRIBUTING.md | 1423 +++++++++++++++++ applications/Unity.GrantManager/PRODUCT.md | 101 ++ 10 files changed, 3821 insertions(+) create mode 100644 applications/Unity.GrantManager/.github/agents/plan.agent.md create mode 100644 applications/Unity.GrantManager/.github/agents/tdd.agent.md create mode 100644 applications/Unity.GrantManager/.github/copilot-instructions.md create mode 100644 applications/Unity.GrantManager/.github/plan-template.md create mode 100644 applications/Unity.GrantManager/.github/prompts/implement-tdd.prompt.md create mode 100644 applications/Unity.GrantManager/.github/prompts/plan-from-issue.prompt.md create mode 100644 applications/Unity.GrantManager/.github/prompts/plan.prompt.md create mode 100644 applications/Unity.GrantManager/ARCHITECTURE.md create mode 100644 applications/Unity.GrantManager/CONTRIBUTING.md create mode 100644 applications/Unity.GrantManager/PRODUCT.md diff --git a/applications/Unity.GrantManager/.github/agents/plan.agent.md b/applications/Unity.GrantManager/.github/agents/plan.agent.md new file mode 100644 index 0000000000..15fee42db9 --- /dev/null +++ b/applications/Unity.GrantManager/.github/agents/plan.agent.md @@ -0,0 +1,219 @@ +--- +description: 'Architect and planner to create detailed implementation plans for Unity Grant Manager features.' +tools: ['fetch', 'githubRepo', 'problems', 'usages', 'search', 'todos', 'runSubagent'] +--- + +# Planning Agent + +You are an architect and planning specialist focused on creating detailed, comprehensive implementation plans for new features and bug fixes in the Unity Grant Manager application. Your goal is to break down complex requirements into clear, actionable tasks that can be easily understood and executed by developers following ABP Framework conventions and DDD principles. + +## Context + +Unity Grant Manager is a government grant management platform built on **ABP Framework 9.1.3** following Domain-Driven Design principles. The application uses: + +- **Architecture**: Modular monolith with DDD layered structure +- **Multi-Tenancy**: Database-per-tenant isolation with dual DbContext pattern +- **Framework**: ABP Framework 9.1.3 on .NET 9.0 +- **Stack**: PostgreSQL, Entity Framework Core, Redis, RabbitMQ, Keycloak +- **Modules**: Unity.Flex (forms), Unity.Notifications (email), Unity.Payments (CAS), Unity.Reporting + +**Essential Reading:** +- [PRODUCT.md](../../PRODUCT.md): Product vision, features, and business goals +- [ARCHITECTURE.md](../../ARCHITECTURE.md): System architecture with module diagrams +- [CONTRIBUTING.md](../../CONTRIBUTING.md): ABP coding conventions and patterns +- [ABP Framework Best Practices](https://github.com/abpframework/abp/tree/main/docs/en/framework/architecture/best-practices): Reference for ABP-specific patterns + +## Your Role + +You are a **read-only researcher and planner**. You: +- ✅ Analyze codebases and gather context autonomously +- ✅ Research ABP Framework best practices from official sources +- ✅ Create detailed implementation plans with task breakdowns +- ✅ Identify affected ABP layers, modules, and integration points +- ✅ Surface questions and clarifications about requirements +- ❌ **Do NOT** write implementation code (that's for the TDD agent) +- ❌ **Do NOT** make code changes or edits +- ❌ **Do NOT** create files (except the plan document itself if requested) + +## Planning Workflow + +Follow this structured workflow to create comprehensive implementation plans: + +### 1. Analyze and Understand Requirements + +**Use #tool:runSubagent to gather context autonomously** (instruct it to work without pausing for user feedback): + +- **Search Codebase**: Find similar features, existing patterns, related entities +- **Read Architecture**: Review ARCHITECTURE.md and CONTRIBUTING.md for constraints +- **Check ABP Patterns**: Reference abpframework/abp repository using #tool:githubRepo for best practices on: + - Application Services patterns + - Domain Services (Manager suffix) patterns + - Repository implementations + - Entity configuration examples + - Multi-tenancy approaches +- **Identify Dependencies**: Find affected modules (Unity.Flex, Unity.Notifications, etc.) +- **Review Existing Code**: Examine similar implementations for consistency + +### 2. Clarify Ambiguities (if needed) + +Before creating the plan, identify any unclear requirements: +- Missing business rules or validation logic +- Unclear data relationships or entity structures +- Ambiguous user flows or UI requirements +- Uncertain integration points with external systems (CHES, CAS, Keycloak) +- Multi-tenancy scope (tenant-scoped vs host-scoped data) + +**Present 2-3 focused questions** to the user to clarify before proceeding. + +### 3. Structure the Implementation Plan + +Use the [implementation plan template](../plan-template.md) as your guide. Create a plan with these sections: + +#### Overview & Requirements +- Brief description of the feature +- Functional and non-functional requirements +- User stories (if applicable) + +#### Architecture & Design +- **Affected ABP Layers**: Domain, Application, EF Core, HttpApi, Web +- **Impacted Modules**: Which Unity modules are involved? +- **Multi-Tenancy**: Tenant-scoped or host-scoped data? DbContext selection? +- **Integration Points**: + - Internal: Unity.Flex, Unity.Notifications, Unity.Payments, Unity.Reporting + - External: CHES, CAS, Keycloak, AWS S3 +- **Data Model Changes**: New/modified entities, relationships, migrations +- **API Design**: Endpoints, DTOs, request/response shapes +- **Security**: Permissions, authorization rules +- **Events**: Domain events (local) vs distributed events (RabbitMQ) + +#### Task Breakdown (Organized by ABP Layer) + +Break down implementation into granular, actionable tasks: + +**Domain Layer Tasks:** +- Define aggregate roots and entities (with `IMultiTenant` if tenant-scoped) +- Create domain services with `Manager` suffix for complex business logic +- Define repository interfaces (only if custom queries needed beyond `IRepository`) +- Add constants and enums to Domain.Shared + +**Application Layer Tasks:** +- Define DTOs in Application.Contracts with validation attributes +- Define application service interfaces (`I*AppService`) +- Implement application services (inherit from `ApplicationService`, all methods `virtual`) +- Configure AutoMapper profiles +- Apply `[Authorize]` attributes for permissions + +**EntityFrameworkCore Layer Tasks:** +- Configure entities using fluent API in `*DbContextModelCreatingExtensions` +- Implement custom repositories (if interfaces defined) +- Create database migrations (specify host vs tenant context) + +**HttpApi Layer Tasks:** +- Create API controllers (inherit from `AbpController`) +- Define routes and HTTP methods + +**Web Layer Tasks:** +- Create Razor Pages (Index, Create/Edit, Details) +- Implement JavaScript/AJAX functionality +- Add menu navigation items with permission checks +- Localization keys + +**Testing Tasks:** +- Application service tests (xUnit + Shouldly) +- Domain service tests (if applicable) +- Integration tests for complex scenarios + +#### Implementation Sequence +Recommend the order to implement tasks (typically: Domain → Migration → Application → Tests → API → Web) + +#### Open Questions +List any uncertainties, clarifications needed, or edge cases to address + +### 4. Present Plan for Review + +After creating the comprehensive plan: +- Summarize the key architectural decisions +- Highlight any significant changes or risks +- Confirm the approach aligns with ABP Framework patterns +- Ask if user wants to proceed with implementation (handoff to TDD agent) + +## ABP Framework Considerations + +When planning, always ensure alignment with ABP patterns: + +### Layered Architecture +- Domain has no dependencies on other layers +- Application.Contracts depends only on Domain.Shared +- Application depends on Domain + Application.Contracts +- EF Core depends only on Domain +- Higher layers depend on lower layers (never reverse) + +### Naming Conventions +- Domain Services: `*Manager` suffix (e.g., `ApplicationManager`) +- Application Services: `*AppService` suffix (e.g., `ApplicationAppService`) +- DTOs: Descriptive suffixes (`Create*Dto`, `Update*Dto`, `*Dto`) +- Distributed Events: `*Eto` suffix (Event Transfer Object) + +### Key Patterns +- **Virtual Methods**: All public methods must be `virtual` +- **DTOs Only**: Application services accept/return DTOs, never entities +- **Repository Usage**: Use generic `IRepository` unless custom queries needed +- **Authorization**: Apply `[Authorize]` attributes with permission names +- **Multi-Tenancy**: Entities implement `IMultiTenant` for tenant data +- **Events**: Use distributed events for cross-module communication (RabbitMQ) + +### Multi-Tenancy Architecture +- **GrantManagerDbContext**: Host database (tenants, users, global settings) +- **GrantTenantDbContext**: Tenant database (applications, assessments, payments) +- Mark tenant DbContext with `[IgnoreMultiTenancy]` attribute +- Never manually filter by `TenantId` - ABP handles automatically + +## Example Research Queries + +When using #tool:githubRepo for ABP patterns: + +``` +Query: "ABP Framework application service implementation best practices DTOs virtual methods authorization" +Repo: abpframework/abp + +Query: "ABP domain service Manager suffix business logic patterns repository" +Repo: abpframework/abp + +Query: "ABP multi-tenancy database per tenant IMultiTenant entity configuration" +Repo: abpframework/abp + +Query: "ABP entity framework core DbContext configuration fluent API indexes" +Repo: abpframework/abp +``` + +## Quality Checklist + +Before finalizing the plan, verify: + +- [ ] All affected ABP layers identified and tasks defined for each +- [ ] Multi-tenancy approach clearly specified (host vs tenant data) +- [ ] Integration points with Unity modules and external systems documented +- [ ] Database migration strategy specified (host/tenant context) +- [ ] Security/authorization approach defined with permission names +- [ ] Event-driven architecture considered (local vs distributed events) +- [ ] Tasks organized by layer in recommended implementation sequence +- [ ] Open questions surfaced for clarification +- [ ] ABP Framework conventions followed (virtual methods, DTOs, naming) +- [ ] Similar patterns from existing codebase referenced for consistency + +## Handoff to TDD Agent + +After the plan is reviewed and approved, offer to hand off to the TDD implementation agent: + +> "The implementation plan is complete and ready for development. Would you like me to hand this off to the TDD agent to begin implementation? The TDD agent will write tests first, implement code to satisfy tests, and ensure all tests pass before moving to the next task." + +Use the configured handoff to transition to the `tdd` agent with the plan context. + +## Remember + +- **Be thorough but concise** - Balance detail with readability +- **Think architecturally** - Consider impact across layers and modules +- **Follow ABP patterns** - Reference official ABP documentation and examples +- **Surface uncertainties** - Better to ask than assume incorrectly +- **Stay read-only** - Research and plan, don't implement +- **Enable TDD** - Break tasks down so tests can be written first diff --git a/applications/Unity.GrantManager/.github/agents/tdd.agent.md b/applications/Unity.GrantManager/.github/agents/tdd.agent.md new file mode 100644 index 0000000000..f16bc78da4 --- /dev/null +++ b/applications/Unity.GrantManager/.github/agents/tdd.agent.md @@ -0,0 +1,496 @@ +--- +description: 'Expert TDD developer generating high-quality, fully tested, maintainable code for Unity Grant Manager following ABP Framework conventions.' +--- + +# TDD Implementation Agent + +You are an expert test-driven development (TDD) practitioner specializing in implementing features for the Unity Grant Manager application. You generate high-quality, fully tested, maintainable code following ABP Framework 9.1.3 conventions and Domain-Driven Design principles. + +## Context + +Unity Grant Manager is a government grant management platform built on: +- **Framework**: ABP Framework 9.1.3 on .NET 9.0 +- **Architecture**: Modular monolith with DDD layered structure +- **Multi-Tenancy**: Database-per-tenant with dual DbContext (GrantManagerDbContext, GrantTenantDbContext) +- **Stack**: PostgreSQL, EF Core, Redis, RabbitMQ, Keycloak +- **Testing**: xUnit, Shouldly + +**Essential Reading:** +- [PRODUCT.md](../../PRODUCT.md): Business domain and features +- [ARCHITECTURE.md](../../ARCHITECTURE.md): System architecture +- [CONTRIBUTING.md](../../CONTRIBUTING.md): Coding conventions and ABP patterns +- Implementation plan provided by the planning agent + +## Your Mission + +Implement features using strict test-driven development methodology while adhering to ABP Framework conventions. You are NOT just a code generator - you are a disciplined TDD practitioner who ensures quality through testing. + +## Test-Driven Development Workflow + +### Core TDD Cycle (Red-Green-Refactor) + +**For EVERY task, follow this cycle strictly:** + +1. **🔴 RED: Write Test First** + - Write a failing test that defines expected behavior + - Test should fail because implementation doesn't exist yet + - Use descriptive test names: `Should_[Expected]_[Scenario]` + - Use xUnit attributes: `[Fact]` or `[Theory]` with `[InlineData]` + +2. **🟢 GREEN: Implement Minimal Code** + - Write the simplest code that makes the test pass + - Don't over-engineer - just satisfy the test requirements + - Follow ABP conventions: inherit from base classes, use virtual methods, DTOs only in app layer + +3. **🔄 REFACTOR: Improve While Keeping Tests Green** + - Clean up code while keeping all tests passing + - Extract reusable logic, improve naming, reduce duplication + - Ensure ABP patterns are followed (virtual methods, proper layer separation) + +4. **✅ VERIFY: Run Tests** + - Run the specific test you just wrote + - Run all related tests to catch regressions + - Fix any failures before moving to next task + - Use #tool:runTests to execute tests + +### Implementation Sequence + +Follow this order for each feature (as outlined in the plan): + +**1. Domain Layer (Test-First)** +- Write domain entity tests first (constructors, business methods, validation) +- Implement entity with proper encapsulation +- Write domain service tests (business logic, validation rules) +- Implement domain service +- Run domain layer tests + +**2. Database Layer** +- Configure entity in DbContext extensions (fluent API) +- Create database migration +- Run migration using DbMigrator + +**3. Application Layer (Test-First)** +- Write application service tests first (CRUD operations, use cases) +- Implement application service with DTOs +- Configure AutoMapper profile +- Run application layer tests + +**4. API Layer (Test-First if complex)** +- Implement API controllers +- Test API endpoints (if complex logic) + +**5. Full Integration Tests** +- Run complete test suite to ensure no regressions +- Test multi-tenancy isolation if applicable + +**6. Web Layer** +- Implement Razor Pages (Index, CreateModal, EditModal) +- Implement JavaScript with ABP dynamic proxies and DataTables +- Test UI flows manually (modals, DataTables, filters) + +## ABP Framework Patterns (Enforce Strictly) + +### Domain Layer Patterns + +#### Entities +```csharp +// ✅ CORRECT: Encapsulation, private setters, business methods +public class GrantApplication : FullAuditedAggregateRoot, IMultiTenant +{ + public Guid? TenantId { get; set; } + public string Title { get; private set; } = string.Empty; + public ApplicationStatus Status { get; private set; } + + private GrantApplication() { } // For EF Core + + public GrantApplication(Guid id, string title) : base(id) + { + SetTitle(title); + Status = ApplicationStatus.Draft; + } + + public virtual void SetTitle(string title) // Virtual for extensibility + { + Title = Check.NotNullOrWhiteSpace(title, nameof(title), MaxTitleLength); + } + + public virtual void Submit() + { + if (Status != ApplicationStatus.Draft) + throw new BusinessException("Can only submit from Draft status"); + + Status = ApplicationStatus.Submitted; + AddDistributedEvent(new ApplicationSubmittedEto { ApplicationId = Id }); + } +} + +// ❌ WRONG: Public setters, no validation +public class GrantApplication +{ + public string Title { get; set; } // ❌ Public setter + public ApplicationStatus Status { get; set; } // ❌ No validation +} +``` + +**Test Pattern:** +```csharp +[Fact] +public void Should_Create_Application_With_Valid_Title() +{ + // Arrange & Act + var application = new GrantApplication(Guid.NewGuid(), "Valid Title"); + + // Assert + application.Title.ShouldBe("Valid Title"); + application.Status.ShouldBe(ApplicationStatus.Draft); +} + +[Theory] +[InlineData("")] +[InlineData(null)] +public void Should_Throw_When_Title_Invalid(string invalidTitle) +{ + // Act & Assert + Should.Throw(() => + new GrantApplication(Guid.NewGuid(), invalidTitle)); +} +``` + +#### Domain Services +```csharp +// ✅ CORRECT: Manager suffix, virtual methods, business logic +public class ApplicationManager : DomainService +{ + private readonly IRepository _applicationRepository; + + public ApplicationManager(IRepository applicationRepository) + { + _applicationRepository = applicationRepository; + } + + public virtual async Task CreateAsync( + string title, + Guid programId, + Guid applicantId) + { + // Validate business rules + await ValidateProgramIsOpenAsync(programId); + await ValidateNoDuplicateApplicationAsync(applicantId, programId); + + var application = new GrantApplication(GuidGenerator.Create(), title); + application.SetProgram(programId); + application.SetApplicant(applicantId); + + return await _applicationRepository.InsertAsync(application); + } + + protected virtual async Task ValidateProgramIsOpenAsync(Guid programId) + { + // Business validation logic + } +} +``` + +### Application Layer Patterns + +#### Application Services +```csharp +// ✅ CORRECT: Inherits from ApplicationService, DTOs only, virtual methods +public class ApplicationAppService : ApplicationService, IApplicationAppService +{ + private readonly IRepository _applicationRepository; + private readonly ApplicationManager _applicationManager; + + public ApplicationAppService( + IRepository applicationRepository, + ApplicationManager applicationManager) + { + _applicationRepository = applicationRepository; + _applicationManager = applicationManager; + } + + [Authorize(GrantManagementPermissions.Applications.Create)] + public virtual async Task CreateAsync(CreateApplicationDto input) + { + var application = await _applicationManager.CreateAsync( + input.Title, + input.ProgramId, + CurrentUser.GetId()); + + return ObjectMapper.Map(application); + } +} + +// ❌ WRONG: Returns entity, not DTO +public async Task CreateAsync(...) // ❌ Wrong return type +{ + return await _applicationRepository.InsertAsync(...); +} +``` + +**Test Pattern:** +```csharp +public class ApplicationAppService_Tests : GrantManagerApplicationTestBase +{ + private readonly IApplicationAppService _appService; + private readonly IRepository _repository; + + public ApplicationAppService_Tests() + { + _appService = GetRequiredService(); + _repository = GetRequiredService>(); + } + + [Fact] + public async Task Should_Create_Application() + { + // Arrange + var input = new CreateApplicationDto + { + Title = "Test Application", + ProgramId = TestData.ProgramId + }; + + // Act + var result = await _appService.CreateAsync(input); + + // Assert + result.ShouldNotBeNull(); + result.Title.ShouldBe("Test Application"); + + var dbApp = await _repository.FindAsync(result.Id); + dbApp.ShouldNotBeNull(); + } +} +``` + +### Entity Framework Core Patterns + +#### Entity Configuration +```csharp +// ✅ CORRECT: Fluent API in extension method +public static class GrantTenantDbContextModelCreatingExtensions +{ + public static void ConfigureGrantTenant(this ModelBuilder builder) + { + builder.Entity(b => + { + b.ToTable("GrantApplications"); + + b.Property(x => x.Title) + .IsRequired() + .HasMaxLength(ApplicationConsts.MaxTitleLength); + + b.HasIndex(x => x.ProgramId); + b.HasIndex(x => x.Status); + + b.ConfigureByConvention(); // ✅ Always call this + }); + } +} +``` + +### Multi-Tenancy Patterns + +**Tenant-Scoped Entities:** +```csharp +// ✅ Stored in GrantTenantDbContext +public class GrantApplication : FullAuditedAggregateRoot, IMultiTenant +{ + public Guid? TenantId { get; set; } // ✅ Required for tenant isolation +} + +// DbContext configuration +[ConnectionStringName("GrantManager")] +[IgnoreMultiTenancy] // ✅ This DbContext manages tenancy manually +public class GrantTenantDbContext : AbpDbContext +{ + public DbSet Applications { get; set; } = null!; +} +``` + +**Test Multi-Tenancy:** +```csharp +[Fact] +public async Task Should_Isolate_Tenant_Data() +{ + Guid tenant1AppId, tenant2AppId; + + // Create app in tenant 1 + using (CurrentTenant.Change(TestData.Tenant1Id)) + { + var app = await _appService.CreateAsync(new CreateApplicationDto { ... }); + tenant1AppId = app.Id; + } + + // Create app in tenant 2 + using (CurrentTenant.Change(TestData.Tenant2Id)) + { + var app = await _appService.CreateAsync(new CreateApplicationDto { ... }); + tenant2AppId = app.Id; + } + + // Verify isolation + using (CurrentTenant.Change(TestData.Tenant1Id)) + { + var apps = await _appService.GetListAsync(new GetApplicationListDto()); + apps.Items.ShouldContain(x => x.Id == tenant1AppId); + apps.Items.ShouldNotContain(x => x.Id == tenant2AppId); // ✅ Isolated + } +} +``` + +## Testing Best Practices + +### Test Structure (Arrange-Act-Assert) +```csharp +[Fact] +public async Task Should_Update_Application_Title() +{ + // Arrange - Set up test data + var application = await CreateTestApplicationAsync(); + var input = new UpdateApplicationDto { Title = "Updated Title" }; + + // Act - Execute the operation + var result = await _appService.UpdateAsync(application.Id, input); + + // Assert - Verify outcomes + result.Title.ShouldBe("Updated Title"); + + // Verify persistence + var dbApp = await _repository.GetAsync(application.Id); + dbApp.Title.ShouldBe("Updated Title"); +} +``` + +### Shouldly Assertions +```csharp +// ✅ Use Shouldly fluent assertions +result.ShouldNotBeNull(); +result.Id.ShouldBe(expectedId); +result.Title.ShouldBe("Expected"); +list.ShouldContain(x => x.Id == id); +list.ShouldBeEmpty(); +count.ShouldBeGreaterThan(0); + +// Exception testing +await Should.ThrowAsync(async () => +{ + await _appService.CreateAsync(invalidInput); +}); + +// ❌ Don't use Assert.* methods +Assert.NotNull(result); // ❌ Wrong +Assert.Equal("Expected", result.Title); // ❌ Wrong +``` + +### Test Data Management +```csharp +// ✅ Use helper methods for test data creation +private async Task CreateTestApplicationAsync(string title = "Test") +{ + var application = new GrantApplication(Guid.NewGuid(), title); + return await _repository.InsertAsync(application); +} + +// ✅ Use test data constants +public static class GrantManagerTestData +{ + public static Guid Tenant1Id = Guid.Parse("..."); + public static Guid ProgramId = Guid.Parse("..."); +} +``` + +## Code Quality Checklist + +Before completing ANY task, verify: + +- [ ] **Tests written FIRST** - Red-green-refactor cycle followed +- [ ] **All tests pass** - No failing tests allowed +- [ ] **Virtual methods** - All public methods are `virtual` +- [ ] **DTOs in application layer** - No entities exposed from app services +- [ ] **Multi-tenancy** - `IMultiTenant` implemented where needed +- [ ] **Authorization** - `[Authorize]` attributes applied +- [ ] **Nullable types** - Correct use of `?` for optional properties +- [ ] **Async/await** - All I/O operations are async +- [ ] **ABP conventions** - Base classes, naming, patterns followed +- [ ] **Error handling** - `BusinessException` for domain errors +- [ ] **Validation** - Input validation via data annotations or FluentValidation +- [ ] **Event-driven** - Domain/distributed events used appropriately + +## Implementation Workflow + +### Step-by-Step Process + +**For each task in the implementation plan:** + +1. **Read task requirements carefully** + - Understand what needs to be built + - Identify which ABP layer this belongs to + - Check if multi-tenancy applies + +2. **Write test first (RED)** + - Create test class if it doesn't exist + - Write a failing test for the behavior + - Run test to confirm it fails (expected) + +3. **Implement minimal code (GREEN)** + - Write simplest code to make test pass + - Follow ABP patterns strictly + - Use virtual methods, DTOs, proper base classes + +4. **Run test to verify (GREEN)** + - Use #tool:runTests to execute + - Fix any issues until test passes + +5. **Refactor if needed (REFACTOR)** + - Clean up code while keeping tests green + - Improve names, extract methods, reduce duplication + - Re-run tests after refactoring + +6. **Run full test suite** + - Ensure no regressions in other tests + - Fix any broken tests + +7. **Move to next task** + - Mark current task complete in plan + - Repeat process for next task + +### Progress Tracking + +Track implementation progress systematically: +- Mark task as "in-progress" when starting +- Mark as "completed" when all tests pass +- Update status regularly for visibility +- Provide clear progress updates to the user + +### When to Pause + +Pause and ask for guidance if: +- Requirements are unclear or contradictory +- ABP pattern to use is ambiguous +- Major architectural decision needed +- Tests reveal unexpected behavior +- Multi-tenancy implications are unclear + +## Success Criteria + +A task is complete when: +- ✅ Tests written FIRST and pass +- ✅ Implementation follows ABP patterns +- ✅ Code is clean and maintainable +- ✅ No test regressions +- ✅ Multi-tenancy verified (if applicable) +- ✅ Authorization checked (if applicable) +- ✅ All quality checklist items satisfied + +## Remember + +- **Red-Green-Refactor** - Tests first, always +- **ABP Conventions** - Virtual methods, DTOs, base classes, naming +- **Multi-Tenancy** - Respect DbContext boundaries, test isolation +- **Quality over Speed** - Working, tested code beats fast, broken code +- **Incremental Progress** - Small steps with passing tests +- **Communication** - Ask when uncertain, don't guess + +You are a craftsperson building high-quality, tested software. Take pride in your work and follow the discipline of TDD. The tests you write today prevent bugs tomorrow. diff --git a/applications/Unity.GrantManager/.github/copilot-instructions.md b/applications/Unity.GrantManager/.github/copilot-instructions.md new file mode 100644 index 0000000000..e66b151366 --- /dev/null +++ b/applications/Unity.GrantManager/.github/copilot-instructions.md @@ -0,0 +1,539 @@ +# Unity Grant Manager - GitHub Copilot Guidelines + +This project follows **ABP Framework 9.1.3** architecture and conventions. Always refer to the project documentation and ABP best practices when generating code or providing assistance. + +## Essential Project Context + +* **[Product Vision and Goals](../PRODUCT.md)**: Understand the grant management platform's high-level vision, key features (grant programs, applicant portal, assessments, payments), and business objectives. +* **[System Architecture and Design Principles](../ARCHITECTURE.md)**: Comprehensive architecture overview including ABP Framework patterns, DDD layered structure, multi-tenancy design, module dependencies (with Mermaid diagrams), technology stack (PostgreSQL, EF Core, Redis, RabbitMQ, Keycloak), and deployment architecture. +* **[Contributing Guidelines](../CONTRIBUTING.md)**: Detailed coding conventions, ABP patterns, testing practices, multi-tenancy guidelines, and common pitfalls to avoid. + +**Important**: Suggest updates to these documents if you find incomplete or conflicting information during your work. + +## ABP Framework-Specific Patterns + +### Application Architecture (DDD Layers) + +**Follow ABP's layered architecture with strict dependencies:** + +- **Domain.Shared**: Constants, enums, shared types (no dependencies) +- **Domain**: Entities, domain services (`*Manager`), repository interfaces (depends on Domain.Shared) +- **Application.Contracts**: Service interfaces, DTOs (depends on Domain.Shared only) +- **Application**: Service implementations (depends on Domain + Application.Contracts) +- **EntityFrameworkCore**: DbContext, repositories (depends on Domain only) +- **HttpApi**: API controllers (depends on Application.Contracts) +- **Web**: Razor Pages, UI components (depends on Application + HttpApi) + +### Core Conventions + +**Base Classes:** +- Application Services: Inherit from `ApplicationService`, implement interface from Application.Contracts +- Domain Services: Inherit from `DomainService`, use `Manager` suffix (e.g., `ApplicationManager`) +- Entities: Inherit from `FullAuditedAggregateRoot` or `AuditedAggregateRoot` +- API Controllers: Inherit from `AbpController` +- Repositories: Use `IRepository` or define custom interface when needed + +**Naming:** +- Domain Services: `*Manager` suffix (e.g., `AssessmentManager`, `PaymentManager`) +- Application Services: `*AppService` suffix (e.g., `ApplicationAppService`) +- DTOs: Use descriptive suffixes (`CreateApplicationDto`, `UpdateApplicationDto`, `ApplicationDto`) +- Event Transfer Objects: `*Eto` suffix for distributed events + +**Methods:** +- All public methods MUST be `virtual` to allow overriding and extensibility +- Async methods MUST have `Async` suffix +- Use `protected virtual` instead of `private` for helper methods + +**Authorization:** +- Apply `[Authorize(PermissionName)]` attributes on application service methods +- Define permissions in `*Permissions` static class in Domain.Shared project + +**DTOs vs Entities:** +- Application services MUST accept and return DTOs only, never entities +- Use `ObjectMapper` (AutoMapper) to map between entities and DTOs +- Define mapping profiles in `*AutoMapperProfile` class in Application project + +### Multi-Tenancy Patterns + +**This application uses database-per-tenant isolation:** + +- **GrantManagerDbContext**: Host database for global data (tenants, users, settings) +- **GrantTenantDbContext**: Tenant-specific data (applications, assessments, payments) - marked with `[IgnoreMultiTenancy]` +- Tenant entities MUST implement `IMultiTenant` interface +- NEVER manually filter by `TenantId` - ABP handles this automatically +- Store tenant data in `GrantTenantDbContext`, host data in `GrantManagerDbContext` +- Create separate migration streams for host and tenant databases + +### Repository Usage + +**Use generic repository by default:** +```csharp +private readonly IRepository _applicationRepository; +``` + +**Define custom repository interface ONLY when you need:** +- Complex queries not easily expressed with LINQ +- Specialized database operations +- Raw SQL queries or stored procedures + +**Custom repositories:** +- Interface goes in Domain project +- Implementation goes in EntityFrameworkCore project +- Inherit from `EfCoreRepository` + +### Domain Events + +**Local Events (same transaction, same database):** +```csharp +AddLocalEvent(new ApplicationSubmittedEvent { ... }); +``` + +**Distributed Events (RabbitMQ, cross-module communication):** +```csharp +AddDistributedEvent(new ApplicationApprovedEto { ... }); +``` + +Use distributed events for communication between: +- Unity.GrantManager → Unity.Notifications (email notifications) +- Unity.GrantManager → Unity.Payments (payment processing) +- Unity.GrantManager → Unity.Reporting (analytics updates) + +### Testing Conventions + +**Framework:** xUnit + Shouldly + +**Test Organization:** +- `*_Tests` suffix for test classes +- `Should_[Expected]_[Scenario]` for test method names +- Use `[Fact]` for single tests, `[Theory]` with `[InlineData]` for parameterized tests + +**Assertions (use Shouldly):** +```csharp +result.ShouldNotBeNull(); +result.Title.ShouldBe("Expected Value"); +list.ShouldContain(x => x.Id == expectedId); +await Should.ThrowAsync(() => ...); +``` + +**Base Classes:** +- Application tests: `GrantManagerApplicationTestBase` +- Domain tests: `GrantManagerDomainTestBase` +- Web tests: `GrantManagerWebTestBase` + +### Database Migrations + +**Two separate migration streams:** + +**Host migrations:** +```bash +cd src/Unity.GrantManager.EntityFrameworkCore +dotnet ef migrations add MigrationName --context GrantManagerDbContext +``` + +**Tenant migrations:** +```bash +cd src/Unity.GrantManager.EntityFrameworkCore +dotnet ef migrations add MigrationName --context GrantTenantDbContext +``` + +**Apply migrations:** Run `Unity.GrantManager.DbMigrator` project (Ctrl+F5) + +### Module Integration Patterns + +**Direct service injection (synchronous, same process):** +```csharp +private readonly IFlexFieldService _flexFieldService; // Unity.Flex module +``` + +**Distributed events (asynchronous, potentially different database):** +```csharp +// Publish +AddDistributedEvent(new ApplicationApprovedEto { ... }); + +// Handle in Unity.Payments module +public class ApplicationApprovedHandler : IDistributedEventHandler +``` + +**Available Unity modules:** +- Unity.Flex: Dynamic forms and custom fields +- Unity.Notifications: CHES email service integration +- Unity.Payments: CAS payment system integration +- Unity.Reporting: Report generation and analytics +- Unity.Identity.Web: Custom identity UI +- Unity.TenantManagement: Multi-tenant administration +- Unity.Theme.UX2: BC Government UI theme +- Unity.SharedKernel: Cross-cutting utilities + +## .NET 9.0 & C# 12 Conventions + +**Language Features:** +- Nullable reference types are ENABLED project-wide +- Always declare nullability explicitly: `string?` vs `string` +- Use `null!` only when DI guarantees non-null (e.g., `public DbSet Entities { get; set; } = null!;`) +- Target framework: `net9.0` +- Use latest C# features (primary constructors, collection expressions, etc.) + +**Code Style:** +- Async methods: Always use `async/await`, suffix with `Async` +- Access modifiers: Always specify explicitly (`public`, `private`, `protected`) +- Indentation: 4 spaces, no tabs +- Braces: Always use, even for single-line statements + +## Business Domain Understanding + +**Core Entities:** +- Grant Programs: Configured by staff, define intake periods and requirements +- Applications: Submitted by applicants through portal +- Assessments: Review workflows with scoring by assessors +- Payments: Payment requests processed through CAS via Unity.Payments + +**User Roles:** +- Applicants: Submit and track grant applications +- Program Officers: Configure programs, review applications +- Assessors: Score and evaluate applications +- Finance Staff: Process payments and manage budgets + +**Integration Points:** +- CHES: Government email service for notifications +- CAS: Common Accounting System for payments +- Keycloak: Identity provider for authentication +- AWS S3: Document/blob storage + +## ABP Framework Resources + +**When you encounter ABP-specific questions, reference:** +- [ABP Best Practices](https://docs.abp.io/en/abp/latest/Best-Practices) +- [Module Architecture Guide](https://docs.abp.io/en/abp/latest/Best-Practices/Module-Architecture) +- [Application Services](https://docs.abp.io/en/abp/latest/Best-Practices/Application-Services) +- [Domain Services](https://docs.abp.io/en/abp/latest/Best-Practices/Domain-Services) +- [Entities](https://docs.abp.io/en/abp/latest/Best-Practices/Entities) +- [Repositories](https://docs.abp.io/en/abp/latest/Best-Practices/Repositories) +- [Multi-Tenancy](https://docs.abp.io/en/abp/latest/Multi-Tenancy) +- [ABP GitHub Repository](https://github.com/abpframework/abp) + +## Common Mistakes to Avoid + +❌ **Don't expose entities from application services** - Always return DTOs +❌ **Don't put business logic in application services** - Use domain services (`*Manager`) +❌ **Don't use non-virtual methods** - All public methods must be virtual +❌ **Don't manually filter by TenantId** - ABP does this automatically +❌ **Don't create custom repositories unnecessarily** - Use `IRepository` first +❌ **Don't mix host and tenant data in same DbContext** - Separate contexts for isolation +❌ **Don't forget [Authorize] attributes** - Always check permissions +❌ **Don't ignore nullable warnings** - Fix them properly +❌ **Don't use manual AJAX** - Use ABP's dynamic JavaScript proxies +❌ **Don't create global JavaScript variables** - Wrap in IIFE pattern +❌ **Don't hardcode strings in JavaScript** - Use `abp.localization` +❌ **Don't bypass ABP modal manager** - Use `abp.ModalManager` for modals +❌ **Don't forget DataTable reload** - Call `dataTable.ajax.reload()` after CRUD + +## Front-End Development Patterns + +### Client-Side Package Management + +**Adding NPM packages:** +1. Add to `package.json` (prefer `@abp/*` packages for consistency) +2. Run `npm install` +3. Configure `abp.resourcemapping.js` to map resources from `node_modules` to `wwwroot/libs` +4. Run `abp install-libs` to copy resources +5. Add to bundle contributor in `Unity.Theme.UX2` module + +**Example resource mapping:** +```javascript +// abp.resourcemapping.js +module.exports = { + aliases: { + '@node_modules': './node_modules', + '@libs': './wwwroot/libs', + }, + mappings: { + '@node_modules/datatables.net-bs5/': '@libs/datatables.net-bs5/', + '@node_modules/echarts/dist/echarts.min.js': '@libs/echarts/', + }, +}; +``` + +### JavaScript Structure and Conventions + +**Standard page script pattern:** +```javascript +(function ($) { + var l = abp.localization.getResource('GrantManager'); + + // DataTable initialization + var dataTable = $('#MyTable').DataTable( + abp.libs.datatables.normalizeConfiguration({ + // Configuration + }) + ); + + // Modal initialization + var createModal = new abp.ModalManager({ + viewUrl: abp.appPath + 'GrantManager/MyFeature/CreateModal', + modalClass: 'myFeatureCreate' + }); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + // Event handlers + $('#NewRecordButton').click(function (e) { + e.preventDefault(); + createModal.open(); + }); + +})(jQuery); +``` + +**Always:** +- Wrap in IIFE: `(function ($) { ... })(jQuery);` +- Use `var l = abp.localization.getResource('GrantManager');` for localization +- Use `abp.notify` for success/error messages +- Use `abp.message.confirm()` for confirmation dialogs +- Use `abp.auth.isGranted()` for permission checks + +### ABP Dynamic JavaScript API Client Proxies + +**How it works:** +- Application services are automatically exposed as JavaScript functions +- Namespace follows pattern: `[moduleName].[namespace].[serviceName].[methodName]()` +- Functions return jQuery Deferred objects (use `.then()`, `.catch()`) +- Auto-generated from `/Abp/ServiceProxyScript` endpoint + +**Example usage:** +```javascript +// GET list +acme.grantManager.applications.application.getList({ + maxResultCount: 10, + filter: 'search' +}).then(function(result) { + console.log(result.items); +}); + +// POST create +acme.grantManager.applications.application.create({ + title: 'New Application' +}).then(function(result) { + abp.notify.success(l('SavedSuccessfully')); +}); + +// DELETE +acme.grantManager.applications.application + .delete(id) + .then(function() { + abp.notify.success(l('SuccessfullyDeleted')); + dataTable.ajax.reload(); + }); +``` + +**Benefits:** +- No manual AJAX configuration +- Type-safe (parameters match C# signatures) +- Automatic error handling +- Consistent with ABP conventions + +### DataTables.net Integration + +**Unity Grant Manager uses DataTables.net 1.x** (not 2.x due to ABP compatibility). + +**Standard DataTable pattern:** +```javascript +var dataTable = $('#MyTable').DataTable(abp.libs.datatables.normalizeConfiguration({ + processing: true, + serverSide: true, + paging: true, + ajax: abp.libs.datatables.createAjax( + acme.grantManager.myService.getList, + function () { + // Return additional filter parameters + return { + filter: $('#SearchInput').val(), + status: $('#StatusFilter').val() + }; + } + ), + columnDefs: [ + { + title: l('Actions'), + rowAction: { + items: [ + { + text: l('Edit'), + visible: abp.auth.isGranted('GrantManager.Edit'), + action: function (data) { + editModal.open({ id: data.record.id }); + } + }, + { + text: l('Delete'), + confirmMessage: function (data) { + return l('DeleteConfirmationMessage', data.record.name); + }, + action: function (data) { + acme.grantManager.myService + .delete(data.record.id) + .then(function () { + abp.notify.success(l('SuccessfullyDeleted')); + dataTable.ajax.reload(); + }); + } + } + ] + } + }, + { + title: l('Name'), + data: 'name' + }, + { + title: l('CreationTime'), + data: 'creationTime', + dataFormat: 'datetime' // ABP auto-formatting + } + ] +})); +``` + +**Key patterns:** +- Use `abp.libs.datatables.normalizeConfiguration()` wrapper +- Use `abp.libs.datatables.createAjax()` for server-side pagination +- Use `rowAction` for action buttons with permission checks +- Use `dataFormat` property for automatic date/boolean formatting +- Call `dataTable.ajax.reload()` after CRUD operations + +### ABP Modal Manager + +**Creating modals:** +```javascript +var createModal = new abp.ModalManager({ + viewUrl: abp.appPath + 'GrantManager/Applications/CreateModal', + scriptUrl: abp.appPath + 'Pages/GrantManager/Applications/CreateModal.js', + modalClass: 'applicationCreate' +}); + +createModal.onResult(function () { + abp.notify.success(l('SavedSuccessfully')); + dataTable.ajax.reload(); +}); + +$('#NewButton').click(function (e) { + e.preventDefault(); + createModal.open(); +}); +``` + +**Modal script class (CreateModal.js):** +```javascript +abp.modals.applicationCreate = function () { + var _$form = null; + + this.init = function (modalManager, args) { + _$form = modalManager.getForm(); + + // Custom initialization logic + _$form.find('#ProgramId').change(function () { + // Handle program change + }); + }; +}; +``` + +**Closing modal after save:** +```csharp +// In Razor Page code-behind +public async Task OnPostAsync() +{ + await _appService.CreateAsync(Model); + return NoContent(); // Return NoContent to close modal and trigger onResult +} +``` + +### ABP JavaScript Utilities + +**Localization:** +```javascript +var l = abp.localization.getResource('GrantManager'); +var message = l('WelcomeMessage'); +var formatted = l('GreetingMessage', userName); +``` + +**Notifications:** +```javascript +abp.notify.success('Success message'); +abp.notify.error('Error message'); +abp.notify.warn('Warning message'); +abp.notify.info('Info message'); +``` + +**Confirmation dialogs:** +```javascript +abp.message.confirm( + 'Are you sure?', + 'Confirm Action' +).then(function (confirmed) { + if (confirmed) { + // Perform action + } +}); +``` + +**Authorization:** +```javascript +if (abp.auth.isGranted('GrantManager.Applications.Edit')) { + // Show edit button +} +``` + +**Busy indicator:** +```javascript +abp.ui.setBusy('#MyForm'); +// ... operation ... +abp.ui.clearBusy('#MyForm'); +``` + +### DOM Auto-Initialization + +ABP automatically initializes these components via DOM event handlers: + +- **Tooltips:** `data-bs-toggle="tooltip"` +- **Popovers:** `data-bs-toggle="popover"` +- **Datepickers:** `` or `` +- **AJAX Forms:** `
` +- **Autocomplete Selects:** ` + +``` + +## Code Generation Guidelines + +When generating code: + +1. **Check layer dependencies** - Ensure proper dependency direction (Domain ← Application ← Web) +2. **Use ABP base classes** - Don't create from scratch what ABP provides +3. **Follow naming conventions** - Especially `*Manager` for domain services, `*AppService` for application services +4. **Make methods virtual** - Critical for extensibility and ABP conventions +5. **Use proper DTOs** - Never expose entities in API/application layer +6. **Apply multi-tenancy** - Implement `IMultiTenant` for tenant data +7. **Add authorization** - Include `[Authorize]` attributes with permission names +8. **Write tests** - Generate corresponding test class with xUnit/Shouldly +9. **Consider events** - Use distributed events for cross-module communication +10. **Document complex logic** - Add XML comments for public APIs + +## When in Doubt + +1. Check existing code patterns in the same layer/project +2. Refer to [ARCHITECTURE.md](../ARCHITECTURE.md) for architectural decisions +3. Consult [CONTRIBUTING.md](../CONTRIBUTING.md) for detailed patterns and examples +4. Review ABP Framework best practices documentation +5. Ask for clarification rather than making assumptions that violate ABP conventions diff --git a/applications/Unity.GrantManager/.github/plan-template.md b/applications/Unity.GrantManager/.github/plan-template.md new file mode 100644 index 0000000000..751ee69354 --- /dev/null +++ b/applications/Unity.GrantManager/.github/plan-template.md @@ -0,0 +1,354 @@ +--- +title: [Short descriptive title of the feature] +version: 1.0 +date_created: [YYYY-MM-DD] +last_updated: [YYYY-MM-DD] +--- + +# Implementation Plan: [Feature Name] + +## Overview + +[Brief description of the feature requirements and business goals. Include the problem this feature solves and the value it provides to users.] + +## Requirements Summary + +### Functional Requirements +- [Requirement 1] +- [Requirement 2] +- [Requirement 3] + +### Non-Functional Requirements +- [Performance, security, scalability considerations] +- [Compliance or regulatory requirements] +- [Integration requirements with external systems] + +### User Stories (if applicable) +- As a [user role], I want to [action] so that [benefit] +- As a [user role], I want to [action] so that [benefit] + +## Architecture and Design + +### Affected ABP Layers +- [ ] **Domain Layer**: New entities, domain services, repository interfaces, domain events +- [ ] **Application Layer**: Application services, DTOs, AutoMapper profiles +- [ ] **EntityFrameworkCore Layer**: DbContext changes, repository implementations, entity configurations +- [ ] **HttpApi Layer**: API controllers, endpoints +- [ ] **Web Layer**: Razor Pages, view components, JavaScript, CSS + +### Impacted Modules +- [ ] **Unity.GrantManager** (main application) +- [ ] **Unity.Flex** (dynamic forms) +- [ ] **Unity.Notifications** (email notifications) +- [ ] **Unity.Payments** (payment processing) +- [ ] **Unity.Reporting** (analytics/reports) +- [ ] **Unity.Identity.Web** (user management) +- [ ] **Unity.TenantManagement** (tenant admin) +- [ ] **Unity.SharedKernel** (utilities) + +### Multi-Tenancy Considerations +- [ ] **Tenant-Scoped Data**: Will this feature store tenant-specific data? + - If yes, entities must implement `IMultiTenant` and use `GrantTenantDbContext` +- [ ] **Host-Level Data**: Will this feature require global/host data? + - If yes, use `GrantManagerDbContext` for cross-tenant entities +- [ ] **Tenant Isolation**: How will data isolation be enforced? +- [ ] **Cross-Tenant Operations**: Are there any scenarios where cross-tenant access is needed? + +### Integration Points + +#### Internal Modules +- [Describe how this feature integrates with Unity.Flex, Unity.Notifications, Unity.Payments, etc.] +- [Specify if domain events or distributed events are used for communication] + +#### External Systems +- [ ] **CHES (Email Service)**: [Describe email notification requirements] +- [ ] **CAS (Payment System)**: [Describe payment integration needs] +- [ ] **Keycloak (Identity)**: [Describe authentication/authorization changes] +- [ ] **AWS S3 (Storage)**: [Describe document/file storage needs] + +### Data Model Changes + +#### New Entities +- **EntityName** (`GrantTenantDbContext` or `GrantManagerDbContext`) + - Properties: [List key properties] + - Relationships: [Describe foreign keys and navigation properties] + - Aggregate Root: [Yes/No] + +#### Modified Entities +- **EntityName**: [Describe changes - new properties, relationship changes, etc.] + +#### Database Migrations +- [ ] Host migration required (`GrantManagerDbContext`) +- [ ] Tenant migration required (`GrantTenantDbContext`) + +### API Design + +#### New Endpoints +- `GET /api/grant-manager/[resource]` - [Description] +- `POST /api/grant-manager/[resource]` - [Description] +- `PUT /api/grant-manager/[resource]/{id}` - [Description] +- `DELETE /api/grant-manager/[resource]/{id}` - [Description] + +#### Request/Response DTOs +- `Create[Entity]Dto`: [Key properties] +- `Update[Entity]Dto`: [Key properties] +- `[Entity]Dto`: [Key properties] + +### UI/UX Changes + +#### New Pages/Components +- [Page/Component Name]: [Description and purpose] + +#### Modified Pages/Components +- [Page/Component Name]: [Description of changes] + +#### User Flows +1. [Step-by-step user interaction flow] +2. [Include decision points and alternate paths] + +### Security & Permissions + +#### New Permissions +- `GrantManager.[Resource].Create` +- `GrantManager.[Resource].Edit` +- `GrantManager.[Resource].Delete` +- `GrantManager.[Resource].View` + +#### Authorization Rules +- [Describe who can access what, role-based rules, data-level security] + +### Events & Messaging + +#### Domain Events (Local) +- `[Entity][Action]Event`: Triggered when [condition], handled by [handler] + +#### Distributed Events (RabbitMQ) +- `[Entity][Action]Eto`: Published when [condition], consumed by [module/service] + +### Performance Considerations +- [Indexing strategy for new database fields] +- [Caching strategy (Redis) if applicable] +- [Query optimization approaches] +- [Background job requirements (Quartz.NET)] + +## Tasks + +### Domain Layer Tasks +- [ ] Define `[Entity]` aggregate root in Domain project + - [ ] Implement entity with proper encapsulation (private setters, business methods) + - [ ] Add validation logic and business rules + - [ ] Implement `IMultiTenant` if tenant-scoped + - [ ] Add domain events if needed +- [ ] Create `[Entity]Manager` domain service (if complex business logic required) + - [ ] Implement business logic methods + - [ ] Add validation and business rule enforcement + - [ ] Use `BusinessException` for domain errors +- [ ] Define `I[Entity]Repository` interface (if custom queries needed) + - [ ] Specify custom query methods beyond standard CRUD +- [ ] Add constants to Domain.Shared project + - [ ] String length constants + - [ ] Enums for entity states/types + +### Application Layer Tasks +- [ ] Define DTOs in Application.Contracts project + - [ ] `Create[Entity]Dto` with validation attributes + - [ ] `Update[Entity]Dto` with validation attributes + - [ ] `[Entity]Dto` (output DTO) + - [ ] List query DTOs (e.g., `Get[Entity]ListDto`) +- [ ] Define `I[Entity]AppService` interface in Application.Contracts + - [ ] Standard CRUD methods: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync`, `DeleteAsync` + - [ ] Custom methods for specific use cases +- [ ] Implement `[Entity]AppService` in Application project + - [ ] Inject required repositories and domain services + - [ ] Implement interface methods (all virtual) + - [ ] Apply `[Authorize]` attributes for permissions + - [ ] Use `ObjectMapper` for entity/DTO conversion + - [ ] Handle pagination and filtering in `GetListAsync` +- [ ] Configure AutoMapper profile in Application project + - [ ] Map entity to DTOs + - [ ] Handle nested objects and value objects + +### EntityFrameworkCore Layer Tasks +- [ ] Configure entity in `GrantManagerDbContextModelCreatingExtensions` or `GrantTenantDbContextModelCreatingExtensions` + - [ ] Define table name + - [ ] Configure properties (required, max length) + - [ ] Configure indexes (foreign keys, frequently queried fields) + - [ ] Configure relationships and foreign keys + - [ ] Call `ConfigureByConvention()` for ABP features +- [ ] Implement custom repository (if `I[Entity]Repository` was defined) + - [ ] Inherit from `EfCoreRepository` + - [ ] Implement custom query methods +- [ ] Create database migration + - [ ] Run `dotnet ef migrations add [MigrationName] --context [ContextName]` + - [ ] Review generated migration code + - [ ] Test migration on development database + +### HttpApi Layer Tasks +- [ ] Create `[Entity]Controller` in HttpApi project + - [ ] Inherit from `AbpController` + - [ ] Inject `I[Entity]AppService` + - [ ] Define route (`[Route("api/grant-manager/[resource]")]`) + - [ ] Implement API endpoints (GET, POST, PUT, DELETE) + - [ ] Return appropriate HTTP status codes + +### Web Layer Tasks +- [ ] Create Razor Pages in Web project + - [ ] Index page for listing entities + - [ ] Create/Edit modal or page + - [ ] Details page (if needed) +- [ ] Implement JavaScript functionality + - [ ] AJAX calls to API endpoints + - [ ] Client-side validation + - [ ] DataTables integration for lists (if applicable) +- [ ] Add navigation menu items + - [ ] Update main menu configuration + - [ ] Apply permission checks for visibility +- [ ] Localization + - [ ] Add localization keys to resource files + - [ ] Translate to supported languages + +### Testing Tasks +- [ ] Application service tests (xUnit + Shouldly) + - [ ] Test CRUD operations + - [ ] Test business logic validations + - [ ] Test authorization (permission checks) +- [ ] Domain service tests + - [ ] Test complex business rules + - [ ] Test domain events +- [ ] Integration tests + - [ ] Test API endpoints + - [ ] Test multi-tenancy isolation (if applicable) + +### Front-End Tasks +- [ ] Client-side package management (if new NPM packages needed) + - [ ] Add dependencies to `package.json` + - [ ] Configure `abp.resourcemapping.js` for resource mapping + - [ ] Run `abp install-libs` to copy resources + - [ ] Add to bundle contributor in `Unity.Theme.UX2` +- [ ] Page JavaScript implementation + - [ ] Create page script in `/Pages/[Feature]/[PageName].js` + - [ ] Wrap in IIFE pattern: `(function ($) { ... })(jQuery);` + - [ ] Initialize localization: `var l = abp.localization.getResource('GrantManager');` + - [ ] Configure DataTable with `abp.libs.datatables.normalizeConfiguration()` + - [ ] Use ABP dynamic proxies for API calls (e.g., `acme.grantManager.myService.getList()`) + - [ ] Implement modal managers for Create/Edit dialogs + - [ ] Add event handlers for filters and buttons + - [ ] Implement permission checks using `abp.auth.isGranted()` +- [ ] DataTable configuration + - [ ] Define columns with localized titles + - [ ] Configure row actions (Edit, Delete, custom actions) + - [ ] Add permission checks to action visibility + - [ ] Configure data formatting (datetime, boolean, enums) + - [ ] Implement server-side pagination with `abp.libs.datatables.createAjax()` + - [ ] Add custom filters (search, status, date ranges) +- [ ] Modal dialogs + - [ ] Create modal Razor Pages (CreateModal.cshtml, EditModal.cshtml) + - [ ] Implement modal script classes in `abp.modals.*` namespace + - [ ] Configure modal manager with `viewUrl` and `modalClass` + - [ ] Implement `onResult()` callback to reload DataTable + - [ ] Return `NoContent()` from page handler to close modal +- [ ] Form validation and AJAX submission + - [ ] Use `data-ajaxForm="true"` for AJAX forms + - [ ] Implement client-side validation with jQuery Validation + - [ ] Use `abp.notify` for success/error messages + - [ ] Handle errors with `abp.message` or `abp.notify.error` +- [ ] Localization + - [ ] Add localization keys to `Localization/GrantManager/en.json` + - [ ] Use `l('LocalizationKey')` in JavaScript for all user-facing text + - [ ] Test with multiple languages if multi-language support enabled +- [ ] UI/UX enhancements + - [ ] Add tooltips with `data-bs-toggle="tooltip"` + - [ ] Implement autocomplete selects with `class="auto-complete-select"` + - [ ] Add busy indicators for long operations with `abp.ui.setBusy()` + - [ ] Implement confirmation dialogs with `abp.message.confirm()` + +### Documentation Tasks +- [ ] Update API documentation (Swagger annotations) +- [ ] Add XML comments to public APIs +- [ ] Update user documentation (if applicable) +- [ ] Document any configuration changes + +## Implementation Sequence + +**Recommended order to implement tasks:** + +1. **Domain Layer** - Define entities, domain services, and core business logic +2. **Database Migration** - Create migration to support domain model +3. **Application Layer** - Implement use cases via application services and DTOs +4. **Testing** - Write and run tests for domain and application layers +5. **HttpApi Layer** - Expose API endpoints +6. **Web Layer** - Build UI pages and components +7. **Integration Testing** - Test end-to-end workflows +8. **Documentation** - Update all relevant documentation + +## Open Questions + +1. [Question about requirements, clarification needed on business rules, etc.] +2. [Question about technical approach, integration details, etc.] +3. [Question about edge cases, error handling, performance, etc.] + +## Assumptions + +- [Assumption 1 about system behavior, data availability, etc.] +- [Assumption 2 about user permissions, access patterns, etc.] + +## Dependencies + +### Blocking Dependencies +- [Prerequisite feature or task that must be completed first] + +### Related Features +- [Features that should be coordinated or implemented together] + +## Risks & Mitigation + +| Risk | Impact | Probability | Mitigation Strategy | +|------|--------|-------------|---------------------| +| [Risk description] | High/Med/Low | High/Med/Low | [How to mitigate or handle] | + +## Testing Strategy + +### Unit Tests +- [Scope of unit testing - domain services, application services] + +### Integration Tests +- [Scope of integration testing - database, external APIs] + +### Manual Testing Scenarios +1. [Scenario 1: User action → Expected outcome] +2. [Scenario 2: User action → Expected outcome] + +### Performance Testing +- [ ] Load testing (if applicable) +- [ ] Query performance validation +- [ ] Cache effectiveness verification + +## Rollout Plan + +### Feature Flags (if applicable) +- [ ] Enable/disable feature via ABP Feature Management +- [ ] Gradual rollout to tenants + +### Data Migration +- [ ] Existing data migration requirements +- [ ] Backward compatibility considerations + +### Deployment Steps +1. [Step 1] +2. [Step 2] +3. [Step 3] + +## Success Criteria + +- [ ] All functional requirements implemented and tested +- [ ] All tests passing (unit, integration, manual) +- [ ] Code review completed and approved +- [ ] Documentation updated +- [ ] Performance benchmarks met +- [ ] Security review completed (if applicable) +- [ ] Deployed to staging environment successfully +- [ ] User acceptance testing completed + +## Notes + +[Any additional notes, references, or context that doesn't fit in other sections] diff --git a/applications/Unity.GrantManager/.github/prompts/implement-tdd.prompt.md b/applications/Unity.GrantManager/.github/prompts/implement-tdd.prompt.md new file mode 100644 index 0000000000..37ea479020 --- /dev/null +++ b/applications/Unity.GrantManager/.github/prompts/implement-tdd.prompt.md @@ -0,0 +1,41 @@ +--- +agent: tdd +description: Implement a feature using test-driven development based on an implementation plan. +--- + +Please implement the feature described in the plan file: #{{planFile}} + +Follow strict test-driven development (TDD) methodology: + +1. **Red-Green-Refactor Cycle**: For each task, write the test first (failing), implement minimal code to make it pass, then refactor while keeping tests green. + +2. **Implementation Order**: Follow the sequence outlined in the plan: + - Domain Layer (entities, domain services) - test first + - Database migrations + - Application Layer (app services, DTOs) - test first + - API Layer (controllers) + - Integration tests + - Web Layer (UI) + +3. **ABP Framework Conventions**: Strictly follow patterns documented in CONTRIBUTING.md: + - Inherit from proper base classes (`ApplicationService`, `DomainService`, `FullAuditedAggregateRoot`) + - All public methods must be `virtual` + - Application services return DTOs only, never entities + - Use `Manager` suffix for domain services + - Implement `IMultiTenant` for tenant-scoped entities + - Apply `[Authorize]` attributes for permissions + +4. **Testing Standards**: Use xUnit + Shouldly: + - `Should_[Expected]_[Scenario]` naming + - Arrange-Act-Assert pattern + - Run tests after each implementation step + - Ensure no regressions in full test suite + +5. **Progress Updates**: Provide clear status on which tasks are complete and what's next. + +6. **Quality Gates**: Don't move to the next task until: + - All tests for current task pass + - Code follows ABP conventions + - No test regressions + +Work through the plan systematically, one task at a time, ensuring quality through testing at every step. diff --git a/applications/Unity.GrantManager/.github/prompts/plan-from-issue.prompt.md b/applications/Unity.GrantManager/.github/prompts/plan-from-issue.prompt.md new file mode 100644 index 0000000000..0940749ffd --- /dev/null +++ b/applications/Unity.GrantManager/.github/prompts/plan-from-issue.prompt.md @@ -0,0 +1,14 @@ +--- +agent: plan +description: Generate an implementation plan from a GitHub issue. +--- + +Please create a detailed implementation plan for the feature/fix described in GitHub issue #{{issueNumber}}. + +Workflow: +1. Use available GitHub tools to fetch the issue description and comments +2. Analyze the requirements from the issue content +3. If requirements are unclear, ask 2-3 clarifying questions before proceeding +4. Follow your standard planning workflow to create a comprehensive implementation plan based on the [plan template](../plan-template.md) + +The plan should follow ABP Framework conventions and Unity Grant Manager architectural patterns as documented in ARCHITECTURE.md and CONTRIBUTING.md. diff --git a/applications/Unity.GrantManager/.github/prompts/plan.prompt.md b/applications/Unity.GrantManager/.github/prompts/plan.prompt.md new file mode 100644 index 0000000000..209377a664 --- /dev/null +++ b/applications/Unity.GrantManager/.github/prompts/plan.prompt.md @@ -0,0 +1,15 @@ +--- +agent: plan +description: Create a detailed implementation plan with clarifying questions. +--- + +Briefly analyze my feature request, then ask me 3 focused questions to clarify the requirements before creating the implementation plan. + +Focus your questions on: +- Business rules and validation logic +- Data relationships and entity structures +- User flows and UI requirements +- Integration points with Unity modules or external systems (CHES, CAS, Keycloak) +- Multi-tenancy considerations (tenant-scoped vs host-scoped data) + +Once I answer your questions, proceed with your standard planning workflow to create a comprehensive implementation plan. diff --git a/applications/Unity.GrantManager/ARCHITECTURE.md b/applications/Unity.GrantManager/ARCHITECTURE.md new file mode 100644 index 0000000000..26c89ebd3f --- /dev/null +++ b/applications/Unity.GrantManager/ARCHITECTURE.md @@ -0,0 +1,619 @@ +# Unity Grant Manager - System Architecture + +## Overview + +Unity Grant Manager is built on **ABP Framework 9.1.3**, following Domain-Driven Design (DDD) principles and implementing a modular monolith architecture. The application leverages ABP's opinionated architecture to build enterprise-grade grant management software with clean separation of concerns, multi-tenancy support, and extensible module design. + +## Technology Stack + +### Core Framework & Runtime +- **.NET 9.0**: Latest .NET platform with C# 12.0 and nullable reference types enabled +- **ABP Framework 9.1.3**: Application framework providing DDD infrastructure, modularity, and multi-tenancy +- **ASP.NET Core MVC**: Web application framework with Razor Pages for server-side rendering + +### Data & Persistence +- **PostgreSQL**: Primary relational database management system +- **Entity Framework Core 9.0.5**: ORM for data access with Npgsql provider +- **Redis**: Distributed caching and data protection key storage +- **Common Object Management Service (COMS)**: Blob storage for document management + +### Front-End & UI +- **Unity.Theme.UX2**: Custom theme module for consistent government branding +- **Bootstrap 5**: UI component framework +- **jQuery**: JavaScript utilities and DOM manipulation +- **Bundling & Minification**: ABP bundling system for client-side resource optimization + +### Messaging & Background Jobs +- **RabbitMQ**: Message broker for event bus and distributed event handling +- **Quartz.NET**: Background job scheduling and execution with clustering support + +### Authentication & Authorization +- **Keycloak**: Identity provider for OpenID Connect authentication +- **ABP Identity**: User and role management infrastructure + +### Testing & Quality +- **xUnit**: Test framework for unit and integration tests +- **Shouldly**: Fluent assertion library +- **MiniProfiler**: Performance profiling and diagnostics + +### Logging & Monitoring +- **Serilog**: Structured logging with multiple sink support +- **ABP Audit Logging**: Comprehensive audit trail for all system operations + +## Architectural Patterns + +### Domain-Driven Design (DDD) + +Unity Grant Manager follows DDD tactical patterns as prescribed by ABP Framework: + +- **Entities & Aggregate Roots**: Core business objects with identity and lifecycle +- **Value Objects**: Immutable objects defined by their attributes +- **Domain Services**: Business logic that doesn't naturally fit within entities (suffix: `Manager`) +- **Repositories**: Abstract data access with `IRepository` pattern +- **Domain Events**: Decouple domain logic and enable event-driven architecture +- **Application Services**: Use case orchestration layer (inherit from `ApplicationService`) +- **Data Transfer Objects (DTOs)**: API contract objects for input/output + +### Multi-Tenancy Architecture + +Unity Grant Manager implements multi-tenancy with **database-per-tenant isolation**: + +```mermaid +graph TB + subgraph "Multi-Tenant Data Architecture" + WebApp[Web Application] + HostDb[(Host Database
GrantManagerDbContext)] + TenantDb1[(Tenant 1 Database
GrantTenantDbContext)] + TenantDb2[(Tenant 2 Database
GrantTenantDbContext)] + TenantDb3[(Tenant N Database
GrantTenantDbContext)] + + WebApp -->|Host Data
Tenants, Users, Settings| HostDb + WebApp -->|Tenant 1 Data
Applications, Assessments| TenantDb1 + WebApp -->|Tenant 2 Data
Applications, Assessments| TenantDb2 + WebApp -->|Tenant N Data
Applications, Assessments| TenantDb3 + end + + style HostDb fill:#e1f5ff + style TenantDb1 fill:#fff4e1 + style TenantDb2 fill:#fff4e1 + style TenantDb3 fill:#fff4e1 +``` + +**Key Components:** +- **GrantManagerDbContext**: Host database context for shared/global data (tenants, users, global settings) +- **GrantTenantDbContext**: Tenant-specific database context with `[IgnoreMultiTenancy]` attribute for tenant-scoped entities +- **Separate Migrations**: Distinct migration streams for host and tenant databases +- **Tenant Resolver**: Automatically determines current tenant from request context (URL, header, or claims) + +## Module Architecture + +Unity Grant Manager follows ABP's modular architecture with internal and external modules: + +### Module Dependency Graph + +```mermaid +--- +config: + layout: elk + theme: redux + htmlLabels: true +title: Module Dependency Graph +--- +flowchart TB + subgraph subGraph0["Unity Grant Manager Application"] + Web["Unity.GrantManager.Web
Razor Pages and UI"] + HttpApi["Unity.GrantManager.HttpApi
REST API Controllers"] + App["Unity.GrantManager.Application
Application Services"] + AppContracts["Unity.GrantManager.Application.Contracts
Service Interfaces, DTOs"] + Domain["Unity.GrantManager.Domain
Entities, Repositories, Domain Services"] + DomainShared["Unity.GrantManager.Domain.Shared
Enums, Constants"] + EFCore["Unity.GrantManager.EntityFrameworkCore
DbContext, Repositories, EF Config"] + end + subgraph subGraph1["Unity Platform Modules"] + Flex["Unity.Flex
Dynamic Forms"] + Notifications["Unity.Notifications
CHES Email Integration"] + Payments["Unity.Payments
CAS Payment Integration"] + Reporting["Unity.Reporting
Report Generation"] + Identity["Unity.Identity.Web
Identity UI"] + Tenant["Unity.TenantManagement
Tenant Admin"] + Theme["Unity.Theme.UX2
UI Theme"] + SharedKernel["Unity.SharedKernel
Utilities, Message Brokers"] + end + Web --> HttpApi & App & Theme & Identity & EFCore + HttpApi --> AppContracts + App --> AppContracts & Domain & Flex & Notifications & Payments & Reporting & SharedKernel + AppContracts --> DomainShared + Domain --> DomainShared + EFCore --> Domain + + Web:::GrantApp + HttpApi:::GrantApp + App:::GrantApp + AppContracts:::GrantApp + Domain:::GrantApp + DomainShared:::GrantApp + EFCore:::GrantApp + Flex:::Platform + Notifications:::Peach + Payments:::Peach + Reporting:::Platform + Identity:::Platform + Tenant:::Platform + Theme:::Platform + SharedKernel:::Platform + classDef GrantApp fill:#BBDEFB,stroke:#000000,stroke-width:4px,color:#0D47A1 + classDef Platform fill:#C8E6C9,stroke:#2E7D32,stroke-width:4px,color:#1B5E20 + classDef Peach fill:#FFEFDB,stroke:#FBB35A,stroke-width:4px,color:#8F632D + classDef Neutral fill:#E0E0E0,stroke:#757575,stroke-width:4px,color:#424242 + style App stroke:#000000 +``` + +### Module Descriptions + +#### Unity.GrantManager (Main Application) +The core grant management application implementing grant programs, applications, assessments, and related business logic. + +**Layers:** +- **Web**: Razor Pages, view components, client-side assets, MVC controllers for UI +- **HttpApi**: RESTful API controllers extending `AbpController` +- **Application**: Application services implementing business use cases, inheriting from `ApplicationService` +- **Application.Contracts**: Service interfaces, DTOs, and application-layer contracts +- **Domain**: Entities (applications, assessments, programs), domain services, repository interfaces +- **Domain.Shared**: Enums, constants, shared types +- **EntityFrameworkCore**: EF Core DbContexts (`GrantManagerDbContext`, `GrantTenantDbContext`), repository implementations, entity configurations + +#### Unity.Flex (Dynamic Forms Module) +Provides dynamic form/field definition and rendering capabilities for customizable grant application forms. + +**Key Features:** +- Custom field definitions with validation rules +- Form layout and section management +- Runtime form rendering with data binding +- Field value storage and retrieval + +**Integration:** Grant application forms are built using Flex definitions, allowing program administrators to customize intake forms without code changes. + +#### Unity.Notifications (Notification Module) +Handles email notifications through CHES (Common Hosted Email Service) integration. + +**Key Features:** +- Email template management +- CHES API integration for government email delivery +- Notification queue and retry logic +- Notification history and tracking + +**Integration:** Triggered by domain events from Grant Manager (application submitted, assessment completed, payment processed) to send automated email notifications. + +#### Unity.Payments (Payment Processing Module) +Integrates with CAS (Common Accounting System) for government payment processing. + +**Key Features:** +- CAS API integration for payment submission +- Payment status tracking and reconciliation +- Invoice generation and management +- Payment approval workflows + +**Integration:** Grant Manager creates payment requests for approved applications, which are processed through Unity.Payments to CAS. + +#### Unity.Reporting (Reporting Module) +Advanced reporting and analytics capabilities. + +**Key Features:** +- Custom report definitions +- Data visualization and dashboards +- Report scheduling and distribution +- Export formats (PDF, Excel, CSV) + +**Integration:** Provides reporting on grant applications, assessment outcomes, payment distributions, and program performance. + +#### Unity.Identity.Web (Identity UI Module) +Custom user interface for identity management operations. + +**Key Features:** +- User registration and profile management +- Login/logout pages with Keycloak integration +- Password reset and account recovery +- Organization/team management UI + +#### Unity.TenantManagement (Tenant Management Module) +Multi-tenant administration interface. + +**Key Features:** +- Tenant creation and configuration +- Database connection string management +- Tenant-specific feature toggles +- Tenant user assignments + +#### Unity.Theme.UX2 (UI Theme Module) +Consistent government branding and user experience. + +**Key Features:** +- BC Government visual identity compliance +- Responsive layouts and components +- Accessibility (WCAG 2.1 AA) compliance +- Reusable UI components and patterns + +#### Unity.SharedKernel (Shared Utilities Module) +Cross-cutting utilities and infrastructure shared across modules. + +**Key Features:** +- HTTP client factories and helpers +- RabbitMQ message broker configuration +- Correlation ID propagation for distributed tracing +- Feature flags and utilities +- Integration abstractions + +### Module Communication Patterns + +```mermaid +sequenceDiagram + participant User + participant GrantManager + participant Flex + participant Notifications + participant Payments + participant RabbitMQ + + User->>GrantManager: Submit Grant Application + GrantManager->>Flex: Validate Form Data + Flex-->>GrantManager: Validation Result + GrantManager->>GrantManager: Create Application Entity + GrantManager->>RabbitMQ: Publish ApplicationSubmittedEvent + + RabbitMQ->>Notifications: ApplicationSubmittedEvent + Notifications->>Notifications: Generate Email from Template + Notifications->>CHES: Send Confirmation Email + CHES-->>Notifications: Email Sent + + Note over GrantManager: Assessment Process... + + GrantManager->>RabbitMQ: Publish ApplicationApprovedEvent + RabbitMQ->>Payments: ApplicationApprovedEvent + Payments->>Payments: Create Payment Request + Payments->>CAS: Submit Payment + CAS-->>Payments: Payment Confirmation + + Payments->>RabbitMQ: Publish PaymentProcessedEvent + RabbitMQ->>GrantManager: PaymentProcessedEvent + GrantManager->>GrantManager: Update Application Status + + RabbitMQ->>Notifications: PaymentProcessedEvent + Notifications->>CHES: Send Payment Notification +``` + +**Communication Mechanisms:** +1. **Direct Service References**: Modules can directly inject and call services from dependent modules (e.g., GrantManager → Flex for form validation) +2. **Domain Events (Local)**: In-process events for same-database transactions using ABP's `ILocalEventBus` +3. **Distributed Events (RabbitMQ)**: Cross-module/cross-database events using ABP's `IDistributedEventBus` with RabbitMQ transport +4. **HTTP APIs**: RESTful APIs for external integrations or microservice scenarios + +## Layer Structure & Dependencies + +Unity Grant Manager follows ABP's layered architecture with strict dependency rules: + +```mermaid +graph TD + subgraph "Presentation Layer" + UI[Web] + end + + subgraph "API Layer" + HttpApi[HttpApi
Controllers] + HttpApiClient[HttpApi.Client
C# API Proxies] + end + + subgraph "Application Layer" + App[Application
Services Implementation] + AppContracts[Application.Contracts
Interfaces & DTOs] + end + + subgraph "Domain Layer" + Domain[Domain
Entities, Domain Services, Repositories] + DomainShared[Domain.Shared
Constants, Enums] + end + + subgraph "Infrastructure Layer" + EFCore[EntityFrameworkCore
DbContext, Repositories] + end + + UI --> HttpApi + UI --> App + UI --> AppContracts + HttpApi --> AppContracts + HttpApiClient --> AppContracts + App --> AppContracts + App --> Domain + AppContracts --> DomainShared + Domain --> DomainShared + EFCore --> Domain + + style UI fill:#4a90e2 + style App fill:#7b68ee + style Domain fill:#50c878 + style EFCore fill:#f4a460 +``` + +### Dependency Rules + +1. **Domain Layer** has no dependencies on other layers (only on ABP framework) +2. **Application.Contracts** depends only on **Domain.Shared** +3. **Application** depends on **Domain** and **Application.Contracts** +4. **Infrastructure** (EF Core) depends on **Domain** only +5. **HttpApi** depends on **Application.Contracts** +6. **Web** can depend on any layer for hosting, but business logic stays in Application/Domain + +### Project Dependencies (Actual) + +**Unity.GrantManager.Web** depends on: +- Unity.GrantManager.Application +- Unity.GrantManager.HttpApi +- Unity.GrantManager.EntityFrameworkCore +- Unity.Theme.UX2 +- Unity.Identity.Web + +**Unity.GrantManager.Application** depends on: +- Unity.GrantManager.Application.Contracts +- Unity.GrantManager.Domain +- Unity.Flex +- Unity.Notifications +- Unity.Payments +- Unity.Reporting +- Unity.SharedKernel + +**Unity.GrantManager.Domain** depends on: +- Unity.GrantManager.Domain.Shared +- Volo.Abp.Identity.Domain +- Volo.Abp.TenantManagement.Domain +- Volo.Abp.AuditLogging.Domain + +**Unity.GrantManager.EntityFrameworkCore** depends on: +- Unity.GrantManager.Domain +- Volo.Abp.EntityFrameworkCore.PostgreSql + +## Data Flow & Request Pipeline + +### Typical Request Flow + +```mermaid +sequenceDiagram + participant Browser + participant Controller + participant AppService + participant DomainService + participant Repository + participant DbContext + participant Database + + Browser->>Controller: HTTP Request (POST /applications) + Controller->>Controller: Model Binding & Validation + Controller->>AppService: CreateApplicationAsync(dto) + + Note over AppService: Authorization Check
[Authorize] Attribute + Note over AppService: Start Unit of Work
Begin Transaction + + AppService->>AppService: Map DTO to Domain Entity + AppService->>DomainService: ValidateApplicationRules(entity) + DomainService-->>AppService: Validation Result + + AppService->>Repository: InsertAsync(entity) + Repository->>DbContext: Add(entity) + + Note over AppService: Publish Domain Event
ApplicationCreatedEvent + + AppService->>AppService: Commit Unit of Work + DbContext->>Database: INSERT Application + Database-->>DbContext: Success + + Note over AppService: Distributed Event
Published to RabbitMQ + + AppService->>AppService: Map Entity to DTO + AppService-->>Controller: ApplicationDto + Controller-->>Browser: HTTP 200 + JSON Response +``` + +### Cross-Cutting Concerns (Automatic via ABP) + +ABP Framework automatically handles the following concerns for application services: + +1. **Authorization**: `[Authorize]` attributes and permission checks via `IAuthorizationService` +2. **Validation**: Automatic input DTO validation using data annotations and FluentValidation +3. **Unit of Work**: Automatic transaction management with commit/rollback +4. **Audit Logging**: Automatic logging of method calls, parameters, and results +5. **Exception Handling**: Global exception filter with appropriate HTTP status codes +6. **Multi-Tenancy**: Automatic tenant resolution and data isolation + +## Database Schema Strategy + +### Multi-Database Approach + +```mermaid +erDiagram + HOST_DB ||--o{ TENANTS : contains + HOST_DB ||--o{ USERS : contains + HOST_DB ||--o{ ROLES : contains + HOST_DB ||--o{ SETTINGS : contains + + TENANT_DB ||--o{ GRANT_PROGRAMS : contains + TENANT_DB ||--o{ APPLICATIONS : contains + TENANT_DB ||--o{ ASSESSMENTS : contains + TENANT_DB ||--o{ PAYMENTS : contains + TENANT_DB ||--o{ DOCUMENTS : contains + + GRANT_PROGRAMS ||--o{ APPLICATIONS : has + APPLICATIONS ||--o{ ASSESSMENTS : has + APPLICATIONS ||--o{ PAYMENTS : receives + APPLICATIONS ||--o{ DOCUMENTS : includes +``` + +**Host Database (`GrantManagerDbContext`):** +- Tenant definitions and configurations +- Users and roles (cross-tenant identity) +- Global settings and feature flags +- Audit logs +- Background job definitions + +**Tenant Databases (`GrantTenantDbContext`):** +- Grant programs and configurations +- Applications and applicant data +- Assessment workflows and scores +- Payment requests and history +- Documents and attachments +- Tenant-specific settings + +### Migration Strategy + +1. **Host Migrations**: Located in `Unity.GrantManager.EntityFrameworkCore/Migrations/` + ```bash + dotnet ef migrations add --context GrantManagerDbContext + ``` + +2. **Tenant Migrations**: Located in `Unity.GrantManager.EntityFrameworkCore/TenantMigrations/` + ```bash + dotnet ef migrations add --context GrantTenantDbContext + ``` + +3. **DbMigrator**: Console application that applies both host and tenant migrations on startup + +## Deployment Architecture + +### Development Environment + +- **Single Instance**: All modules hosted in single ASP.NET Core process +- **Database**: Local PostgreSQL instance (can be Docker container) +- **Redis**: Local Redis instance (optional, uses in-memory cache as fallback) +- **RabbitMQ**: Local RabbitMQ instance (can be disabled for development) + +### Production Environment (Modular Monolith) + +```mermaid +graph TB + subgraph "Load Balancer (nginx)" + LB[nginx
Round-robin] + end + + subgraph "Web Application (3 replicas)" + Web1[Unity.GrantManager.Web
Instance 1] + Web2[Unity.GrantManager.Web
Instance 2] + Web3[Unity.GrantManager.Web
Instance 3] + end + + subgraph "Data Layer" + PG[(PostgreSQL
Host + Tenant DBs)] + Redis[(Redis
Cache + Sessions)] + S3[(Common Object Management Service
Blob Storage)] + end + + subgraph "Message Broker" + RabbitMQ[RabbitMQ
Event Bus] + end + + subgraph "External Services" + Keycloak[Keycloak
Identity Provider] + CHES[CHES
Email Service] + CAS[CAS
Payment System] + end + + LB --> Web1 + LB --> Web2 + LB --> Web3 + + Web1 --> PG + Web2 --> PG + Web3 --> PG + + Web1 --> Redis + Web2 --> Redis + Web3 --> Redis + + Web1 --> S3 + Web2 --> S3 + Web3 --> S3 + + Web1 --> RabbitMQ + Web2 --> RabbitMQ + Web3 --> RabbitMQ + + Web1 --> Keycloak + Web2 --> Keycloak + Web3 --> Keycloak + + Web1 --> CHES + Web2 --> CHES + Web3 --> CHES + + Web1 --> CAS + Web2 --> CAS + Web3 --> CAS +``` + +**Configuration:** +- Load balancer distributes requests across 3 web instances (Docker Compose with nginx) +- Redis used for distributed caching and session storage +- RabbitMQ provides reliable message delivery between instances +- PostgreSQL handles both host and multiple tenant databases +- Background jobs coordinated via Quartz.NET clustering + +## Security Architecture + +### Authentication Flow + +```mermaid +sequenceDiagram + participant User + participant WebApp + participant Keycloak + participant Database + + User->>WebApp: Access Protected Page + WebApp->>WebApp: Check Authentication + WebApp-->>User: Redirect to Login + User->>Keycloak: Login (username/password) + Keycloak->>Keycloak: Validate Credentials + Keycloak-->>User: Redirect with Auth Code + User->>WebApp: Auth Code + WebApp->>Keycloak: Exchange Code for Token + Keycloak-->>WebApp: ID Token + Access Token + WebApp->>WebApp: Validate Token & Create Session + WebApp->>Database: Load User Permissions + Database-->>WebApp: Roles & Permissions + WebApp-->>User: Redirect to Requested Page +``` + +### Authorization Model + +- **Role-Based Access Control (RBAC)**: Roles assigned to users (Admin, ProgramOfficer, Assessor, Applicant) +- **Permission-Based**: Granular permissions checked via `[Authorize]` attributes and `IAuthorizationService` +- **Multi-Tenant Isolation**: Tenant context automatically applied to all queries and operations +- **Data-Level Security**: Row-level security via ABP's data filters and tenant resolution + +## Performance & Scalability Considerations + +### Caching Strategy +- **Distributed Cache (Redis)**: Application settings, user permissions, frequently accessed lookup data +- **Local Memory Cache**: Static configuration, short-lived data +- **HTTP Response Caching**: Public pages and API responses with ETags + +### Database Optimization +- **Indexing**: Strategic indexes on foreign keys, tenant IDs, and frequently queried fields +- **Query Optimization**: `IQueryable` projections to load only required fields +- **Eager Loading**: Configured includes to avoid N+1 query problems +- **Async Operations**: All database operations use async/await pattern + +### Background Processing +- **Quartz.NET Jobs**: Long-running tasks (report generation, payment processing, email sending) +- **Clustering**: Background jobs coordinated across multiple instances +- **Event-Driven**: Asynchronous processing via RabbitMQ distributed events + +### Scalability +- **Horizontal Scaling**: Stateless web instances can be added behind load balancer +- **Database Partitioning**: Separate tenant databases enable independent scaling +- **Blob Storage**: Large files stored in COMS, not in database +- **CDN Ready**: Static assets can be served from CDN + +## References + +- [ABP Framework Documentation](https://docs.abp.io/en/abp/latest) +- [ABP Domain Driven Design](https://docs.abp.io/en/abp/latest/Domain-Driven-Design) +- [ABP Multi-Tenancy](https://docs.abp.io/en/abp/latest/Multi-Tenancy) +- [ABP Module Architecture Best Practices](https://docs.abp.io/en/abp/latest/Best-Practices/Module-Architecture) +- [Implementing Domain Driven Design (e-book)](https://abp.io/books/implementing-domain-driven-design) diff --git a/applications/Unity.GrantManager/CONTRIBUTING.md b/applications/Unity.GrantManager/CONTRIBUTING.md new file mode 100644 index 0000000000..a617e4fd93 --- /dev/null +++ b/applications/Unity.GrantManager/CONTRIBUTING.md @@ -0,0 +1,1423 @@ +# Contributing to Unity Grant Manager + +## Overview + +Unity Grant Manager is built on **ABP Framework 9.1.3** following Domain-Driven Design (DDD) principles. This guide outlines coding conventions, patterns, and best practices to ensure consistency and maintainability across the codebase. + +## Prerequisites + +- .NET 9.0 SDK +- PostgreSQL 15 or higher +- ABP CLI (`dotnet tool install -g Volo.Abp.Cli`) +- Visual Studio 2022 or JetBrains Rider +- Docker Desktop (for local Redis and RabbitMQ) + +## Getting Started + +1. **Clone the repository** and navigate to `Unity.GrantManager` +2. **Install JavaScript dependencies**: Run `abp install-libs` in the application root +3. **Apply database migrations**: Set `Unity.GrantManager.DbMigrator` as startup project and run (Ctrl+F5) +4. **Run the application**: Set `Unity.GrantManager.Web` as startup project and run (F5) + +## ABP Framework Conventions + +Unity Grant Manager follows ABP Framework's opinionated architecture and best practices. All contributors should familiarize themselves with: + +- [ABP Best Practices Guide](https://docs.abp.io/en/abp/latest/Best-Practices) +- [Module Architecture Best Practices](https://docs.abp.io/en/abp/latest/Best-Practices/Module-Architecture) +- [Implementing Domain Driven Design (e-book)](https://abp.io/books/implementing-domain-driven-design) + +## Project Structure & Layers + +Unity Grant Manager follows ABP's layered architecture with strict dependency rules: + +``` +Unity.GrantManager.Domain.Shared (Constants, Enums) + ↑ +Unity.GrantManager.Domain (Entities, Domain Services, Repository Interfaces) + ↑ +Unity.GrantManager.Application.Contracts (Service Interfaces, DTOs) + ↑ +Unity.GrantManager.Application (Application Services Implementation) + ↑ +Unity.GrantManager.EntityFrameworkCore (DbContext, Repositories, EF Configuration) + ↑ +Unity.GrantManager.HttpApi (API Controllers) + ↑ +Unity.GrantManager.Web (Razor Pages, UI Components) +``` + +**Dependency Rules:** +- Domain layer has no dependencies on other layers (only ABP framework) +- Application.Contracts depends only on Domain.Shared +- Application depends on Domain and Application.Contracts +- EntityFrameworkCore depends only on Domain +- Higher layers can depend on lower layers, but not vice versa + +## Coding Conventions + +### C# Language Features + +- **Target Framework**: .NET 9.0 +- **Language Version**: C# 12.0 (latest) +- **Nullable Reference Types**: Enabled project-wide + - Always declare nullability explicitly: `string?` for nullable, `string` for non-nullable + - Use `null!` only when you're certain a value won't be null (e.g., dependency injection) + - Avoid nullable warnings; fix them properly + +### Naming Conventions + +- **Classes, Methods, Properties**: PascalCase (e.g., `GrantApplication`, `CreateApplicationAsync`) +- **Private Fields**: Camel case with underscore prefix (e.g., `_repository`, `_logger`) +- **Parameters, Local Variables**: Camel case (e.g., `applicationDto`, `userId`) +- **Constants**: PascalCase (e.g., `MaxApplicationTitleLength`) +- **Interfaces**: PascalCase with `I` prefix (e.g., `IApplicationRepository`) +- **Domain Services**: Suffix with `Manager` (e.g., `ApplicationManager`, `AssessmentManager`) +- **Application Services**: Suffix with `AppService` (e.g., `ApplicationAppService`) +- **DTOs**: Suffix with purpose (e.g., `CreateApplicationDto`, `ApplicationDto`) + +### Code Style + +- **Indentation**: 4 spaces (no tabs) +- **Line Length**: Aim for 120 characters max +- **Braces**: Always use braces for control structures, even single-line statements +- **Access Modifiers**: Always specify explicitly (e.g., `public`, `private`, `protected`) +- **Using Directives**: Place at the top of the file, outside namespace +- **Async Suffix**: Always suffix async methods with `Async` (e.g., `CreateAsync`, `GetListAsync`) + +## Domain Layer Patterns + +### Entities & Aggregate Roots + +**Inherit from ABP base classes:** + +```csharp +// For entities with GUID keys +public class GrantApplication : FullAuditedAggregateRoot, IMultiTenant +{ + public Guid? TenantId { get; set; } // Required for multi-tenant entities + + // Properties with private setters for encapsulation + public string Title { get; private set; } = string.Empty; + public ApplicationStatus Status { get; private set; } + + // Parameterless constructor for EF Core + private GrantApplication() { } + + // Public constructor with required parameters + public GrantApplication(Guid id, string title) : base(id) + { + SetTitle(title); + Status = ApplicationStatus.Draft; + } + + // Business logic methods (not simple setters) + public void SetTitle(string title) + { + Title = Check.NotNullOrWhiteSpace(title, nameof(title), MaxTitleLength); + } + + public void Submit() + { + if (Status != ApplicationStatus.Draft) + throw new BusinessException("Application can only be submitted from Draft status"); + + Status = ApplicationStatus.Submitted; + + // Publish domain event + AddDistributedEvent(new ApplicationSubmittedEto { ApplicationId = Id, Title = Title }); + } +} +``` + +**Best Practices:** +- Use `FullAuditedAggregateRoot` for entities requiring full audit trails (creation, modification, deletion tracking) +- Use `AuditedAggregateRoot` if soft-delete is not needed +- Use `AggregateRoot` for simple entities without auditing +- Implement `IMultiTenant` for tenant-specific entities (stored in `GrantTenantDbContext`) +- Use private setters and expose business methods instead +- Validate input in constructors and methods using `Check` helper class +- Publish domain events using `AddLocalEvent()` or `AddDistributedEvent()` + +### Domain Services + +**Use the `Manager` suffix and inherit from `DomainService`:** + +```csharp +public class ApplicationManager : DomainService +{ + private readonly IRepository _applicationRepository; + private readonly IRepository _programRepository; + + public ApplicationManager( + IRepository applicationRepository, + IRepository programRepository) + { + _applicationRepository = applicationRepository; + _programRepository = programRepository; + } + + public virtual async Task CreateAsync( + string title, + Guid programId, + Guid applicantId) + { + // Validate business rules + var program = await _programRepository.GetAsync(programId); + if (!program.IsAcceptingApplications()) + throw new BusinessException("Program is not currently accepting applications"); + + // Create entity + var application = new GrantApplication(GuidGenerator.Create(), title); + application.SetProgram(programId); + application.SetApplicant(applicantId); + + return await _applicationRepository.InsertAsync(application); + } + + public virtual async Task ValidateForSubmission(GrantApplication application) + { + // Complex validation logic that doesn't belong in the entity + if (string.IsNullOrWhiteSpace(application.Title)) + throw new BusinessException("Application must have a title"); + + // Check for duplicate submissions + var existingCount = await _applicationRepository.CountAsync(x => + x.ApplicantId == application.ApplicantId && + x.ProgramId == application.ProgramId && + x.Status != ApplicationStatus.Draft); + + if (existingCount > 0) + throw new BusinessException("You have already submitted an application for this program"); + } +} +``` + +**Best Practices:** +- Use domain services for business logic that doesn't naturally fit within a single entity +- Only include state-changing methods; use repositories directly for queries in application services +- Make methods `virtual` to allow overriding in derived classes +- Throw `BusinessException` with clear error codes for domain validation failures +- Accept and return domain entities only (never DTOs) +- Do not implement interfaces unless there's a specific need for multiple implementations + +### Repositories + +**Define custom repository interfaces only when needed:** + +```csharp +public interface IApplicationRepository : IRepository +{ + Task> GetApplicationsByProgramAsync(Guid programId, CancellationToken cancellationToken = default); + + Task GetSubmittedCountByApplicantAsync(Guid applicantId, CancellationToken cancellationToken = default); +} +``` + +**Best Practices:** +- Use `IRepository` generic repository for standard CRUD operations +- Define custom repository interface only for complex queries or specialized operations +- Place repository interfaces in the Domain project +- Implement custom repositories in EntityFrameworkCore project +- Use async methods with `CancellationToken` support +- Return domain entities, not DTOs (mapping happens in application layer) + +## Application Layer Patterns + +### Application Services + +**Inherit from `ApplicationService` and implement interface from Application.Contracts:** + +```csharp +// In Application.Contracts project +public interface IApplicationAppService : IApplicationService +{ + Task GetAsync(Guid id); + Task> GetListAsync(GetApplicationListDto input); + Task CreateAsync(CreateApplicationDto input); + Task UpdateAsync(Guid id, UpdateApplicationDto input); + Task DeleteAsync(Guid id); +} + +// In Application project +public class ApplicationAppService : ApplicationService, IApplicationAppService +{ + private readonly IRepository _applicationRepository; + private readonly ApplicationManager _applicationManager; + + public ApplicationAppService( + IRepository applicationRepository, + ApplicationManager applicationManager) + { + _applicationRepository = applicationRepository; + _applicationManager = applicationManager; + } + + [Authorize(GrantManagementPermissions.Applications.Create)] + public virtual async Task CreateAsync(CreateApplicationDto input) + { + // Use domain service for business logic + var application = await _applicationManager.CreateAsync( + input.Title, + input.ProgramId, + CurrentUser.GetId()); + + await _applicationRepository.InsertAsync(application); + + return ObjectMapper.Map(application); + } + + [Authorize(GrantManagementPermissions.Applications.Default)] + public virtual async Task GetAsync(Guid id) + { + var application = await _applicationRepository.GetAsync(id); + return ObjectMapper.Map(application); + } + + [Authorize(GrantManagementPermissions.Applications.Default)] + public virtual async Task> GetListAsync(GetApplicationListDto input) + { + var queryable = await _applicationRepository.GetQueryableAsync(); + + // Apply filters + queryable = queryable.WhereIf(!input.Filter.IsNullOrWhiteSpace(), + x => x.Title.Contains(input.Filter!)); + + // Get total count + var totalCount = await AsyncExecuter.CountAsync(queryable); + + // Apply sorting and paging + queryable = queryable + .OrderBy(input.Sorting ?? "Title") + .PageBy(input.SkipCount, input.MaxResultCount); + + // Execute query and map to DTOs + var applications = await AsyncExecuter.ToListAsync(queryable); + var dtos = ObjectMapper.Map, List>(applications); + + return new PagedResultDto(totalCount, dtos); + } +} +``` + +**Best Practices:** +- One application service per aggregate root +- Make all public methods `virtual` for extensibility +- Use `[Authorize]` attributes for permission checks +- Accept and return DTOs only (never expose domain entities directly) +- Use `ObjectMapper` for entity-to-DTO mapping (AutoMapper) +- Use `CurrentUser` to access current user information +- Use `AsyncExecuter` to execute async LINQ queries +- Methods are automatically wrapped in Unit of Work (transaction) +- Use domain services for complex business logic +- Use `WhereIf`, `OrderBy`, `PageBy` extension methods for querying + +### Data Transfer Objects (DTOs) + +**Define DTOs in Application.Contracts project:** + +```csharp +// Input DTO +public class CreateApplicationDto +{ + [Required] + [StringLength(ApplicationConsts.MaxTitleLength)] + public string Title { get; set; } = string.Empty; + + [Required] + public Guid ProgramId { get; set; } + + public string? Description { get; set; } +} + +// Output DTO +public class ApplicationDto : AuditedEntityDto +{ + public string Title { get; set; } = string.Empty; + public Guid ProgramId { get; set; } + public string ProgramName { get; set; } = string.Empty; + public ApplicationStatus Status { get; set; } + public string? Description { get; set; } +} + +// List query DTO +public class GetApplicationListDto : PagedAndSortedResultRequestDto +{ + public string? Filter { get; set; } + public ApplicationStatus? Status { get; set; } +} +``` + +**Best Practices:** +- Use data annotations for validation (`[Required]`, `[StringLength]`, etc.) +- Inherit from ABP base DTO classes when appropriate: + - `EntityDto`: Basic DTO with ID + - `AuditedEntityDto`: Includes creation time and creator + - `FullAuditedEntityDto`: Includes modification and deletion info + - `PagedAndSortedResultRequestDto`: For list queries with paging/sorting +- Use nullable types (`?`) for optional properties +- Initialize string properties to `string.Empty` to avoid nullable warnings +- Define constants for string lengths in Domain.Shared project + +### Object Mapping (AutoMapper) + +**Configure mappings in Application project's module class:** + +```csharp +public class GrantManagerApplicationAutoMapperProfile : Profile +{ + public GrantManagerApplicationAutoMapperProfile() + { + // Entity to DTO (read) + CreateMap(); + + // DTO to Entity (write) - rarely used, prefer constructors + CreateMap() + .Ignore(x => x.Id) + .Ignore(x => x.TenantId); + } +} +``` + +## Entity Framework Core Patterns + +### DbContext Configuration + +**Two DbContext classes for multi-tenancy:** + +```csharp +// Host database context (non-tenant data) +[ConnectionStringName("Default")] +public class GrantManagerDbContext : AbpDbContext +{ + public DbSet Users { get; set; } = null!; + public DbSet Tenants { get; set; } = null!; + + public GrantManagerDbContext(DbContextOptions options) + : base(options) { } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + builder.ConfigureGrantManager(); // Extension method for entity configuration + } +} + +// Tenant database context (tenant-specific data) +[ConnectionStringName("GrantManager")] +[IgnoreMultiTenancy] // This DbContext manages its own tenancy +public class GrantTenantDbContext : AbpDbContext +{ + public DbSet Applications { get; set; } = null!; + public DbSet Programs { get; set; } = null!; + public DbSet Assessments { get; set; } = null!; + + public GrantTenantDbContext(DbContextOptions options) + : base(options) { } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + builder.ConfigureGrantTenant(); + } +} +``` + +### Entity Configuration + +**Use fluent API in extension methods:** + +```csharp +public static class GrantManagerDbContextModelCreatingExtensions +{ + public static void ConfigureGrantTenant(this ModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + builder.Entity(b => + { + b.ToTable("GrantApplications"); + + // Configure properties + b.Property(x => x.Title) + .IsRequired() + .HasMaxLength(ApplicationConsts.MaxTitleLength); + + b.Property(x => x.Description) + .HasMaxLength(ApplicationConsts.MaxDescriptionLength); + + // Configure indexes + b.HasIndex(x => x.ProgramId); + b.HasIndex(x => x.ApplicantId); + b.HasIndex(x => x.Status); + + // Configure relationships + b.HasOne() + .WithMany() + .HasForeignKey(x => x.ProgramId) + .OnDelete(DeleteBehavior.Restrict); + + // Configure ABP features + b.ConfigureByConvention(); // Configures audit properties, multi-tenancy, etc. + }); + } +} +``` + +**Best Practices:** +- Separate entity configuration from DbContext class +- Use `ConfigureByConvention()` to apply ABP conventions +- Configure indexes on foreign keys and frequently queried fields +- Specify max lengths for string properties +- Use `DeleteBehavior.Restrict` for important relationships to prevent accidental cascading deletes +- Use table name pluralization (e.g., `GrantApplications`) + +### Custom Repository Implementation + +**Implement custom repositories in EntityFrameworkCore project:** + +```csharp +public class ApplicationRepository : EfCoreRepository, IApplicationRepository +{ + public ApplicationRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) { } + + public virtual async Task> GetApplicationsByProgramAsync( + Guid programId, + CancellationToken cancellationToken = default) + { + var dbSet = await GetDbSetAsync(); + return await dbSet + .Where(x => x.ProgramId == programId) + .OrderByDescending(x => x.CreationTime) + .ToListAsync(cancellationToken); + } + + public virtual async Task GetSubmittedCountByApplicantAsync( + Guid applicantId, + CancellationToken cancellationToken = default) + { + var dbSet = await GetDbSetAsync(); + return await dbSet + .CountAsync(x => x.ApplicantId == applicantId && x.Status != ApplicationStatus.Draft, + cancellationToken); + } +} +``` + +## Testing Patterns + +### Unit Testing (Application Layer) + +**Use xUnit and Shouldly:** + +```csharp +public class ApplicationAppService_Tests : GrantManagerApplicationTestBase +{ + private readonly IApplicationAppService _applicationAppService; + private readonly IRepository _applicationRepository; + + public ApplicationAppService_Tests() + { + _applicationAppService = GetRequiredService(); + _applicationRepository = GetRequiredService>(); + } + + [Fact] + public async Task Should_Create_Application() + { + // Arrange + var input = new CreateApplicationDto + { + Title = "Test Application", + ProgramId = GrantManagerTestData.ProgramId + }; + + // Act + var result = await _applicationAppService.CreateAsync(input); + + // Assert + result.ShouldNotBeNull(); + result.Title.ShouldBe("Test Application"); + + // Verify in database + var application = await _applicationRepository.FindAsync(result.Id); + application.ShouldNotBeNull(); + application!.Title.ShouldBe("Test Application"); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public async Task Should_Not_Create_Application_With_Invalid_Title(string? invalidTitle) + { + // Arrange + var input = new CreateApplicationDto + { + Title = invalidTitle!, + ProgramId = GrantManagerTestData.ProgramId + }; + + // Act & Assert + await Should.ThrowAsync(async () => + { + await _applicationAppService.CreateAsync(input); + }); + } +} +``` + +**Best Practices:** +- Inherit from test base class that sets up DI container and database +- Use `[Fact]` for single test cases, `[Theory]` with `[InlineData]` for parameterized tests +- Use Shouldly assertions: `ShouldBe()`, `ShouldNotBeNull()`, `ShouldThrow()`, etc. +- Follow Arrange-Act-Assert pattern +- Use meaningful test method names: `Should_[Expected]_[Scenario]` +- Test both success and failure paths +- Clean up test data if not using transaction rollback + +### Integration Testing (Domain Layer) + +```csharp +public class ApplicationManager_Tests : GrantManagerDomainTestBase +{ + private readonly ApplicationManager _applicationManager; + private readonly IRepository _applicationRepository; + + public ApplicationManager_Tests() + { + _applicationManager = GetRequiredService(); + _applicationRepository = GetRequiredService>(); + } + + [Fact] + public async Task Should_Create_Application_When_Program_Is_Open() + { + // Arrange + var programId = GrantManagerTestData.OpenProgramId; + var applicantId = Guid.NewGuid(); + + // Act + var application = await _applicationManager.CreateAsync("Test", programId, applicantId); + + // Assert + application.ShouldNotBeNull(); + application.Status.ShouldBe(ApplicationStatus.Draft); + } + + [Fact] + public async Task Should_Throw_When_Program_Is_Closed() + { + // Arrange + var programId = GrantManagerTestData.ClosedProgramId; + var applicantId = Guid.NewGuid(); + + // Act & Assert + var exception = await Should.ThrowAsync(async () => + { + await _applicationManager.CreateAsync("Test", programId, applicantId); + }); + + exception.Message.ShouldContain("not currently accepting applications"); + } +} +``` + +## Database Migrations + +### Creating Migrations + +**Host Database (GrantManagerDbContext):** +```powershell +# Navigate to EntityFrameworkCore project +cd src/Unity.GrantManager.EntityFrameworkCore + +# Add migration +dotnet ef migrations add AddUserPreferences --context GrantManagerDbContext + +# Remove last migration if needed +dotnet ef migrations remove --context GrantManagerDbContext +``` + +**Tenant Database (GrantTenantDbContext):** +```powershell +# Navigate to EntityFrameworkCore project +cd src/Unity.GrantManager.EntityFrameworkCore + +# Add migration +dotnet ef migrations add AddApplicationAttachments --context GrantTenantDbContext + +# Remove last migration if needed +dotnet ef migrations remove --context GrantTenantDbContext +``` + +### Applying Migrations + +**Use DbMigrator project:** +```powershell +# Set DbMigrator as startup project in Visual Studio +# Press Ctrl+F5 to run without debugging +# DbMigrator will apply all pending migrations to both host and tenant databases +``` + +**Best Practices:** +- Use descriptive migration names (e.g., `AddApplicationStatus`, `UpdateAssessmentSchema`) +- Review generated migration code before applying +- Never modify migration files after they've been applied in production +- Always test migrations on a copy of production data +- Keep migrations small and focused on single changes +- Add seed data in `GrantManagerDataSeedContributor` class, not in migrations + +## Multi-Tenancy Guidelines + +### Tenant-Aware Entities + +```csharp +public class GrantApplication : FullAuditedAggregateRoot, IMultiTenant +{ + public Guid? TenantId { get; set; } // Automatically set by ABP + + // ... other properties +} +``` + +**Best Practices:** +- Implement `IMultiTenant` for all tenant-specific entities +- Store tenant data in `GrantTenantDbContext` (separate database) +- ABP automatically filters queries by current tenant +- Use `[IgnoreMultiTenancy]` attribute on DbContext to manage tenant data manually +- Never manually filter by `TenantId` in queries (ABP does this automatically) + +### Testing Multi-Tenancy + +```csharp +[Fact] +public async Task Should_Only_Get_Current_Tenant_Applications() +{ + // Arrange - switch to specific tenant + using (CurrentTenant.Change(GrantManagerTestData.TenantId)) + { + // Act + var applications = await _applicationRepository.GetListAsync(); + + // Assert - all applications belong to current tenant + applications.ShouldAllBe(x => x.TenantId == GrantManagerTestData.TenantId); + } +} +``` + +## Event-Driven Architecture + +### Publishing Domain Events + +**Local Events (same database transaction):** +```csharp +public class GrantApplication : FullAuditedAggregateRoot +{ + public void Submit() + { + Status = ApplicationStatus.Submitted; + + // Published when UnitOfWork commits + AddLocalEvent(new ApplicationSubmittedEvent + { + ApplicationId = Id, + Title = Title + }); + } +} +``` + +**Distributed Events (RabbitMQ, cross-module):** +```csharp +public class GrantApplication : FullAuditedAggregateRoot +{ + public void Approve() + { + Status = ApplicationStatus.Approved; + + // Published to RabbitMQ after UnitOfWork commits + AddDistributedEvent(new ApplicationApprovedEto // ETO = Event Transfer Object + { + ApplicationId = Id, + Title = Title, + ApplicantId = ApplicantId + }); + } +} +``` + +### Handling Events + +**Local Event Handler:** +```csharp +public class ApplicationSubmittedHandler : ILocalEventHandler, ITransientDependency +{ + private readonly ILogger _logger; + + public ApplicationSubmittedHandler(ILogger logger) + { + _logger = logger; + } + + public virtual async Task HandleEventAsync(ApplicationSubmittedEvent eventData) + { + _logger.LogInformation($"Application {eventData.ApplicationId} was submitted"); + + // Handle within same transaction + await Task.CompletedTask; + } +} +``` + +**Distributed Event Handler:** +```csharp +public class ApplicationApprovedHandler : IDistributedEventHandler, ITransientDependency +{ + private readonly IPaymentService _paymentService; + + public ApplicationApprovedHandler(IPaymentService paymentService) + { + _paymentService = paymentService; + } + + public virtual async Task HandleEventAsync(ApplicationApprovedEto eventData) + { + // Create payment request in different module/database + await _paymentService.CreatePaymentRequestAsync(eventData.ApplicationId); + } +} +``` + +## Front-End Development + +### Client-Side Package Management + +Unity Grant Manager uses **NPM** for client-side package management with ABP's resource mapping system. + +**Adding a new NPM package:** + +1. Add dependency to `package.json`: +```json +{ + "dependencies": { + "@abp/bootstrap": "^8.3.4", + "datatables.net-bs5": "^1.13.6", + "your-package-name": "^1.0.0" + } +} +``` + +2. Run npm install: +```bash +npm install +``` + +3. Configure resource mapping in `abp.resourcemapping.js`: +```javascript +module.exports = { + aliases: { + '@node_modules': './node_modules', + '@libs': './wwwroot/libs', + }, + mappings: { + '@node_modules/your-package/dist/': '@libs/your-package/', + }, +}; +``` + +4. Run ABP CLI to copy resources: +```bash +abp install-libs +``` + +5. Add to bundle contributor in `Unity.Theme.UX2`: +```csharp +// UnityThemeUX2GlobalScriptContributor.cs +public override void ConfigureBundle(BundleConfigurationContext context) +{ + context.Files.AddIfNotContains("/libs/your-package/your-script.js"); +} +``` + +**Best Practices:** +- Prefer `@abp/*` packages (e.g., `@abp/jquery`, `@abp/bootstrap`) for version consistency across modules +- Always use `AddIfNotContains()` to prevent duplicate script/style references +- Use `abp install-libs` after any `package.json` changes +- Map only necessary files (js, css, fonts) to reduce bundle size + +### JavaScript Conventions + +**File Organization:** +- Place page-specific JavaScript in `/Pages/[Feature]/[PageName].js` +- Place reusable utilities in `/wwwroot/scripts/` or theme modules +- Use IIFE pattern to avoid global scope pollution + +**Standard Pattern:** +```javascript +(function ($) { + var l = abp.localization.getResource('GrantManager'); + + var dataTable = $('#MyTable').DataTable(abp.libs.datatables.normalizeConfiguration({ + processing: true, + serverSide: true, + paging: true, + ajax: abp.libs.datatables.createAjax( + acme.grantManager.myFeature.myService.getList + ), + columnDefs: [ + { + title: l('Actions'), + rowAction: { + items: [ + { + text: l('Edit'), + action: function (data) { + editModal.open({ id: data.record.id }); + } + }, + { + text: l('Delete'), + confirmMessage: function (data) { + return l('DeleteConfirmationMessage', data.record.name); + }, + action: function (data) { + acme.grantManager.myFeature.myService + .delete(data.record.id) + .then(function () { + abp.notify.success(l('SuccessfullyDeleted')); + dataTable.ajax.reload(); + }); + } + } + ] + } + }, + { + title: l('Name'), + data: 'name' + }, + { + title: l('CreationTime'), + data: 'creationTime', + dataFormat: 'datetime' + } + ] + })); + + var createModal = new abp.ModalManager({ + viewUrl: abp.appPath + 'GrantManager/MyFeature/CreateModal', + modalClass: 'myFeatureCreate' + }); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + $('#NewRecordButton').click(function (e) { + e.preventDefault(); + createModal.open(); + }); + +})(jQuery); +``` + +### ABP Dynamic JavaScript API Client Proxies + +ABP automatically generates JavaScript client proxies for your application services. + +**How it works:** +- Application services inheriting from `ApplicationService` are auto-exposed as HTTP APIs +- `/Abp/ServiceProxyScript` endpoint generates JavaScript proxy functions +- Proxies follow namespace convention: `[moduleName].[namespace].[serviceName].[methodName]` + +**Example Application Service:** +```csharp +public class ApplicationAppService : ApplicationService, IApplicationAppService +{ + public virtual async Task> GetListAsync(GetApplicationListDto input) + { + // Implementation + } + + public virtual async Task CreateAsync(CreateApplicationDto input) + { + // Implementation + } +} +``` + +**Generated JavaScript Proxy Usage:** +```javascript +// GET list +acme.grantManager.applications.application.getList({ + maxResultCount: 10, + skipCount: 0, + filter: 'search term' +}).then(function(result) { + console.log(result.items); + console.log(result.totalCount); +}); + +// POST create +acme.grantManager.applications.application.create({ + title: 'My Application', + description: 'Description' +}).then(function(result) { + abp.notify.success('Successfully created!'); +}); + +// DELETE +acme.grantManager.applications.application + .delete('3fa85f64-5717-4562-b3fc-2c963f66afa6') + .then(function() { + abp.notify.success('Successfully deleted!'); + }); + +// PUT update +acme.grantManager.applications.application + .update('3fa85f64-5717-4562-b3fc-2c963f66afa6', { + title: 'Updated Title' + }) + .then(function(result) { + abp.notify.success('Successfully updated!'); + }); +``` + +**AJAX Options:** +You can override AJAX options by passing an additional parameter: +```javascript +acme.grantManager.applications.application + .delete(id, { + type: 'POST', // Override HTTP method + dataType: 'json', + success: function() { + console.log('Custom success handler'); + } + }); +``` + +**Benefits:** +- Type-safe API calls (parameters match C# method signatures) +- Automatic error handling via `abp.ajax` +- No manual AJAX configuration needed +- Returns jQuery Deferred objects (`.then()`, `.catch()`, `.always()`) + +### DataTables.net Integration + +Unity Grant Manager uses DataTables.net 1.x with Bootstrap 5 styling. + +**Basic DataTable Setup:** +```javascript +var dataTable = $('#MyTable').DataTable(abp.libs.datatables.normalizeConfiguration({ + processing: true, + serverSide: true, + paging: true, + searching: true, + autoWidth: false, + scrollCollapse: true, + order: [[1, "asc"]], + ajax: abp.libs.datatables.createAjax( + acme.grantManager.myService.getList, + function () { + return { + filter: $('#SearchInput').val(), + status: $('#StatusFilter').val() + }; + } + ), + columnDefs: [ + { + title: l('Actions'), + rowAction: { + items: [ + { + text: l('Edit'), + visible: abp.auth.isGranted('GrantManager.Edit'), + action: function (data) { + editModal.open({ id: data.record.id }); + } + } + ] + } + }, + { + title: l('Name'), + data: 'name', + orderable: true + }, + { + title: l('Status'), + data: 'status', + render: function (data) { + return l('Enum:ApplicationStatus.' + data); + } + } + ] +})); +``` + +**Advanced DataTable Features:** + +1. **Custom Filters:** +```javascript +$('#SearchInput').on('input', function () { + dataTable.ajax.reload(); +}); + +$('#StatusFilter').change(function () { + dataTable.ajax.reload(); +}); +``` + +2. **Row Selection:** +```javascript +var dataTable = $('#MyTable').DataTable({ + // ... other config + select: { + style: 'multi' + } +}); + +$('#BulkDeleteButton').click(function () { + var selectedRows = dataTable.rows({ selected: true }).data().toArray(); + // Process selected rows +}); +``` + +3. **Export Buttons:** +```javascript +var dataTable = $('#MyTable').DataTable({ + // ... other config + buttons: [ + { + extend: 'excel', + text: l('ExportToExcel'), + exportOptions: { + columns: ':visible' + } + }, + { + extend: 'csv', + text: l('ExportToCsv') + } + ] +}); +``` + +4. **Column Visibility:** +```javascript +var dataTable = $('#MyTable').DataTable({ + // ... other config + buttons: [ + { + extend: 'colvis', + text: l('ColumnVisibility') + } + ] +}); +``` + +**DataTable Best Practices:** +- Always use `abp.libs.datatables.normalizeConfiguration()` for ABP integration +- Use `abp.libs.datatables.createAjax()` for automatic server-side pagination +- Leverage `rowAction` for action buttons with permission checks +- Use `dataFormat` property for date/datetime/boolean formatting +- Call `dataTable.ajax.reload()` after CRUD operations + +### ABP Modal Manager + +Use `abp.ModalManager` for consistent modal dialogs. + +**Basic Modal Usage:** +```javascript +var createModal = new abp.ModalManager({ + viewUrl: abp.appPath + 'GrantManager/Applications/CreateModal', + scriptUrl: abp.appPath + 'Pages/GrantManager/Applications/CreateModal.js', + modalClass: 'applicationCreate' +}); + +createModal.onOpen(function () { + console.log('Modal opened'); +}); + +createModal.onResult(function (result) { + abp.notify.success(l('SavedSuccessfully')); + dataTable.ajax.reload(); +}); + +createModal.onClose(function () { + console.log('Modal closed'); +}); + +$('#NewApplicationButton').click(function (e) { + e.preventDefault(); + createModal.open(); +}); +``` + +**Modal Script Pattern (CreateModal.js):** +```javascript +abp.modals.applicationCreate = function () { + var l = abp.localization.getResource('GrantManager'); + var _$form = null; + var _$modal = null; + + this.init = function (modalManager, args) { + _$modal = modalManager.getModal(); + _$form = modalManager.getForm(); + + // Initialize form validation + _$form.data('validator').settings.ignore = ''; + + // Custom form logic + $('#ProgramSelect').change(function () { + var programId = $(this).val(); + // Load dynamic fields based on program + }); + }; +}; +``` + +**Submitting Modal Forms:** +```csharp +// In Razor Page (CreateModal.cshtml.cs) +public async Task OnPostAsync() +{ + await _applicationAppService.CreateAsync(Application); + return NoContent(); // Return NoContent to close modal and trigger onResult +} +``` + +### ABP JavaScript Utilities + +**Localization:** +```javascript +var l = abp.localization.getResource('GrantManager'); +var message = l('WelcomeMessage'); +var formatted = l('GreetingMessage', userName); // With parameters +``` + +**Notifications:** +```javascript +abp.notify.success('Operation completed successfully'); +abp.notify.info('Information message'); +abp.notify.warn('Warning message'); +abp.notify.error('An error occurred'); +``` + +**Confirmation Dialogs:** +```javascript +abp.message.confirm( + 'Are you sure you want to delete this item?', + 'Confirm Delete' +).then(function (confirmed) { + if (confirmed) { + // Perform delete + } +}); +``` + +**Busy Indicator:** +```javascript +abp.ui.setBusy('#MyForm'); + +// ... perform operation + +abp.ui.clearBusy('#MyForm'); +``` + +**AJAX Calls (when proxy not available):** +```javascript +abp.ajax({ + url: '/api/app/my-service/custom-endpoint', + type: 'POST', + data: JSON.stringify({ key: 'value' }), + contentType: 'application/json' +}).then(function (result) { + console.log(result); +}); +``` + +**Authorization:** +```javascript +if (abp.auth.isGranted('GrantManager.Applications.Edit')) { + // Show edit button +} +``` + +**Settings:** +```javascript +var settingValue = abp.setting.get('SettingName'); +var intValue = abp.setting.getInt('NumericSetting'); +var boolValue = abp.setting.getBoolean('BooleanSetting'); +``` + +### DOM Event Handlers + +ABP provides automatic initialization for common UI components via DOM event handlers. + +**Auto-Initialized Components:** +- **Tooltips:** `data-bs-toggle="tooltip"` +- **Popovers:** `data-bs-toggle="popover"` +- **Datepickers:** `input.datepicker` or `input[type=date]` +- **AJAX Forms:** `data-ajaxForm="true"` +- **Autocomplete Selects:** `class="auto-complete-select"` + +**Example - Autocomplete Select:** +```html + +``` + +**Example - Confirmation Dialog:** +```html + + + + +``` + +**Example - AJAX Form:** +```html +
+ +
+``` + +### JavaScript Best Practices + +**DO:** +- ✅ Use ABP dynamic proxies instead of manual `$.ajax` calls +- ✅ Wrap code in IIFE to avoid global scope pollution: `(function ($) { ... })(jQuery);` +- ✅ Use `abp.localization` for all user-facing text +- ✅ Use `abp.notify` and `abp.message` for user feedback +- ✅ Use `abp.auth.isGranted()` for permission checks in UI +- ✅ Use `abp.ModalManager` for modal dialogs +- ✅ Use `abp.libs.datatables` helpers for DataTables integration +- ✅ Call `dataTable.ajax.reload()` after CRUD operations +- ✅ Use `abp.ui.setBusy()` for long-running operations + +**DON'T:** +- ❌ Don't use global variables (use module pattern or IIFE) +- ❌ Don't hardcode text strings (use localization) +- ❌ Don't use `alert()` or `confirm()` (use `abp.notify` and `abp.message`) +- ❌ Don't manually construct API URLs (use dynamic proxies) +- ❌ Don't forget to handle errors in promise chains +- ❌ Don't bypass ABP's modal manager for modal dialogs +- ❌ Don't forget to check permissions before showing UI elements + +## Common Pitfalls & Solutions + +### ❌ Don't: Expose entities directly from application services +```csharp +public async Task GetAsync(Guid id) // ❌ Wrong +{ + return await _applicationRepository.GetAsync(id); +} +``` + +### ✅ Do: Return DTOs +```csharp +public async Task GetAsync(Guid id) // ✅ Correct +{ + var application = await _applicationRepository.GetAsync(id); + return ObjectMapper.Map(application); +} +``` + +### ❌ Don't: Put business logic in application services +```csharp +public async Task CreateAsync(CreateApplicationDto input) // ❌ Wrong +{ + var application = new GrantApplication(GuidGenerator.Create(), input.Title); + + // Complex validation logic here (should be in domain service) + var program = await _programRepository.GetAsync(input.ProgramId); + if (!program.IsAcceptingApplications()) + throw new BusinessException("..."); + + await _applicationRepository.InsertAsync(application); + return ObjectMapper.Map(application); +} +``` + +### ✅ Do: Use domain services for business logic +```csharp +public async Task CreateAsync(CreateApplicationDto input) // ✅ Correct +{ + // Delegate to domain service + var application = await _applicationManager.CreateAsync( + input.Title, + input.ProgramId, + CurrentUser.GetId()); + + await _applicationRepository.InsertAsync(application); + return ObjectMapper.Map(application); +} +``` + +### ❌ Don't: Use non-virtual methods +```csharp +public async Task GetAsync(Guid id) // ❌ Can't be overridden +{ + // ... +} +``` + +### ✅ Do: Make methods virtual for extensibility +```csharp +public virtual async Task GetAsync(Guid id) // ✅ Can be overridden +{ + // ... +} +``` + +### ❌ Don't: Manually filter by TenantId +```csharp +var applications = await _applicationRepository // ❌ Wrong + .GetListAsync(x => x.TenantId == CurrentTenant.Id); +``` + +### ✅ Do: Let ABP handle tenant filtering automatically +```csharp +var applications = await _applicationRepository.GetListAsync(); // ✅ Correct +// ABP automatically filters by current tenant +``` + +## Code Review Checklist + +Before submitting a pull request, ensure: + +- [ ] Code follows ABP Framework conventions and patterns +- [ ] All public methods are `virtual` +- [ ] Nullable reference types are handled correctly +- [ ] DTOs are used for application service inputs/outputs (not entities) +- [ ] Domain logic is in domain layer (entities/domain services) +- [ ] Application services orchestrate use cases (thin layer) +- [ ] Repository interfaces are defined only when custom queries needed +- [ ] Entity configurations use fluent API in extension methods +- [ ] Multi-tenant entities implement `IMultiTenant` +- [ ] Tests are written for new functionality (xUnit + Shouldly) +- [ ] Database migrations are created for schema changes +- [ ] Authorization attributes (`[Authorize]`) are applied +- [ ] Logging is added for important operations +- [ ] Exception handling uses `BusinessException` for domain errors +- [ ] Async/await is used consistently +- [ ] No nullable reference type warnings + +## Resources + +- [ABP Framework Documentation](https://docs.abp.io/en/abp/latest) +- [ABP Best Practices](https://docs.abp.io/en/abp/latest/Best-Practices) +- [Implementing Domain Driven Design (e-book)](https://abp.io/books/implementing-domain-driven-design) +- [ABP Community](https://community.abp.io/) +- [ABP GitHub Repository](https://github.com/abpframework/abp) +- [ARCHITECTURE.md](./ARCHITECTURE.md) - System architecture overview +- [PRODUCT.md](./PRODUCT.md) - Product vision and features diff --git a/applications/Unity.GrantManager/PRODUCT.md b/applications/Unity.GrantManager/PRODUCT.md new file mode 100644 index 0000000000..e1ff34461b --- /dev/null +++ b/applications/Unity.GrantManager/PRODUCT.md @@ -0,0 +1,101 @@ +# Unity Grant Manager - Product Vision + +## Overview + +Unity Grant Manager is a comprehensive grant management platform designed for the Government of British Columbia to streamline and automate the entire grants lifecycle. The application enables government staff to manage grant programs, review applications, conduct assessments, and process payments, while providing applicants with a user-friendly portal to submit and track their grant applications. + +## Product Goals + +1. **Streamline Grant Administration**: Reduce administrative overhead by automating grant program management, application intake, assessment workflows, and payment processing. + +2. **Enhance Transparency**: Provide applicants and stakeholders with real-time visibility into application status, assessment progress, and payment tracking. + +3. **Ensure Compliance**: Maintain audit trails, enforce business rules, and integrate with government systems (CHES email service, CAS payment system) to ensure regulatory compliance. + +4. **Enable Flexibility**: Support dynamic form creation for diverse grant programs with varying requirements through the Unity.Flex module. + +5. **Support Multi-Tenancy**: Enable multiple government organizations or programs to operate independently within a shared platform instance. + +## Key Features + +### Grant Program Management +- Configure and manage multiple grant programs with unique requirements, eligibility criteria, and assessment workflows +- Define program intake periods, budget allocations, and reporting requirements +- Track program performance metrics and funding distribution + +### Applicant Portal +- Self-service application submission with dynamic forms tailored to each grant program +- Document upload and management for supporting materials +- Real-time application status tracking and notifications +- Application editing and resubmission capabilities during intake periods + +### Application Assessment & Scoring +- Configurable assessment workflows with multiple review stages +- Collaborative review process with scoring rubrics and criteria +- Assignment of applications to assessors and review teams +- Consolidated scoring and recommendation reporting +- Comment threads and internal discussions on applications + +### Payment Processing +- Integration with CAS (Common Accounting System) for government payment processing +- Payment milestone tracking and approval workflows +- Payment history and reconciliation reporting +- Support for installment-based and milestone-based payment schedules + +### Notifications & Communications +- Automated email notifications via CHES (Common Hosted Email Service) +- Configurable notification templates for application status changes +- Event-driven notifications for assessments, payments, and program updates +- Communication history tracking + +### Reporting & Analytics +- Customizable reports on application volumes, assessment outcomes, and payment distributions +- Program performance dashboards and metrics +- Export capabilities for external analysis and compliance reporting +- Integration with Unity.Reporting module for advanced reporting features + +### User & Role Management +- Role-based access control for staff, assessors, and applicants +- Integration with Keycloak for authentication and single sign-on +- Organization and team-based permission structures +- Audit logging for all user actions + +## Target Users + +### Government Program Staff +- Grant program administrators who configure programs and manage intake periods +- Grant officers who oversee application processing and assessment coordination +- Finance staff who manage payment processing and budget tracking + +### Assessors & Reviewers +- Internal and external subject matter experts who evaluate applications +- Review panel members who participate in scoring and recommendation processes + +### Applicants +- Individuals, organizations, or businesses applying for government grants +- Grant recipients tracking payment schedules and reporting requirements + +## Integration Points + +### Unity Platform Modules +- **Unity.Flex**: Dynamic form definitions for customizable application forms +- **Unity.Notifications**: Email notifications via CHES integration +- **Unity.Payments**: Payment processing through CAS integration +- **Unity.Reporting**: Advanced reporting and data visualization +- **Unity.Identity.Web**: User authentication and authorization +- **Unity.TenantManagement**: Multi-tenant configuration and isolation +- **Unity.Theme.UX2**: Consistent user interface and experience + +### External Systems +- **CHES (Common Hosted Email Service)**: Government email notification service +- **CAS (Common Accounting System)**: Government payment processing system +- **Keycloak**: Enterprise identity and access management +- **AWS S3**: Document storage and blob management + +## Success Criteria + +1. **Efficiency**: Reduce grant processing time by 40% through automation and streamlined workflows +2. **User Satisfaction**: Achieve 85%+ satisfaction ratings from both applicants and staff users +3. **Transparency**: Provide real-time status updates for 100% of applications +4. **Compliance**: Maintain complete audit trails and pass all security/privacy audits +5. **Scalability**: Support multiple concurrent grant programs with thousands of applications From cacabc08863428013d8ce71329ade53a298e9245 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:10:45 -0800 Subject: [PATCH 2/6] AB#32037 - Update applications/Unity.GrantManager/.github/copilot-instructions.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- applications/Unity.GrantManager/.github/copilot-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/.github/copilot-instructions.md b/applications/Unity.GrantManager/.github/copilot-instructions.md index e66b151366..c151061894 100644 --- a/applications/Unity.GrantManager/.github/copilot-instructions.md +++ b/applications/Unity.GrantManager/.github/copilot-instructions.md @@ -335,7 +335,7 @@ acme.grantManager.applications.application ### DataTables.net Integration -**Unity Grant Manager uses DataTables.net 1.x** (not 2.x due to ABP compatibility). +**Unity Grant Manager uses DataTables.net 2.x** with the Bootstrap 5 integration package (`datatables.net-bs5`). Ensure generated examples and APIs target DataTables 2.x. **Standard DataTable pattern:** ```javascript From 4e3792a21eec024c1d5eecbf08a5b8e2f9e23cc0 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:11:16 -0800 Subject: [PATCH 3/6] AB#32037 - Update applications/Unity.GrantManager/PRODUCT.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- applications/Unity.GrantManager/PRODUCT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/PRODUCT.md b/applications/Unity.GrantManager/PRODUCT.md index e1ff34461b..bbdd7f8b47 100644 --- a/applications/Unity.GrantManager/PRODUCT.md +++ b/applications/Unity.GrantManager/PRODUCT.md @@ -90,7 +90,7 @@ Unity Grant Manager is a comprehensive grant management platform designed for th - **CHES (Common Hosted Email Service)**: Government email notification service - **CAS (Common Accounting System)**: Government payment processing system - **Keycloak**: Enterprise identity and access management -- **AWS S3**: Document storage and blob management +- **COMS (Common Object Management Service)**: Document storage and blob management via S3-compatible APIs ## Success Criteria From f67f8879fdca2443ee6ae93f772f1e56dfbfaf8f Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:20:51 -0700 Subject: [PATCH 4/6] feat: Add initial setup for Copilot and documentation standards - Created GitHub Actions workflow for Copilot setup steps. - Added agent documentation for Architect, Debugger, Reviewer, Software Engineer roles. - Established code review, C# development, documentation, JavaScript, performance, security, and testing standards. - Introduced skills for code review, debugging, documentation generation, code refactoring, component setup, and test writing. --- .github/workflows/copilot-setup-steps.yml | 33 ++++++++ .../.github/agents/architect.agent.md | 48 +++++++++++ .../.github/agents/debugger.agent.md | 71 ++++++++++++++++ .../.github/agents/reviewer.agent.md | 70 ++++++++++++++++ .../.github/agents/software-engineer.agent.md | 52 ++++++++++++ .../instructions/code-review.instructions.md | 59 ++++++++++++++ .../instructions/csharp.instructions.md | 80 +++++++++++++++++++ .../documentation.instructions.md | 43 ++++++++++ .../instructions/javascript.instructions.md | 54 +++++++++++++ .../instructions/performance.instructions.md | 52 ++++++++++++ .../instructions/security.instructions.md | 50 ++++++++++++ .../instructions/testing.instructions.md | 55 +++++++++++++ .../.github/skills/code-review/SKILL.md | 37 +++++++++ .../.github/skills/debug-issue/SKILL.md | 46 +++++++++++ .../.github/skills/generate-docs/SKILL.md | 34 ++++++++ .../.github/skills/refactor-code/SKILL.md | 38 +++++++++ .../.github/skills/setup-component/SKILL.md | 46 +++++++++++ .../.github/skills/write-tests/SKILL.md | 41 ++++++++++ 18 files changed, 909 insertions(+) create mode 100644 .github/workflows/copilot-setup-steps.yml create mode 100644 applications/Unity.GrantManager/.github/agents/architect.agent.md create mode 100644 applications/Unity.GrantManager/.github/agents/debugger.agent.md create mode 100644 applications/Unity.GrantManager/.github/agents/reviewer.agent.md create mode 100644 applications/Unity.GrantManager/.github/agents/software-engineer.agent.md create mode 100644 applications/Unity.GrantManager/.github/instructions/code-review.instructions.md create mode 100644 applications/Unity.GrantManager/.github/instructions/csharp.instructions.md create mode 100644 applications/Unity.GrantManager/.github/instructions/documentation.instructions.md create mode 100644 applications/Unity.GrantManager/.github/instructions/javascript.instructions.md create mode 100644 applications/Unity.GrantManager/.github/instructions/performance.instructions.md create mode 100644 applications/Unity.GrantManager/.github/instructions/security.instructions.md create mode 100644 applications/Unity.GrantManager/.github/instructions/testing.instructions.md create mode 100644 applications/Unity.GrantManager/.github/skills/code-review/SKILL.md create mode 100644 applications/Unity.GrantManager/.github/skills/debug-issue/SKILL.md create mode 100644 applications/Unity.GrantManager/.github/skills/generate-docs/SKILL.md create mode 100644 applications/Unity.GrantManager/.github/skills/refactor-code/SKILL.md create mode 100644 applications/Unity.GrantManager/.github/skills/setup-component/SKILL.md create mode 100644 applications/Unity.GrantManager/.github/skills/write-tests/SKILL.md diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 0000000000..c033b5f0f5 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,33 @@ +name: "Copilot Setup Steps" +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up .NET 9 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "9.0.x" + + - name: Restore dependencies + run: dotnet restore applications/Unity.GrantManager/Unity.GrantManager.sln + + - name: Build solution + run: dotnet build applications/Unity.GrantManager/Unity.GrantManager.sln --no-restore + + - name: Run tests + run: dotnet test applications/Unity.GrantManager/Unity.GrantManager.sln --no-build diff --git a/applications/Unity.GrantManager/.github/agents/architect.agent.md b/applications/Unity.GrantManager/.github/agents/architect.agent.md new file mode 100644 index 0000000000..f356615c84 --- /dev/null +++ b/applications/Unity.GrantManager/.github/agents/architect.agent.md @@ -0,0 +1,48 @@ +--- +description: "Solution architect analyzing codebase structure, designing features, and ensuring architectural integrity." +--- + +# Architect + +You are a solution architect for the Unity Grant Manager application. You analyze the existing codebase, design new features, evaluate architectural trade-offs, and ensure changes align with ABP Framework conventions and DDD principles. + +## Context + +Unity Grant Manager is a government grant management platform built on: +- **Framework**: ABP Framework 9.1.3 on .NET 9.0 +- **Architecture**: Modular monolith with DDD layered structure +- **Multi-Tenancy**: Database-per-tenant with dual DbContext (GrantManagerDbContext, GrantTenantDbContext) +- **Modules**: Unity.Flex, Unity.Notifications, Unity.Payments, Unity.Reporting, Unity.SharedKernel +- **External**: CHES (email), CAS (payments), Keycloak (identity), AWS S3 (storage) + +**Essential Reading:** +- [ARCHITECTURE.md](../../ARCHITECTURE.md): Comprehensive system architecture with Mermaid diagrams +- [PRODUCT.md](../../PRODUCT.md): Product vision and business goals +- [CONTRIBUTING.md](../../CONTRIBUTING.md): ABP patterns and conventions + +## Your Role + +You are a **read-only analyst and designer**: +- ✅ Analyze codebase and module dependencies +- ✅ Design data models, API contracts, and integration patterns +- ✅ Evaluate architectural trade-offs and recommend approaches +- ✅ Identify affected layers, modules, and integration points +- ✅ Create architecture decision records and design documents +- ❌ Do NOT write implementation code (delegate to software-engineer or tdd agents) + +## Design Considerations + +- **Layer Dependencies**: Domain has no dependencies; Application depends on Domain; Web depends on all +- **Multi-Tenancy**: Determine whether data is tenant-scoped (GrantTenantDbContext) or host-scoped (GrantManagerDbContext) +- **Module Communication**: Direct service injection for same-process; distributed events for cross-module +- **Events**: Local events for same-database transactions; distributed events (RabbitMQ) for cross-module +- **Security**: Permission model, authorization boundaries, data isolation + +## Output Format + +Provide architectural recommendations as: +1. **Affected layers and modules** with impact assessment +2. **Data model design** with entity relationships +3. **Integration points** (internal modules and external systems) +4. **Mermaid diagrams** for complex flows +5. **Trade-offs and risks** with mitigation strategies diff --git a/applications/Unity.GrantManager/.github/agents/debugger.agent.md b/applications/Unity.GrantManager/.github/agents/debugger.agent.md new file mode 100644 index 0000000000..54019733c0 --- /dev/null +++ b/applications/Unity.GrantManager/.github/agents/debugger.agent.md @@ -0,0 +1,71 @@ +--- +description: "Debugging specialist diagnosing and resolving issues in ABP Framework applications." +--- + +# Debugger + +You are a debugging specialist for the Unity Grant Manager application. You systematically diagnose and resolve bugs, errors, and unexpected behavior using structured debugging methodology. + +## Context + +Unity Grant Manager is a government grant management platform built on: +- **Framework**: ABP Framework 9.1.3 on .NET 9.0 +- **Architecture**: Modular monolith with DDD layered structure +- **Multi-Tenancy**: Database-per-tenant with dual DbContext +- **Stack**: PostgreSQL, EF Core, Redis, RabbitMQ, Keycloak +- **Logging**: Serilog with structured logging +- **Profiling**: MiniProfiler for development + +**Essential Reading:** +- [copilot-instructions.md](../copilot-instructions.md): Common mistakes and patterns +- [ARCHITECTURE.md](../../ARCHITECTURE.md): Module communication and data flow + +## Your Role + +You diagnose and fix issues methodically: +- ✅ Reproduce and isolate bugs +- ✅ Analyze logs, stack traces, and database state +- ✅ Identify root causes across ABP layers and modules +- ✅ Propose minimal, targeted fixes following ABP conventions +- ✅ Write regression tests to prevent recurrence +- ✅ Consider multi-tenancy implications + +## Debugging Methodology + +### 1. Reproduce +- Confirm the issue with exact conditions +- Identify the affected tenant (if multi-tenant issue) +- Check which ABP layer the error originates from + +### 2. Isolate +- Trace the request flow: Browser → Controller → AppService → Domain → Repository → Database +- Check Serilog logs for error details and correlation IDs +- Verify tenant context is correct during the operation +- Check if the issue is tenant-specific or global + +### 3. Diagnose Common ABP Issues +- **Missing `virtual` keyword**: Methods not being intercepted by ABP +- **Wrong DbContext**: Tenant data in host context or vice versa +- **Manual TenantId filter**: Overriding ABP's automatic filtering +- **Entity in DTO boundary**: Entities exposed from application services +- **Missing `[Authorize]`**: Unprotected service methods +- **Event handler not registered**: Distributed events not being consumed +- **AutoMapper misconfiguration**: Missing or incorrect mapping profiles +- **EF Core query issues**: N+1 queries, missing includes, wrong tracking + +### 4. Fix +- Apply minimal fix following ABP conventions +- Verify all existing tests still pass +- Write regression test for the specific bug + +### 5. Verify +- Run the full test suite +- Test in the affected tenant context +- Confirm the fix doesn't introduce new issues + +## Output Format + +1. **Diagnosis**: Root cause analysis with evidence +2. **Fix**: Code changes with explanation +3. **Regression Test**: Test that would have caught this issue +4. **Prevention**: Recommendations to avoid similar issues diff --git a/applications/Unity.GrantManager/.github/agents/reviewer.agent.md b/applications/Unity.GrantManager/.github/agents/reviewer.agent.md new file mode 100644 index 0000000000..0f61023184 --- /dev/null +++ b/applications/Unity.GrantManager/.github/agents/reviewer.agent.md @@ -0,0 +1,70 @@ +--- +description: "Code reviewer ensuring ABP Framework compliance, security, and quality standards." +--- + +# Reviewer + +You are a senior code reviewer for the Unity Grant Manager application. You review code changes for compliance with ABP Framework conventions, DDD principles, security requirements, performance, and project standards. + +## Context + +Unity Grant Manager is a government grant management platform built on: +- **Framework**: ABP Framework 9.1.3 on .NET 9.0 +- **Architecture**: Modular monolith with DDD layered structure +- **Multi-Tenancy**: Database-per-tenant with dual DbContext +- **Testing**: xUnit + Shouldly + +**Essential Reading:** +- [copilot-instructions.md](../copilot-instructions.md): Comprehensive development guidelines +- [CONTRIBUTING.md](../../CONTRIBUTING.md): Coding conventions and common pitfalls +- [code-review.instructions.md](../instructions/code-review.instructions.md): Review standards + +## Your Role + +You review code changes thoroughly: +- ✅ Verify ABP Framework compliance (base classes, virtual methods, DTOs, naming) +- ✅ Check architectural layer boundaries and dependency direction +- ✅ Validate multi-tenancy patterns and data isolation +- ✅ Assess security: authorization, input validation, secrets +- ✅ Review test coverage and quality +- ✅ Identify performance concerns +- ❌ Do NOT make code changes — only provide review feedback + +## Review Methodology + +### 1. Architecture Compliance +- Correct ABP base class inheritance +- Layer boundary respect (Domain ← Application ← Web) +- Multi-tenancy: `IMultiTenant`, correct DbContext, no manual TenantId filtering + +### 2. Code Quality +- All public methods are `virtual` +- Nullable reference types handled correctly +- Async/await used properly with `Async` suffix +- `BusinessException` used for domain errors +- No entities exposed from application services + +### 3. Security +- `[Authorize]` attributes on all mutating operations +- No secrets in code +- Input validation at service boundaries +- Parameterized queries only + +### 4. Testing +- Tests follow `Should_[Expected]_[Scenario]` naming +- Shouldly assertions used exclusively +- Critical paths have coverage +- Multi-tenancy isolation tested + +### 5. Frontend (if applicable) +- IIFE wrapping for JavaScript +- ABP localization for user-facing strings +- ABP dynamic proxies instead of manual AJAX +- DataTable reload after CRUD operations + +## Output Format + +Organize findings by severity: +- 🔴 **Critical**: Security vulnerabilities, data leaks, architectural violations +- 🟡 **Important**: Missing tests, convention violations, performance issues +- 🟢 **Suggestion**: Style improvements, refactoring opportunities diff --git a/applications/Unity.GrantManager/.github/agents/software-engineer.agent.md b/applications/Unity.GrantManager/.github/agents/software-engineer.agent.md new file mode 100644 index 0000000000..e7df1d5ce9 --- /dev/null +++ b/applications/Unity.GrantManager/.github/agents/software-engineer.agent.md @@ -0,0 +1,52 @@ +--- +description: "Expert .NET/ABP software engineer implementing features with clean, tested, production-ready code." +--- + +# Software Engineer + +You are an expert .NET software engineer specializing in ABP Framework 9.1.3 development for the Unity Grant Manager application. You implement features, fix bugs, and write production-ready code following DDD principles and ABP conventions. + +## Context + +Unity Grant Manager is a government grant management platform built on: +- **Framework**: ABP Framework 9.1.3 on .NET 9.0 +- **Architecture**: Modular monolith with DDD layered structure +- **Multi-Tenancy**: Database-per-tenant with dual DbContext +- **Stack**: PostgreSQL, EF Core, Redis, RabbitMQ, Keycloak +- **Frontend**: Razor Pages, jQuery, Bootstrap 5, DataTables.net 2.x +- **Testing**: xUnit + Shouldly + +**Essential Reading:** +- [ARCHITECTURE.md](../../ARCHITECTURE.md): System architecture and module dependencies +- [CONTRIBUTING.md](../../CONTRIBUTING.md): Coding conventions and ABP patterns +- [copilot-instructions.md](../copilot-instructions.md): Comprehensive development guidelines + +## Your Role + +You implement features following ABP conventions: +- ✅ Write clean, well-tested C# code with proper ABP base classes +- ✅ Follow DDD layered architecture with strict dependency rules +- ✅ Ensure all public methods are `virtual` for extensibility +- ✅ Return DTOs from application services, never entities +- ✅ Apply `[Authorize]` attributes and implement `IMultiTenant` where appropriate +- ✅ Write xUnit tests with Shouldly assertions +- ✅ Handle multi-tenancy correctly with proper DbContext selection + +## Implementation Guidelines + +1. **Check existing patterns** in the codebase for consistency +2. **Follow the layer order**: Domain → EF Core → Application → HttpApi → Web +3. **Write tests** for all business logic and application services +4. **Use ABP utilities**: `BusinessException`, `Check.*`, `ObjectMapper`, `GuidGenerator` +5. **Prefer generic repositories** unless custom queries are genuinely needed +6. **Use distributed events** for cross-module communication via RabbitMQ + +## Quality Checklist + +Before completing any implementation: +- [ ] All public methods are `virtual` +- [ ] Application services return DTOs only +- [ ] Tests pass (existing and new) +- [ ] Multi-tenancy handled correctly +- [ ] Authorization applied +- [ ] Nullable annotations correct diff --git a/applications/Unity.GrantManager/.github/instructions/code-review.instructions.md b/applications/Unity.GrantManager/.github/instructions/code-review.instructions.md new file mode 100644 index 0000000000..2a8b63396c --- /dev/null +++ b/applications/Unity.GrantManager/.github/instructions/code-review.instructions.md @@ -0,0 +1,59 @@ +--- +applyTo: "**/*" +description: "Code review standards for Unity Grant Manager" +--- + +# Code Review Standards + +Apply the repository-wide guidance from `../copilot-instructions.md` when reviewing code. + +## ABP Framework Compliance + +- Verify correct use of ABP base classes and inheritance +- Ensure all public methods are `virtual` for ABP extensibility +- Confirm application services return DTOs, never entities +- Check domain services use `Manager` suffix and contain business logic +- Verify `[Authorize]` attributes are applied with correct permission names + +## Architecture & Layer Boundaries + +- Ensure strict layer dependency direction (Domain ← Application ← Web) +- Verify Domain layer has no dependencies on Application or Infrastructure +- Confirm Application.Contracts depends only on Domain.Shared +- Check that EF Core layer depends only on Domain + +## Multi-Tenancy + +- Verify tenant-scoped entities implement `IMultiTenant` +- Confirm correct DbContext usage (host vs tenant) +- Ensure no manual TenantId filtering +- Check for cross-tenant data leaks + +## Code Quality + +- Nullable reference types handled correctly — no suppression without justification +- Async/await used consistently with `Async` suffix on method names +- Error handling uses `BusinessException` with meaningful error codes +- No hardcoded strings — use localization and constants + +## Testing + +- Verify tests follow `Should_[Expected]_[Scenario]` naming +- Check that Shouldly assertions are used, not `Assert.*` +- Ensure critical paths have test coverage +- Verify multi-tenancy isolation tests for tenant-scoped features + +## Security + +- No secrets or connection strings in code +- Input validation at application service boundary +- Authorization checks present on all mutating operations +- No raw SQL with string concatenation + +## Frontend + +- JavaScript wrapped in IIFE pattern +- ABP localization used for all user-facing strings +- ABP dynamic proxies used instead of manual AJAX +- DataTable reload called after CRUD operations +- Modal Manager used for dialog management diff --git a/applications/Unity.GrantManager/.github/instructions/csharp.instructions.md b/applications/Unity.GrantManager/.github/instructions/csharp.instructions.md new file mode 100644 index 0000000000..1366fa2738 --- /dev/null +++ b/applications/Unity.GrantManager/.github/instructions/csharp.instructions.md @@ -0,0 +1,80 @@ + +--- +applyTo: "**/*.cs" +description: "C# and .NET 9 development standards for ABP Framework 9.1.3" +--- + +# C# Development Standards + +Apply the repository-wide guidance from `../copilot-instructions.md` to all C# code. + +## Language & Framework + +- Target .NET 9.0 with C# 12 features (primary constructors, collection expressions, etc.) +- Nullable reference types are ENABLED project-wide — always declare nullability explicitly +- Use `is null` or `is not null` instead of `== null` or `!= null` +- Use `null!` only when DI guarantees non-null (e.g., `DbSet` properties) + +## ABP Base Classes + +- Application Services: Inherit `ApplicationService`, implement interface from Application.Contracts +- Domain Services: Inherit `DomainService`, use `Manager` suffix +- Entities: Inherit `FullAuditedAggregateRoot` or `AuditedAggregateRoot` +- API Controllers: Inherit `AbpController` +- Repositories: Use `IRepository` by default; custom only when needed + +## Naming Conventions + +- Follow PascalCase for public members, types, and methods +- Use camelCase for private fields and local variables +- Prefix interface names with `I` +- Domain Services: `*Manager` suffix (e.g., `AssessmentManager`) +- Application Services: `*AppService` suffix (e.g., `ApplicationAppService`) +- DTOs: Descriptive suffixes (`CreateApplicationDto`, `UpdateApplicationDto`, `ApplicationDto`) +- Event Transfer Objects: `*Eto` suffix for distributed events + +## Method Conventions + +- All public methods MUST be `virtual` for ABP extensibility +- Async methods MUST have `Async` suffix and use `async/await` +- Use `protected virtual` instead of `private` for helper methods +- Always specify access modifiers explicitly + +## Code Style + +- 4 spaces indentation, no tabs +- Always use braces, even for single-line statements +- Apply code-formatting style defined in `.editorconfig` +- Use `nameof` instead of string literals when referring to member names +- Prefer pattern matching and switch expressions where appropriate + +## DTOs vs Entities + +- Application services MUST accept and return DTOs only, never entities +- Use `ObjectMapper` (AutoMapper) to map between entities and DTOs +- Define mapping profiles in `*AutoMapperProfile` class in Application project + +## Authorization + +- Apply `[Authorize(PermissionName)]` attributes on application service methods +- Define permissions in `*Permissions` static class in Domain.Shared project + +## Multi-Tenancy + +- Tenant entities MUST implement `IMultiTenant` interface +- NEVER manually filter by `TenantId` — ABP handles this automatically +- Use `GrantTenantDbContext` for tenant data, `GrantManagerDbContext` for host data + +## Error Handling + +- Use `BusinessException` for domain-level errors with error codes +- Catch specific exception types, not generic `Exception` +- Ensure XML doc comments are created for public APIs + +## Common Mistakes to Avoid + +- Don't expose entities from application services — always return DTOs +- Don't put business logic in application services — use domain services +- Don't create custom repositories unnecessarily — use generic `IRepository` first +- Don't mix host and tenant data in same DbContext +- Don't ignore nullable warnings — fix them properly diff --git a/applications/Unity.GrantManager/.github/instructions/documentation.instructions.md b/applications/Unity.GrantManager/.github/instructions/documentation.instructions.md new file mode 100644 index 0000000000..14abe19258 --- /dev/null +++ b/applications/Unity.GrantManager/.github/instructions/documentation.instructions.md @@ -0,0 +1,43 @@ +--- +applyTo: "**/*.cs,**/*.md" +description: "Documentation standards for Unity Grant Manager" +--- + +# Documentation Standards + +Apply the repository-wide guidance from `../copilot-instructions.md` to all documentation. + +## XML Documentation Comments + +- Create XML doc comments for all public APIs, classes, and interfaces +- Include ``, ``, ``, and `` tags as appropriate +- When applicable, include `` and `` blocks for complex APIs +- Document business rules and domain-specific behavior in entity and domain service comments + +## Markdown Documentation + +- Use clear headings with proper hierarchy (H1 → H2 → H3) +- Include Mermaid diagrams for architectural and flow documentation +- Keep documentation close to the code it describes +- Update ARCHITECTURE.md when making structural changes +- Update CONTRIBUTING.md when changing conventions or patterns + +## Code Comments + +- Comment the "why", not the "what" — code should be self-documenting +- Document complex business rules, non-obvious design decisions, and workarounds +- Add TODO comments with context for deferred work +- Reference ABP documentation links for framework-specific patterns + +## API Documentation + +- Document endpoint behavior, expected inputs, and response shapes +- Include authorization requirements and permission names +- Document error codes and exception scenarios +- Keep Swagger/OpenAPI annotations up to date + +## Localization + +- All user-facing strings must use localization keys +- Add new keys to `Localization/GrantManager/en.json` +- Use descriptive, hierarchical key names (e.g., `Menu:Applications`, `Permissions:Edit`) diff --git a/applications/Unity.GrantManager/.github/instructions/javascript.instructions.md b/applications/Unity.GrantManager/.github/instructions/javascript.instructions.md new file mode 100644 index 0000000000..b8e7a60e3f --- /dev/null +++ b/applications/Unity.GrantManager/.github/instructions/javascript.instructions.md @@ -0,0 +1,54 @@ +--- +applyTo: "**/*.js" +description: "JavaScript development standards for ABP Framework frontend patterns" +--- + +# JavaScript Development Standards + +Apply the repository-wide guidance from `../copilot-instructions.md` to all JavaScript code. + +## General Patterns + +- Wrap all page scripts in IIFE: `(function ($) { ... })(jQuery);` +- Never create global JavaScript variables +- Use `var l = abp.localization.getResource('GrantManager');` for all user-facing text +- Use ABP's dynamic JavaScript API client proxies instead of manual AJAX + +## ABP JavaScript Utilities + +- Notifications: `abp.notify.success()`, `.error()`, `.warn()`, `.info()` +- Confirmation: `abp.message.confirm()` for destructive actions +- Authorization: `abp.auth.isGranted()` for permission checks +- Busy indicators: `abp.ui.setBusy()` / `abp.ui.clearBusy()` +- Localization: `l('LocalizationKey')` — never hardcode user-facing strings + +## DataTables Integration + +- Use DataTables.net 2.x with Bootstrap 5 integration (`datatables.net-bs5`) +- Always wrap configuration with `abp.libs.datatables.normalizeConfiguration()` +- Use `abp.libs.datatables.createAjax()` for server-side pagination +- Use `rowAction` for action buttons with `abp.auth.isGranted()` visibility checks +- Use `dataFormat` property for automatic date/boolean formatting +- Always call `dataTable.ajax.reload()` after CRUD operations + +## Modal Manager + +- Use `abp.ModalManager` for all modal dialogs +- Configure with `viewUrl`, `scriptUrl`, and `modalClass` +- Implement `onResult()` callback to reload DataTable after save +- Modal script classes: register in `abp.modals.*` namespace +- Return `NoContent()` from Razor Page handler to close modal + +## DOM Auto-Initialization + +- ABP auto-initializes: tooltips, popovers, datepickers, AJAX forms, autocomplete selects +- Use `data-bs-toggle="tooltip"` for tooltips +- Use `class="auto-complete-select"` with `data-autocomplete-*` attributes for lookups +- Use `data-ajaxForm="true"` for AJAX form submission + +## Client-Side Package Management + +- Add NPM packages to `package.json`, prefer `@abp/*` packages +- Configure `abp.resourcemapping.js` to map from `node_modules` to `wwwroot/libs` +- Run `abp install-libs` to copy resources +- Add to bundle contributor in `Unity.Theme.UX2` module diff --git a/applications/Unity.GrantManager/.github/instructions/performance.instructions.md b/applications/Unity.GrantManager/.github/instructions/performance.instructions.md new file mode 100644 index 0000000000..fb20b1ed7b --- /dev/null +++ b/applications/Unity.GrantManager/.github/instructions/performance.instructions.md @@ -0,0 +1,52 @@ +--- +applyTo: "**/*.cs" +description: "Performance optimization guidelines for Unity Grant Manager" +--- + +# Performance Standards + +Apply the repository-wide guidance from `../copilot-instructions.md` to all performance-sensitive code. + +## Entity Framework Core + +- Use async methods for all database operations (`ToListAsync`, `FirstOrDefaultAsync`, etc.) +- Avoid N+1 queries — use `Include()` and `ThenInclude()` for eager loading when needed +- Add database indexes for frequently queried columns and foreign keys +- Use projections (`.Select()`) when you don't need full entities +- Avoid loading entire collections into memory — use server-side pagination +- Configure entity properties with appropriate max lengths in fluent API + +## Caching Strategy + +- Use Redis distributed cache for frequently accessed, rarely changing data +- Follow ABP's caching patterns with `IDistributedCache` +- Set appropriate expiration times based on data volatility +- Invalidate cache entries when underlying data changes + +## Query Optimization + +- Use `IQueryable` to build queries and let EF Core translate to SQL +- Avoid `ToList()` before applying filters — filter at the database level +- Use `AsNoTracking()` for read-only queries +- Implement pagination using ABP's `PagedAndSortedResultRequestDto` + +## Background Jobs + +- Use Quartz.NET for long-running or scheduled operations +- Don't block HTTP requests with expensive computations +- Use distributed events (RabbitMQ) for cross-module async processing +- Keep background job execution time reasonable with proper error handling + +## Frontend Performance + +- Use ABP's bundling and minification system for client-side assets +- Implement server-side pagination in DataTables +- Lazy-load non-critical resources +- Use ABP's dynamic JavaScript proxies — they handle serialization efficiently + +## Monitoring + +- Use Serilog structured logging for performance-relevant events +- Use MiniProfiler for development-time query profiling +- Log slow queries and long-running operations +- Monitor memory usage in multi-tenant scenarios diff --git a/applications/Unity.GrantManager/.github/instructions/security.instructions.md b/applications/Unity.GrantManager/.github/instructions/security.instructions.md new file mode 100644 index 0000000000..176e3ba886 --- /dev/null +++ b/applications/Unity.GrantManager/.github/instructions/security.instructions.md @@ -0,0 +1,50 @@ +--- +applyTo: "**/*.cs,**/*.cshtml,**/*.js" +description: "Security best practices for Unity Grant Manager" +--- + +# Security Standards + +Apply the repository-wide guidance from `../copilot-instructions.md` to all security-sensitive code. + +## Authorization + +- Apply `[Authorize(PermissionName)]` attributes on all application service methods +- Define permissions in `*Permissions` static class in Domain.Shared project +- Use `abp.auth.isGranted()` in JavaScript for UI permission checks +- Never rely solely on UI-level permission hiding — always enforce server-side + +## Multi-Tenancy Security + +- Never manually filter by `TenantId` — ABP handles tenant isolation automatically +- Ensure tenant-scoped entities implement `IMultiTenant` +- Test cross-tenant data isolation explicitly +- Use `GrantTenantDbContext` for tenant data, `GrantManagerDbContext` for host data +- Be cautious with `[IgnoreMultiTenancy]` — understand the security implications + +## Input Validation + +- Validate all inputs at the application service boundary using data annotations or FluentValidation +- Use ABP's `Check.*` methods for domain-level validation (e.g., `Check.NotNullOrWhiteSpace`) +- Sanitize user inputs before storage — prevent XSS and injection attacks +- Use parameterized queries — never concatenate user input into SQL + +## Secrets Management + +- Never commit secrets, connection strings, or API keys to source code +- Use environment variables or secure configuration providers +- Reference `.env.example` for required environment variables +- Store sensitive configuration in Keycloak, Azure Key Vault, or equivalent + +## Authentication + +- Authentication is handled via Keycloak (OpenID Connect) +- Do not implement custom authentication — use ABP's identity infrastructure +- Ensure all API endpoints require authentication unless explicitly public + +## Data Protection + +- Use Redis-backed data protection for key storage in distributed deployments +- Encrypt sensitive data at rest when required by compliance +- Follow government security standards (BC Government policies) +- Audit logging is enabled via ABP — ensure sensitive operations are captured diff --git a/applications/Unity.GrantManager/.github/instructions/testing.instructions.md b/applications/Unity.GrantManager/.github/instructions/testing.instructions.md new file mode 100644 index 0000000000..e0a1568132 --- /dev/null +++ b/applications/Unity.GrantManager/.github/instructions/testing.instructions.md @@ -0,0 +1,55 @@ +--- +applyTo: "**/*Tests*/**/*.cs,**/*Test*/**/*.cs" +description: "Testing standards using xUnit and Shouldly for ABP Framework" +--- + +# Testing Standards + +Apply the repository-wide guidance from `../copilot-instructions.md` to all test code. + +## Framework & Libraries + +- Test framework: xUnit +- Assertion library: Shouldly (fluent assertions) +- Use `[Fact]` for single tests, `[Theory]` with `[InlineData]` for parameterized tests + +## Test Class Conventions + +- Suffix test classes with `_Tests` (e.g., `ApplicationAppService_Tests`) +- Test method naming: `Should_[Expected]_[Scenario]` +- Follow Arrange-Act-Assert pattern consistently +- Do not emit "Arrange", "Act", or "Assert" comments in generated tests + +## ABP Test Base Classes + +- Application service tests: Inherit `GrantManagerApplicationTestBase` +- Domain tests: Inherit `GrantManagerDomainTestBase` +- Web tests: Inherit `GrantManagerWebTestBase` + +## Shouldly Assertions + +- Use Shouldly fluent assertions exclusively — never use `Assert.*` methods +- `result.ShouldNotBeNull()` — existence checks +- `result.Title.ShouldBe("Expected")` — equality +- `list.ShouldContain(x => x.Id == id)` — collection membership +- `count.ShouldBeGreaterThan(0)` — numeric comparisons +- `await Should.ThrowAsync(...)` — exception testing + +## Multi-Tenancy Testing + +- Test tenant data isolation using `CurrentTenant.Change(tenantId)` +- Verify that data created in one tenant is not visible in another +- Test both host-level and tenant-level operations + +## Test Data Management + +- Use helper methods for test data creation (e.g., `CreateTestApplicationAsync()`) +- Use static test data constants for well-known IDs +- Keep test data self-contained — each test should set up its own state + +## TDD Workflow + +- Write failing test first (Red) +- Implement minimal code to pass (Green) +- Refactor while keeping tests green (Refactor) +- Run full test suite after each change to catch regressions diff --git a/applications/Unity.GrantManager/.github/skills/code-review/SKILL.md b/applications/Unity.GrantManager/.github/skills/code-review/SKILL.md new file mode 100644 index 0000000000..ea01ae8a60 --- /dev/null +++ b/applications/Unity.GrantManager/.github/skills/code-review/SKILL.md @@ -0,0 +1,37 @@ +--- +name: code-review +description: "Review code changes for ABP Framework compliance and project standards" +--- + +# Code Review + +Review code changes in the Unity Grant Manager for compliance with ABP Framework conventions, DDD principles, security requirements, and project standards. + +Ask for the following if not provided: +- The files or pull request to review +- Specific areas of concern (architecture, security, performance, etc.) + +## Requirements + +- Check ABP Framework compliance: base classes, virtual methods, DTOs, naming conventions +- Verify layer boundary integrity (Domain → Application → Web dependency direction) +- Confirm multi-tenancy patterns: `IMultiTenant`, correct DbContext, no manual TenantId filtering +- Validate authorization: `[Authorize]` attributes with permission names +- Check nullable reference type handling +- Verify test coverage for new/changed code +- Review JavaScript for IIFE wrapping, ABP proxy usage, localization +- Flag security concerns: secrets, injection risks, missing validation +- Assess performance: N+1 queries, missing indexes, unnecessary loading + +## Review Output + +Provide findings organized by severity: +1. **Critical**: Security vulnerabilities, data leaks, architectural violations +2. **Important**: Missing tests, ABP convention violations, performance issues +3. **Suggestion**: Code style improvements, refactoring opportunities + +## References + +- [code-review.instructions.md](../../instructions/code-review.instructions.md) for review standards +- [security.instructions.md](../../instructions/security.instructions.md) for security checklist +- [CONTRIBUTING.md](../../../CONTRIBUTING.md) for project conventions diff --git a/applications/Unity.GrantManager/.github/skills/debug-issue/SKILL.md b/applications/Unity.GrantManager/.github/skills/debug-issue/SKILL.md new file mode 100644 index 0000000000..e4165dde12 --- /dev/null +++ b/applications/Unity.GrantManager/.github/skills/debug-issue/SKILL.md @@ -0,0 +1,46 @@ +--- +name: debug-issue +description: "Diagnose and resolve issues in Unity Grant Manager using structured debugging" +--- + +# Debug Issue + +Systematically diagnose and resolve bugs, errors, and unexpected behavior in the Unity Grant Manager application. + +Ask for the following if not provided: +- Error message or unexpected behavior description +- Steps to reproduce (if known) +- Affected layer or module (Domain, Application, Web, etc.) + +## Requirements + +- Follow a structured debugging methodology: reproduce, isolate, diagnose, fix, verify +- Search for similar patterns in existing codebase before proposing fixes +- Consider multi-tenancy implications — is the issue tenant-specific or global? +- Check ABP Framework conventions — many issues stem from convention violations +- Write a regression test before or alongside the fix +- Ensure the fix doesn't break existing tests + +## Debugging Checklist + +1. **Reproduce**: Confirm the issue and identify exact conditions +2. **Isolate**: Determine the affected layer (Domain, Application, EF Core, Web) +3. **Investigate**: Check logs (Serilog), database state, tenant context +4. **Root Cause**: Identify why the issue occurs — framework misuse, business logic error, data issue +5. **Fix**: Apply minimal, targeted fix following ABP conventions +6. **Test**: Write regression test, run full test suite +7. **Document**: Add comments explaining the fix if the root cause was non-obvious + +## Common ABP Issues + +- Missing `virtual` keyword on overridden methods +- Manual TenantId filtering instead of ABP automatic filtering +- Entity exposed from application service instead of DTO +- Wrong DbContext used (host vs tenant data) +- Missing `[Authorize]` attribute on new service methods +- Distributed event handler not registered + +## References + +- [copilot-instructions.md](../../copilot-instructions.md) for common mistakes +- [ARCHITECTURE.md](../../../ARCHITECTURE.md) for module communication patterns diff --git a/applications/Unity.GrantManager/.github/skills/generate-docs/SKILL.md b/applications/Unity.GrantManager/.github/skills/generate-docs/SKILL.md new file mode 100644 index 0000000000..2f0f9a2a8f --- /dev/null +++ b/applications/Unity.GrantManager/.github/skills/generate-docs/SKILL.md @@ -0,0 +1,34 @@ +--- +name: generate-docs +description: "Generate documentation for Unity Grant Manager components and APIs" +--- + +# Generate Documentation + +Generate or update documentation for Unity Grant Manager components, APIs, and architectural decisions. + +Ask for the following if not provided: +- The component or feature to document +- Documentation type (XML comments, markdown, API docs, architecture) + +## Requirements + +- Follow the project's documentation standards +- Use XML doc comments for all public C# APIs with ``, ``, ``, `` tags +- Include `` blocks for complex APIs +- Use Mermaid diagrams for architectural and data flow documentation +- Keep documentation close to the code it describes +- Use localization keys for user-facing content — add keys to `Localization/GrantManager/en.json` + +## Documentation Types + +- **XML Comments**: Public classes, methods, interfaces, and DTOs +- **Architecture Docs**: Update ARCHITECTURE.md for structural changes with Mermaid diagrams +- **API Documentation**: Swagger annotations, endpoint behavior, authorization requirements +- **Module Documentation**: Describe module purpose, integration points, and communication patterns +- **Migration Guides**: Document breaking changes and upgrade steps + +## References + +- [documentation.instructions.md](../../instructions/documentation.instructions.md) for documentation standards +- [ARCHITECTURE.md](../../../ARCHITECTURE.md) for existing architectural documentation diff --git a/applications/Unity.GrantManager/.github/skills/refactor-code/SKILL.md b/applications/Unity.GrantManager/.github/skills/refactor-code/SKILL.md new file mode 100644 index 0000000000..d499355594 --- /dev/null +++ b/applications/Unity.GrantManager/.github/skills/refactor-code/SKILL.md @@ -0,0 +1,38 @@ +--- +name: refactor-code +description: "Refactor code following ABP Framework best practices and DDD principles" +--- + +# Refactor Code + +Refactor existing Unity Grant Manager code to improve quality, maintainability, and alignment with ABP Framework conventions and DDD principles. + +Ask for the following if not provided: +- The code or files to refactor +- The refactoring goal (e.g., extract domain service, improve testability, fix layer violations) + +## Requirements + +- Preserve existing behavior — refactoring must not change functionality +- Ensure all existing tests pass after refactoring +- Follow ABP patterns: virtual methods, proper base classes, DTOs in application layer +- Respect layer boundaries — move logic to the correct architectural layer +- Extract business logic from application services into domain services (`*Manager`) +- Replace custom repositories with generic `IRepository` when possible +- Improve nullable reference type annotations +- Simplify complex LINQ queries and improve readability +- Remove code duplication while maintaining ABP conventions + +## Common Refactoring Patterns + +- **Extract Domain Service**: Move business logic from AppService to Manager class +- **Introduce DTOs**: Replace entity exposure with proper DTO mapping +- **Fix Layer Violations**: Move code to correct architectural layer +- **Improve Testability**: Break dependencies, introduce interfaces +- **Multi-Tenancy Compliance**: Add `IMultiTenant`, fix DbContext usage +- **Modernize C#**: Apply C# 12 features where appropriate + +## References + +- [csharp.instructions.md](../../instructions/csharp.instructions.md) for C# standards +- [ARCHITECTURE.md](../../../ARCHITECTURE.md) for layer dependencies diff --git a/applications/Unity.GrantManager/.github/skills/setup-component/SKILL.md b/applications/Unity.GrantManager/.github/skills/setup-component/SKILL.md new file mode 100644 index 0000000000..dad0692796 --- /dev/null +++ b/applications/Unity.GrantManager/.github/skills/setup-component/SKILL.md @@ -0,0 +1,46 @@ +--- +name: setup-component +description: "Scaffold a new ABP Framework component following DDD layered architecture" +--- + +# Setup Component + +Create a new ABP Framework component (entity, application service, API, and UI) following the Unity Grant Manager's DDD layered architecture and ABP conventions. + +Ask for the following if not provided: +- Component name (e.g., "Assessment", "PaymentRequest") +- Whether it is tenant-scoped or host-scoped +- Required properties and their types +- Whether it needs a domain service (complex business logic) + +## Requirements + +- Follow ABP's layered architecture: Domain → Application → HttpApi → Web +- Use the existing project structure and naming conventions +- Inherit from proper ABP base classes (`FullAuditedAggregateRoot`, `ApplicationService`, `AbpController`) +- All public methods must be `virtual` +- Application services return DTOs only, never entities +- Implement `IMultiTenant` for tenant-scoped entities +- Apply `[Authorize]` attributes with permission names +- Configure entity in the correct DbContext (`GrantManagerDbContext` or `GrantTenantDbContext`) +- Add constants to Domain.Shared project +- Create AutoMapper profile in Application project +- Generate corresponding xUnit test class with Shouldly assertions +- Add localization keys to resource files + +## Layer Checklist + +1. **Domain.Shared**: Constants, enums +2. **Domain**: Entity, repository interface (if custom), domain service (if needed) +3. **Application.Contracts**: DTOs, service interface +4. **Application**: Service implementation, AutoMapper profile +5. **EntityFrameworkCore**: Entity configuration, DbSet, migration +6. **HttpApi**: API controller +7. **Web**: Razor Pages, JavaScript +8. **Tests**: Application service tests + +## References + +- [ARCHITECTURE.md](../../../ARCHITECTURE.md) for layer dependencies +- [CONTRIBUTING.md](../../../CONTRIBUTING.md) for coding conventions +- [copilot-instructions.md](../../copilot-instructions.md) for ABP patterns diff --git a/applications/Unity.GrantManager/.github/skills/write-tests/SKILL.md b/applications/Unity.GrantManager/.github/skills/write-tests/SKILL.md new file mode 100644 index 0000000000..ec26eb20d6 --- /dev/null +++ b/applications/Unity.GrantManager/.github/skills/write-tests/SKILL.md @@ -0,0 +1,41 @@ +--- +name: write-tests +description: "Generate xUnit tests with Shouldly assertions following ABP testing conventions" +--- + +# Write Tests + +Generate comprehensive test suites for Unity Grant Manager components using xUnit and Shouldly, following the project's TDD conventions and ABP testing patterns. + +Ask for the following if not provided: +- The class or feature to test +- Test scope (unit, integration, or both) +- Specific scenarios or edge cases to cover + +## Requirements + +- Use xUnit with `[Fact]` and `[Theory]` attributes +- Use Shouldly for all assertions — never use `Assert.*` methods +- Follow `Should_[Expected]_[Scenario]` naming convention +- Inherit from the correct ABP test base class: + - `GrantManagerApplicationTestBase` for application service tests + - `GrantManagerDomainTestBase` for domain logic tests + - `GrantManagerWebTestBase` for web layer tests +- Follow Arrange-Act-Assert pattern without section comments +- Test multi-tenancy isolation when entities implement `IMultiTenant` +- Test authorization by verifying permission enforcement +- Use helper methods for test data creation +- Test both happy paths and error scenarios +- Verify persistence by reading back from repository after mutations + +## Test Categories + +- **Entity tests**: Constructor validation, business methods, state transitions +- **Domain service tests**: Business rule enforcement, validation logic +- **Application service tests**: CRUD operations, DTO mapping, authorization +- **Integration tests**: End-to-end workflows, multi-tenancy isolation + +## References + +- [testing.instructions.md](../../instructions/testing.instructions.md) for testing standards +- [copilot-instructions.md](../../copilot-instructions.md) for ABP test patterns From 7c8279c08b46ae8262b3f06d392772f010ade21f Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:41:20 -0700 Subject: [PATCH 5/6] AB#31037 - Update copilot instructions --- .../.github/agents/architect.agent.md | 1 + .../.github/agents/debugger.agent.md | 1 + .../.github/agents/reviewer.agent.md | 1 + .../.github/agents/software-engineer.agent.md | 1 + .../.github/agents/tdd.agent.md | 1 + .../.github/copilot-instructions.md | 82 ++++++++++++++++--- .../instructions/code-review.instructions.md | 15 +++- .../instructions/csharp.instructions.md | 40 ++++++++- .../documentation.instructions.md | 2 + 9 files changed, 129 insertions(+), 15 deletions(-) diff --git a/applications/Unity.GrantManager/.github/agents/architect.agent.md b/applications/Unity.GrantManager/.github/agents/architect.agent.md index f356615c84..e16f218372 100644 --- a/applications/Unity.GrantManager/.github/agents/architect.agent.md +++ b/applications/Unity.GrantManager/.github/agents/architect.agent.md @@ -1,5 +1,6 @@ --- description: "Solution architect analyzing codebase structure, designing features, and ensuring architectural integrity." +tools: ['codebase', 'problems', 'usages', 'fetch', 'githubRepo'] --- # Architect diff --git a/applications/Unity.GrantManager/.github/agents/debugger.agent.md b/applications/Unity.GrantManager/.github/agents/debugger.agent.md index 54019733c0..513f6c5064 100644 --- a/applications/Unity.GrantManager/.github/agents/debugger.agent.md +++ b/applications/Unity.GrantManager/.github/agents/debugger.agent.md @@ -1,5 +1,6 @@ --- description: "Debugging specialist diagnosing and resolving issues in ABP Framework applications." +tools: ['codebase', 'problems', 'usages', 'findTestFiles', 'runTests', 'terminalLastCommand'] --- # Debugger diff --git a/applications/Unity.GrantManager/.github/agents/reviewer.agent.md b/applications/Unity.GrantManager/.github/agents/reviewer.agent.md index 0f61023184..50a2302cd2 100644 --- a/applications/Unity.GrantManager/.github/agents/reviewer.agent.md +++ b/applications/Unity.GrantManager/.github/agents/reviewer.agent.md @@ -1,5 +1,6 @@ --- description: "Code reviewer ensuring ABP Framework compliance, security, and quality standards." +tools: ['codebase', 'problems', 'usages', 'findTestFiles', 'runTests'] --- # Reviewer diff --git a/applications/Unity.GrantManager/.github/agents/software-engineer.agent.md b/applications/Unity.GrantManager/.github/agents/software-engineer.agent.md index e7df1d5ce9..881831fc45 100644 --- a/applications/Unity.GrantManager/.github/agents/software-engineer.agent.md +++ b/applications/Unity.GrantManager/.github/agents/software-engineer.agent.md @@ -1,5 +1,6 @@ --- description: "Expert .NET/ABP software engineer implementing features with clean, tested, production-ready code." +tools: ['codebase', 'problems', 'usages', 'findTestFiles', 'runTests', 'githubRepo'] --- # Software Engineer diff --git a/applications/Unity.GrantManager/.github/agents/tdd.agent.md b/applications/Unity.GrantManager/.github/agents/tdd.agent.md index f16bc78da4..12cbf79b7b 100644 --- a/applications/Unity.GrantManager/.github/agents/tdd.agent.md +++ b/applications/Unity.GrantManager/.github/agents/tdd.agent.md @@ -1,5 +1,6 @@ --- description: 'Expert TDD developer generating high-quality, fully tested, maintainable code for Unity Grant Manager following ABP Framework conventions.' +tools: ['codebase', 'problems', 'usages', 'findTestFiles', 'runTests', 'githubRepo'] --- # TDD Implementation Agent diff --git a/applications/Unity.GrantManager/.github/copilot-instructions.md b/applications/Unity.GrantManager/.github/copilot-instructions.md index c151061894..1ad0539167 100644 --- a/applications/Unity.GrantManager/.github/copilot-instructions.md +++ b/applications/Unity.GrantManager/.github/copilot-instructions.md @@ -33,6 +33,17 @@ This project follows **ABP Framework 9.1.3** architecture and conventions. Alway - API Controllers: Inherit from `AbpController` - Repositories: Use `IRepository` or define custom interface when needed +**ABP Base Class Injected Properties (available in ApplicationService, DomainService, AbpController):** +- `GuidGenerator` — Use to create new entity IDs; never use `Guid.NewGuid()` +- `Clock` — Use `Clock.Now` instead of `DateTime.Now` or `DateTime.UtcNow` +- `CurrentUser` — Access authenticated user info (Id, Name, Email, Roles) +- `CurrentTenant` — Access current tenant context (Id, Name) +- `L` or `L["Key"]` — Localization shortcut +- `ObjectMapper` — AutoMapper-based DTO/entity mapping +- `Logger` — Structured logging via `ILogger` +- `AuthorizationService` — Programmatic authorization checks +- `UnitOfWorkManager` — Manual unit-of-work control when needed + **Naming:** - Domain Services: `*Manager` suffix (e.g., `AssessmentManager`, `PaymentManager`) - Application Services: `*AppService` suffix (e.g., `ApplicationAppService`) @@ -43,6 +54,8 @@ This project follows **ABP Framework 9.1.3** architecture and conventions. Alway - All public methods MUST be `virtual` to allow overriding and extensibility - Async methods MUST have `Async` suffix - Use `protected virtual` instead of `private` for helper methods +- Application service methods: Use simple names (`GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync`, `DeleteAsync`) — do NOT embed entity name (e.g., use `GetAsync` not `GetApplicationAsync`) +- For `UpdateAsync`, pass `id` as separate parameter — do NOT include it inside the DTO **Authorization:** - Apply `[Authorize(PermissionName)]` attributes on application service methods @@ -53,6 +66,33 @@ This project follows **ABP Framework 9.1.3** architecture and conventions. Alway - Use `ObjectMapper` (AutoMapper) to map between entities and DTOs - Define mapping profiles in `*AutoMapperProfile` class in Application project +### Dependency Injection Conventions + +ABP auto-registers services using marker interfaces — do NOT use `services.AddScoped<>()` or `services.AddTransient<>()` manually for ABP services. + +- `ITransientDependency` — Registered as transient (new instance per injection) +- `ISingletonDependency` — Registered as singleton +- `IScopedDependency` — Registered as scoped (per-request) +- ABP application services, domain services, and repositories are auto-registered — no manual registration needed + +### Entity Constructor Conventions + +- Always include a `protected` parameterless constructor for EF Core/ORM deserialization +- Public constructor should accept `Guid id` from `IGuidGenerator` (never call `Guid.NewGuid()` yourself) +- Use ABP's `Check.NotNullOrWhiteSpace()` and `Check.NotNull()` for constructor parameter validation +- Set required properties in the constructor; use internal/private setters to protect invariants + +### Time Handling + +- ALWAYS use `Clock.Now` (from ABP base classes) or inject `IClock` — never use `DateTime.Now` or `DateTime.UtcNow` +- This ensures consistent time handling and testability across the application + +### BusinessException Patterns + +- Use namespaced error codes: `"GrantManager:ApplicationAlreadyExists"` +- Map error codes to localization keys in `Localization/GrantManager/en.json` for user-friendly messages +- Use `.WithData("key", value)` to pass interpolation parameters to localized error messages + ### Multi-Tenancy Patterns **This application uses database-per-tenant isolation:** @@ -81,6 +121,12 @@ private readonly IRepository _applicationRepository; - Implementation goes in EntityFrameworkCore project - Inherit from `EfCoreRepository` +**Repository method conventions:** +- Always pass `CancellationToken` as the last parameter +- Use `includeDetails: true` when you need navigation properties (default is `false`) +- Prefer `GetAsync(id)` over `FindAsync(id)` when entity must exist (throws `EntityNotFoundException`) +- Use `GetListAsync()` / `GetCountAsync()` for pagination; prefer `IQueryable` via `GetQueryableAsync()` for complex queries + ### Domain Events **Local Events (same transaction, same database):** @@ -213,19 +259,29 @@ public class ApplicationApprovedHandler : IDistributedEventHandler` first -❌ **Don't mix host and tenant data in same DbContext** - Separate contexts for isolation -❌ **Don't forget [Authorize] attributes** - Always check permissions -❌ **Don't ignore nullable warnings** - Fix them properly -❌ **Don't use manual AJAX** - Use ABP's dynamic JavaScript proxies -❌ **Don't create global JavaScript variables** - Wrap in IIFE pattern -❌ **Don't hardcode strings in JavaScript** - Use `abp.localization` -❌ **Don't bypass ABP modal manager** - Use `abp.ModalManager` for modals -❌ **Don't forget DataTable reload** - Call `dataTable.ajax.reload()` after CRUD +### Backend Anti-Patterns +❌ **Don't expose entities from application services** — Always return DTOs +❌ **Don't put business logic in application services** — Use domain services (`*Manager`) +❌ **Don't use non-virtual methods** — All public methods must be virtual +❌ **Don't manually filter by TenantId** — ABP does this automatically +❌ **Don't create custom repositories unnecessarily** — Use `IRepository` first +❌ **Don't mix host and tenant data in same DbContext** — Separate contexts for isolation +❌ **Don't forget [Authorize] attributes** — Always check permissions +❌ **Don't ignore nullable warnings** — Fix them properly +❌ **Don't use `DateTime.Now` or `DateTime.UtcNow`** — Use `Clock.Now` or inject `IClock` +❌ **Don't use `Guid.NewGuid()`** — Use `GuidGenerator.Create()` from ABP base classes +❌ **Don't use `services.AddScoped<>()` for ABP services** — Use `ITransientDependency` / `IScopedDependency` marker interfaces +❌ **Don't call application services from other services in the same module** — Extract shared logic to a domain service +❌ **Don't inject `DbContext` directly** — Use repositories for all data access +❌ **Don't embed entity name in application service methods** — Use `GetAsync`, not `GetApplicationAsync` +❌ **Don't put `Id` inside update DTOs** — Pass `id` as a separate parameter to `UpdateAsync` + +### Frontend Anti-Patterns +❌ **Don't use manual AJAX** — Use ABP's dynamic JavaScript proxies +❌ **Don't create global JavaScript variables** — Wrap in IIFE pattern +❌ **Don't hardcode strings in JavaScript** — Use `abp.localization` +❌ **Don't bypass ABP modal manager** — Use `abp.ModalManager` for modals +❌ **Don't forget DataTable reload** — Call `dataTable.ajax.reload()` after CRUD ## Front-End Development Patterns diff --git a/applications/Unity.GrantManager/.github/instructions/code-review.instructions.md b/applications/Unity.GrantManager/.github/instructions/code-review.instructions.md index 2a8b63396c..2b51b82faa 100644 --- a/applications/Unity.GrantManager/.github/instructions/code-review.instructions.md +++ b/applications/Unity.GrantManager/.github/instructions/code-review.instructions.md @@ -14,6 +14,11 @@ Apply the repository-wide guidance from `../copilot-instructions.md` when review - Confirm application services return DTOs, never entities - Check domain services use `Manager` suffix and contain business logic - Verify `[Authorize]` attributes are applied with correct permission names +- Confirm `Clock.Now` used instead of `DateTime.Now` / `DateTime.UtcNow` +- Verify `GuidGenerator.Create()` used instead of `Guid.NewGuid()` +- Check DI uses marker interfaces (`ITransientDependency`), not manual `services.AddScoped<>()` +- Confirm entity constructors include `protected` parameterless constructor for EF Core +- Verify application services don't call other application services in the same module ## Architecture & Layer Boundaries @@ -33,8 +38,9 @@ Apply the repository-wide guidance from `../copilot-instructions.md` when review - Nullable reference types handled correctly — no suppression without justification - Async/await used consistently with `Async` suffix on method names -- Error handling uses `BusinessException` with meaningful error codes +- Error handling uses `BusinessException` with namespaced error codes - No hardcoded strings — use localization and constants +- Repository methods pass `CancellationToken` as last parameter ## Testing @@ -57,3 +63,10 @@ Apply the repository-wide guidance from `../copilot-instructions.md` when review - ABP dynamic proxies used instead of manual AJAX - DataTable reload called after CRUD operations - Modal Manager used for dialog management + +## Review Severity Levels + +Organize findings by severity: +- 🔴 **Critical**: Security vulnerabilities, data leaks, architectural violations, cross-tenant data exposure +- 🟡 **Important**: Missing tests, convention violations, performance issues, missing authorization +- 🟢 **Suggestion**: Style improvements, refactoring opportunities, documentation gaps diff --git a/applications/Unity.GrantManager/.github/instructions/csharp.instructions.md b/applications/Unity.GrantManager/.github/instructions/csharp.instructions.md index 1366fa2738..f26f1fba3d 100644 --- a/applications/Unity.GrantManager/.github/instructions/csharp.instructions.md +++ b/applications/Unity.GrantManager/.github/instructions/csharp.instructions.md @@ -23,6 +23,37 @@ Apply the repository-wide guidance from `../copilot-instructions.md` to all C# c - API Controllers: Inherit `AbpController` - Repositories: Use `IRepository` by default; custom only when needed +### Injected Properties Available in Base Classes + +These properties are pre-injected in `ApplicationService`, `DomainService`, and `AbpController`: + +| Property | Purpose | +|---|---| +| `GuidGenerator` | Create new entity IDs — never use `Guid.NewGuid()` | +| `Clock` | Use `Clock.Now` — never use `DateTime.Now` or `DateTime.UtcNow` | +| `CurrentUser` | Access authenticated user (Id, Name, Email, Roles) | +| `CurrentTenant` | Access current tenant context (Id, Name) | +| `L` / `L["Key"]` | Localization shortcut | +| `ObjectMapper` | AutoMapper-based mapping | +| `Logger` | Structured logging via `ILogger` | +| `AuthorizationService` | Programmatic authorization checks | +| `UnitOfWorkManager` | Manual unit-of-work control | + +## Dependency Injection + +- ABP auto-registers services using marker interfaces — do NOT manually call `services.AddScoped<>()` +- `ITransientDependency` — new instance per injection +- `ISingletonDependency` — single shared instance +- `IScopedDependency` — one per request +- Application services, domain services, and repositories are auto-registered by ABP + +## Entity Constructors + +- Always include a `protected` parameterless constructor for EF Core deserialization +- Public constructor accepts `Guid id` from `IGuidGenerator` — never call `Guid.NewGuid()` +- Use `Check.NotNullOrWhiteSpace()` and `Check.NotNull()` for constructor validation +- Use internal/private setters to protect domain invariants + ## Naming Conventions - Follow PascalCase for public members, types, and methods @@ -67,7 +98,9 @@ Apply the repository-wide guidance from `../copilot-instructions.md` to all C# c ## Error Handling -- Use `BusinessException` for domain-level errors with error codes +- Use `BusinessException` for domain-level errors with namespaced error codes (e.g., `"GrantManager:ApplicationNotFound"`) +- Map error codes to localization keys for user-friendly messages +- Use `.WithData("key", value)` for localized message interpolation - Catch specific exception types, not generic `Exception` - Ensure XML doc comments are created for public APIs @@ -78,3 +111,8 @@ Apply the repository-wide guidance from `../copilot-instructions.md` to all C# c - Don't create custom repositories unnecessarily — use generic `IRepository` first - Don't mix host and tenant data in same DbContext - Don't ignore nullable warnings — fix them properly +- Don't use `DateTime.Now` — use `Clock.Now` or inject `IClock` +- Don't use `Guid.NewGuid()` — use `GuidGenerator.Create()` +- Don't use `services.AddScoped<>()` for ABP services — use marker interfaces +- Don't call application services from within the same module — extract shared logic to a domain service +- Don't embed entity name in app service methods — use `GetAsync`, not `GetApplicationAsync` diff --git a/applications/Unity.GrantManager/.github/instructions/documentation.instructions.md b/applications/Unity.GrantManager/.github/instructions/documentation.instructions.md index 14abe19258..b70b89b875 100644 --- a/applications/Unity.GrantManager/.github/instructions/documentation.instructions.md +++ b/applications/Unity.GrantManager/.github/instructions/documentation.instructions.md @@ -41,3 +41,5 @@ Apply the repository-wide guidance from `../copilot-instructions.md` to all docu - All user-facing strings must use localization keys - Add new keys to `Localization/GrantManager/en.json` - Use descriptive, hierarchical key names (e.g., `Menu:Applications`, `Permissions:Edit`) +- Map `BusinessException` error codes to localization keys for user-friendly error messages +- Keep localization files organized and free of unused keys From 3a870f9e3b0d89edc04549ed8cafca1c15a6c5d7 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:56:25 -0700 Subject: [PATCH 6/6] [AB#32037] Add skills documentation for ABP Framework components --- .../agents/architect.agent.md | 0 .../agents/debugger.agent.md | 0 .../.github => .github}/agents/plan.agent.md | 0 .../agents/reviewer.agent.md | 0 .../agents/software-engineer.agent.md | 0 .../.github => .github}/agents/tdd.agent.md | 0 .../copilot-instructions.md | 0 .../instructions/code-review.instructions.md | 0 .../instructions/csharp.instructions.md | 0 .../documentation.instructions.md | 0 .../instructions/javascript.instructions.md | 0 .../instructions/performance.instructions.md | 0 .../instructions/security.instructions.md | 0 .../instructions/testing.instructions.md | 0 .../.github => .github}/plan-template.md | 0 .../prompts/implement-tdd.prompt.md | 0 .../prompts/plan-from-issue.prompt.md | 0 .../prompts/plan.prompt.md | 0 .github/skills/abp-angular/SKILL.md | 220 ++++++++++++++ .github/skills/abp-application-layer/SKILL.md | 239 ++++++++++++++++ .github/skills/abp-authorization/SKILL.md | 182 ++++++++++++ .github/skills/abp-cli/SKILL.md | 89 ++++++ .github/skills/abp-core/SKILL.md | 190 +++++++++++++ .github/skills/abp-ddd/SKILL.md | 248 ++++++++++++++++ .github/skills/abp-dependency-rules/SKILL.md | 150 ++++++++++ .github/skills/abp-development-flow/SKILL.md | 261 +++++++++++++++++ .github/skills/abp-ef-core/SKILL.md | 262 +++++++++++++++++ .github/skills/abp-infrastructure/SKILL.md | 243 ++++++++++++++++ .github/skills/abp-microservice/SKILL.md | 209 ++++++++++++++ .github/skills/abp-module/SKILL.md | 234 +++++++++++++++ .github/skills/abp-multi-tenancy/SKILL.md | 161 +++++++++++ .github/skills/abp-mvc/SKILL.md | 257 +++++++++++++++++ .github/skills/abp-testing/SKILL.md | 269 ++++++++++++++++++ .../skills/code-review/SKILL.md | 0 .../skills/debug-issue/SKILL.md | 0 .../skills/generate-docs/SKILL.md | 0 .../skills/refactor-code/SKILL.md | 0 .../skills/setup-component/SKILL.md | 0 .../skills/write-tests/SKILL.md | 0 .../Unity.GrantManager/.vscode/settings.json | 7 + 40 files changed, 3221 insertions(+) rename {applications/Unity.GrantManager/.github => .github}/agents/architect.agent.md (100%) rename {applications/Unity.GrantManager/.github => .github}/agents/debugger.agent.md (100%) rename {applications/Unity.GrantManager/.github => .github}/agents/plan.agent.md (100%) rename {applications/Unity.GrantManager/.github => .github}/agents/reviewer.agent.md (100%) rename {applications/Unity.GrantManager/.github => .github}/agents/software-engineer.agent.md (100%) rename {applications/Unity.GrantManager/.github => .github}/agents/tdd.agent.md (100%) rename {applications/Unity.GrantManager/.github => .github}/copilot-instructions.md (100%) rename {applications/Unity.GrantManager/.github => .github}/instructions/code-review.instructions.md (100%) rename {applications/Unity.GrantManager/.github => .github}/instructions/csharp.instructions.md (100%) rename {applications/Unity.GrantManager/.github => .github}/instructions/documentation.instructions.md (100%) rename {applications/Unity.GrantManager/.github => .github}/instructions/javascript.instructions.md (100%) rename {applications/Unity.GrantManager/.github => .github}/instructions/performance.instructions.md (100%) rename {applications/Unity.GrantManager/.github => .github}/instructions/security.instructions.md (100%) rename {applications/Unity.GrantManager/.github => .github}/instructions/testing.instructions.md (100%) rename {applications/Unity.GrantManager/.github => .github}/plan-template.md (100%) rename {applications/Unity.GrantManager/.github => .github}/prompts/implement-tdd.prompt.md (100%) rename {applications/Unity.GrantManager/.github => .github}/prompts/plan-from-issue.prompt.md (100%) rename {applications/Unity.GrantManager/.github => .github}/prompts/plan.prompt.md (100%) create mode 100644 .github/skills/abp-angular/SKILL.md create mode 100644 .github/skills/abp-application-layer/SKILL.md create mode 100644 .github/skills/abp-authorization/SKILL.md create mode 100644 .github/skills/abp-cli/SKILL.md create mode 100644 .github/skills/abp-core/SKILL.md create mode 100644 .github/skills/abp-ddd/SKILL.md create mode 100644 .github/skills/abp-dependency-rules/SKILL.md create mode 100644 .github/skills/abp-development-flow/SKILL.md create mode 100644 .github/skills/abp-ef-core/SKILL.md create mode 100644 .github/skills/abp-infrastructure/SKILL.md create mode 100644 .github/skills/abp-microservice/SKILL.md create mode 100644 .github/skills/abp-module/SKILL.md create mode 100644 .github/skills/abp-multi-tenancy/SKILL.md create mode 100644 .github/skills/abp-mvc/SKILL.md create mode 100644 .github/skills/abp-testing/SKILL.md rename {applications/Unity.GrantManager/.github => .github}/skills/code-review/SKILL.md (100%) rename {applications/Unity.GrantManager/.github => .github}/skills/debug-issue/SKILL.md (100%) rename {applications/Unity.GrantManager/.github => .github}/skills/generate-docs/SKILL.md (100%) rename {applications/Unity.GrantManager/.github => .github}/skills/refactor-code/SKILL.md (100%) rename {applications/Unity.GrantManager/.github => .github}/skills/setup-component/SKILL.md (100%) rename {applications/Unity.GrantManager/.github => .github}/skills/write-tests/SKILL.md (100%) create mode 100644 applications/Unity.GrantManager/.vscode/settings.json diff --git a/applications/Unity.GrantManager/.github/agents/architect.agent.md b/.github/agents/architect.agent.md similarity index 100% rename from applications/Unity.GrantManager/.github/agents/architect.agent.md rename to .github/agents/architect.agent.md diff --git a/applications/Unity.GrantManager/.github/agents/debugger.agent.md b/.github/agents/debugger.agent.md similarity index 100% rename from applications/Unity.GrantManager/.github/agents/debugger.agent.md rename to .github/agents/debugger.agent.md diff --git a/applications/Unity.GrantManager/.github/agents/plan.agent.md b/.github/agents/plan.agent.md similarity index 100% rename from applications/Unity.GrantManager/.github/agents/plan.agent.md rename to .github/agents/plan.agent.md diff --git a/applications/Unity.GrantManager/.github/agents/reviewer.agent.md b/.github/agents/reviewer.agent.md similarity index 100% rename from applications/Unity.GrantManager/.github/agents/reviewer.agent.md rename to .github/agents/reviewer.agent.md diff --git a/applications/Unity.GrantManager/.github/agents/software-engineer.agent.md b/.github/agents/software-engineer.agent.md similarity index 100% rename from applications/Unity.GrantManager/.github/agents/software-engineer.agent.md rename to .github/agents/software-engineer.agent.md diff --git a/applications/Unity.GrantManager/.github/agents/tdd.agent.md b/.github/agents/tdd.agent.md similarity index 100% rename from applications/Unity.GrantManager/.github/agents/tdd.agent.md rename to .github/agents/tdd.agent.md diff --git a/applications/Unity.GrantManager/.github/copilot-instructions.md b/.github/copilot-instructions.md similarity index 100% rename from applications/Unity.GrantManager/.github/copilot-instructions.md rename to .github/copilot-instructions.md diff --git a/applications/Unity.GrantManager/.github/instructions/code-review.instructions.md b/.github/instructions/code-review.instructions.md similarity index 100% rename from applications/Unity.GrantManager/.github/instructions/code-review.instructions.md rename to .github/instructions/code-review.instructions.md diff --git a/applications/Unity.GrantManager/.github/instructions/csharp.instructions.md b/.github/instructions/csharp.instructions.md similarity index 100% rename from applications/Unity.GrantManager/.github/instructions/csharp.instructions.md rename to .github/instructions/csharp.instructions.md diff --git a/applications/Unity.GrantManager/.github/instructions/documentation.instructions.md b/.github/instructions/documentation.instructions.md similarity index 100% rename from applications/Unity.GrantManager/.github/instructions/documentation.instructions.md rename to .github/instructions/documentation.instructions.md diff --git a/applications/Unity.GrantManager/.github/instructions/javascript.instructions.md b/.github/instructions/javascript.instructions.md similarity index 100% rename from applications/Unity.GrantManager/.github/instructions/javascript.instructions.md rename to .github/instructions/javascript.instructions.md diff --git a/applications/Unity.GrantManager/.github/instructions/performance.instructions.md b/.github/instructions/performance.instructions.md similarity index 100% rename from applications/Unity.GrantManager/.github/instructions/performance.instructions.md rename to .github/instructions/performance.instructions.md diff --git a/applications/Unity.GrantManager/.github/instructions/security.instructions.md b/.github/instructions/security.instructions.md similarity index 100% rename from applications/Unity.GrantManager/.github/instructions/security.instructions.md rename to .github/instructions/security.instructions.md diff --git a/applications/Unity.GrantManager/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md similarity index 100% rename from applications/Unity.GrantManager/.github/instructions/testing.instructions.md rename to .github/instructions/testing.instructions.md diff --git a/applications/Unity.GrantManager/.github/plan-template.md b/.github/plan-template.md similarity index 100% rename from applications/Unity.GrantManager/.github/plan-template.md rename to .github/plan-template.md diff --git a/applications/Unity.GrantManager/.github/prompts/implement-tdd.prompt.md b/.github/prompts/implement-tdd.prompt.md similarity index 100% rename from applications/Unity.GrantManager/.github/prompts/implement-tdd.prompt.md rename to .github/prompts/implement-tdd.prompt.md diff --git a/applications/Unity.GrantManager/.github/prompts/plan-from-issue.prompt.md b/.github/prompts/plan-from-issue.prompt.md similarity index 100% rename from applications/Unity.GrantManager/.github/prompts/plan-from-issue.prompt.md rename to .github/prompts/plan-from-issue.prompt.md diff --git a/applications/Unity.GrantManager/.github/prompts/plan.prompt.md b/.github/prompts/plan.prompt.md similarity index 100% rename from applications/Unity.GrantManager/.github/prompts/plan.prompt.md rename to .github/prompts/plan.prompt.md diff --git a/.github/skills/abp-angular/SKILL.md b/.github/skills/abp-angular/SKILL.md new file mode 100644 index 0000000000..3723cc3266 --- /dev/null +++ b/.github/skills/abp-angular/SKILL.md @@ -0,0 +1,220 @@ +--- +name: abp-angular +description: ABP Angular UI patterns - generate-proxy, ListService, PermissionGuard, abpLocalization pipe, ConfirmationService, ToasterService, ConfigStateService. Use when building or reviewing Angular UI components, routing, or service integration in ABP Angular projects. +--- + +# ABP Angular UI + +> **Docs**: https://abp.io/docs/latest/framework/ui/angular/overview + +## Project Structure +``` +src/app/ +├── proxy/ # Auto-generated service proxies +├── shared/ # Shared components, pipes, directives +├── book/ # Feature module +│ ├── book.module.ts +│ ├── book-routing.module.ts +│ ├── book-list/ +│ │ ├── book-list.component.ts +│ │ ├── book-list.component.html +│ │ └── book-list.component.scss +│ └── book-detail/ +``` + +## Generate Service Proxies +```bash +abp generate-proxy -t ng +``` + +This generates typed service classes in `src/app/proxy/`. + +## List Component Pattern +```typescript +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html' +}) +export class BookListComponent implements OnInit { + books = { items: [], totalCount: 0 } as PagedResultDto; + + constructor( + public readonly list: ListService, + private bookService: BookService, + private confirmation: ConfirmationService + ) {} + + ngOnInit(): void { + this.hookToQuery(); + } + + private hookToQuery(): void { + this.list.hookToQuery(query => + this.bookService.getList(query) + ).subscribe(response => { + this.books = response; + }); + } + + create(): void { + // Open create modal + } + + delete(book: BookDto): void { + this.confirmation + .warn('::AreYouSureToDelete', '::AreYouSure') + .subscribe(status => { + if (status === Confirmation.Status.confirm) { + this.bookService.delete(book.id).subscribe(() => this.list.get()); + } + }); + } +} +``` + +## Localization +```typescript +// In component +constructor(private localizationService: LocalizationService) {} + +getText(): string { + return this.localizationService.instant('::Books'); +} +``` + +```html + +

{{ '::Books' | abpLocalization }}

+ + +

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

+``` + +## Authorization + +### Permission Directive +```html + +``` + +### Permission Guard +```typescript +const routes: Routes = [ + { + path: '', + component: BookListComponent, + canActivate: [PermissionGuard], + data: { + requiredPolicy: 'BookStore.Books' + } + } +]; +``` + +### Programmatic Check +```typescript +constructor(private permissionService: PermissionService) {} + +canCreate(): boolean { + return this.permissionService.getGrantedPolicy('BookStore.Books.Create'); +} +``` + +## Forms with Validation +```typescript +@Component({...}) +export class BookFormComponent { + form: FormGroup; + + constructor(private fb: FormBuilder) { + this.buildForm(); + } + + buildForm(): void { + this.form = this.fb.group({ + name: ['', [Validators.required, Validators.maxLength(128)]], + price: [0, [Validators.required, Validators.min(0)]] + }); + } + + save(): void { + if (this.form.invalid) return; + + this.bookService.create(this.form.value).subscribe(() => { + // Handle success + }); + } +} +``` + +```html +
+
+ + +
+ + +
+``` + +## Configuration API +```typescript +constructor(private configService: ConfigStateService) {} + +getCurrentUser(): CurrentUserDto { + return this.configService.getOne('currentUser'); +} + +getSettings(): void { + const setting = this.configService.getSetting('MyApp.MaxItemCount'); +} +``` + +## Modal Service +```typescript +constructor(private modalService: ModalService) {} + +openCreateModal(): void { + const modalRef = this.modalService.open(BookFormComponent, { + size: 'lg' + }); + + modalRef.result.then(result => { + if (result) { + this.list.get(); + } + }); +} +``` + +## Toast Notifications +```typescript +constructor(private toaster: ToasterService) {} + +showSuccess(): void { + this.toaster.success('::BookCreatedSuccessfully', '::Success'); +} + +showError(error: string): void { + this.toaster.error(error, '::Error'); +} +``` + +## Lazy Loading Modules +```typescript +// app-routing.module.ts +const routes: Routes = [ + { + path: 'books', + loadChildren: () => import('./book/book.module').then(m => m.BookModule) + } +]; +``` + +## Theme & Styling +- Use Bootstrap classes +- ABP provides theme variables via CSS custom properties +- Component-specific styles in `.component.scss` diff --git a/.github/skills/abp-application-layer/SKILL.md b/.github/skills/abp-application-layer/SKILL.md new file mode 100644 index 0000000000..d5507c2a7e --- /dev/null +++ b/.github/skills/abp-application-layer/SKILL.md @@ -0,0 +1,239 @@ +--- +name: abp-application-layer +description: ABP Application Services, DTOs, CRUD service, object mapping (Mapperly/AutoMapper), validation, error handling. Use when creating or reviewing application services, DTOs, or working in the Application or Application.Contracts projects. +--- + +# ABP Application Layer Patterns + +> **Docs**: https://abp.io/docs/latest/framework/architecture/domain-driven-design/application-services + +## Anti-Patterns to Avoid + +- **Entity name in method**: use `GetAsync` not `GetBookAsync` +- **ID inside UpdateDto**: pass `id` as a separate parameter, not inside the DTO +- **Calling other app services in the same module**: use domain services or repositories directly +- **Using `IFormFile`/`Stream` in app service**: accept `byte[]` from controllers instead +- **Business logic in app service**: put it in domain entities or domain services + +## Application Service Structure + +### Interface (Application.Contracts) +```csharp +public interface IBookAppService : IApplicationService +{ + Task GetAsync(Guid id); + Task> GetListAsync(GetBookListInput input); + Task CreateAsync(CreateBookDto input); + Task UpdateAsync(Guid id, UpdateBookDto input); + Task DeleteAsync(Guid id); +} +``` + +### Implementation (Application) +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + private readonly IBookRepository _bookRepository; + private readonly BookManager _bookManager; + private readonly BookMapper _bookMapper; + + public BookAppService( + IBookRepository bookRepository, + BookManager bookManager, + BookMapper bookMapper) + { + _bookRepository = bookRepository; + _bookManager = bookManager; + _bookMapper = bookMapper; + } + + public async Task GetAsync(Guid id) + { + var book = await _bookRepository.GetAsync(id); + return _bookMapper.MapToDto(book); + } + + [Authorize(BookStorePermissions.Books.Create)] + public async Task CreateAsync(CreateBookDto input) + { + var book = await _bookManager.CreateAsync(input.Name, input.Price); + await _bookRepository.InsertAsync(book); + return _bookMapper.MapToDto(book); + } + + [Authorize(BookStorePermissions.Books.Edit)] + public async Task UpdateAsync(Guid id, UpdateBookDto input) + { + var book = await _bookRepository.GetAsync(id); + await _bookManager.ChangeNameAsync(book, input.Name); + book.SetPrice(input.Price); + await _bookRepository.UpdateAsync(book); + return _bookMapper.MapToDto(book); + } +} +``` + +## Application Service Best Practices +- Don't repeat entity name in method names (`GetAsync` not `GetBookAsync`) +- Accept/return DTOs only, never entities +- ID not inside UpdateDto - pass separately +- Use custom repositories when you need custom queries, generic repository is fine for simple CRUD +- Call `UpdateAsync` explicitly (don't assume change tracking) +- Don't call other app services in same module +- Don't use `IFormFile`/`Stream` - pass `byte[]` from controllers +- Use base class properties (`Clock`, `CurrentUser`, `GuidGenerator`, `L`) instead of injecting these services + +## DTO Naming Conventions + +| Purpose | Convention | Example | +|---------|------------|---------| +| Query input | `Get{Entity}Input` | `GetBookInput` | +| List query input | `Get{Entity}ListInput` | `GetBookListInput` | +| Create input | `Create{Entity}Dto` | `CreateBookDto` | +| Update input | `Update{Entity}Dto` | `UpdateBookDto` | +| Single entity output | `{Entity}Dto` | `BookDto` | +| List item output | `{Entity}ListItemDto` | `BookListItemDto` | + +## DTO Location +- Define DTOs in `*.Application.Contracts` project +- This allows sharing with clients (Blazor, HttpApi.Client) + +## Validation + +### Data Annotations +```csharp +public class CreateBookDto +{ + [Required] + [StringLength(100, MinimumLength = 3)] + public string Name { get; set; } + + [Range(0, 999.99)] + public decimal Price { get; set; } +} +``` + +### Custom Validation with IValidatableObject +Before adding custom validation, decide if it's a **domain rule** or **application rule**: +- **Domain rule**: Put validation in entity constructor or domain service (enforces business invariants) +- **Application rule**: Use DTO validation (input format, required fields) + +Only use `IValidatableObject` for application-level validation that can't be expressed with data annotations: + +```csharp +public class CreateBookDto : IValidatableObject +{ + public string Name { get; set; } + public string Description { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (Name == Description) + { + yield return new ValidationResult( + "Name and Description cannot be the same!", + new[] { nameof(Name), nameof(Description) } + ); + } + } +} +``` + +### FluentValidation +```csharp +public class CreateBookDtoValidator : AbstractValidator +{ + public CreateBookDtoValidator() + { + RuleFor(x => x.Name).NotEmpty().Length(3, 100); + RuleFor(x => x.Price).GreaterThan(0); + } +} +``` + +## Error Handling + +### Business Exceptions +```csharp +throw new BusinessException("BookStore:010001") + .WithData("BookName", name); +``` + +### Entity Not Found +```csharp +var book = await _bookRepository.FindAsync(id); +if (book == null) +{ + throw new EntityNotFoundException(typeof(Book), id); +} +``` + +### User-Friendly Exceptions +```csharp +throw new UserFriendlyException(L["BookNotAvailable"]); +``` + +### HTTP Status Code Mapping +Status code mapping is **configurable** in ABP (do not rely on a fixed mapping in business logic). + +| Exception | Typical HTTP Status | +|-----------|-------------| +| `AbpValidationException` | 400 | +| `AbpAuthorizationException` | 401/403 | +| `EntityNotFoundException` | 404 | +| `BusinessException` | 403 (but configurable) | +| Other exceptions | 500 | + +## Auto API Controllers +ABP automatically generates API controllers for application services: +- Interface must inherit `IApplicationService` (which already has `[RemoteService]` attribute) +- HTTP methods determined by method name prefix (Get, Create, Update, Delete) +- Use `[RemoteService(false)]` to disable auto API generation for specific methods + +## Object Mapping (Mapperly / AutoMapper) +ABP supports **both Mapperly and AutoMapper** integrations. But the default mapping library is Mapperly. You need to first check the project's active mapping library. +- Prefer the mapping provider already used in the solution (check existing mapping files / loaded modules). +- In mixed solutions, explicitly setting the default provider may be required (see `docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md`). + +### Mapperly (compile-time) +Define mappers as partial classes: + +```csharp +[Mapper] +public partial class BookMapper +{ + public partial BookDto MapToDto(Book book); + public partial List MapToDtoList(List books); +} +``` + +Register in module: +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddSingleton(); +} +``` + +Usage in application service: +```csharp +public class BookAppService : ApplicationService +{ + private readonly BookMapper _bookMapper; + + public BookAppService(BookMapper bookMapper) + { + _bookMapper = bookMapper; + } + + public BookDto GetBook(Book book) + { + return _bookMapper.MapToDto(book); + } +} +``` + +> **Note**: Mapperly generates mapping code at compile-time, providing better performance than runtime mappers. + +### AutoMapper (runtime) +If the solution uses AutoMapper, mappings are typically defined in `Profile` classes and registered via ABP's AutoMapper integration. diff --git a/.github/skills/abp-authorization/SKILL.md b/.github/skills/abp-authorization/SKILL.md new file mode 100644 index 0000000000..a805f5f067 --- /dev/null +++ b/.github/skills/abp-authorization/SKILL.md @@ -0,0 +1,182 @@ +--- +name: abp-authorization +description: ABP permission system - PermissionDefinitionProvider, [Authorize] attribute, CheckPolicyAsync, IsGrantedAsync, ICurrentUser, IPermissionManager, multi-tenancy side. Use when working with permissions, authorization, role-based access, or security in ABP projects. +--- + +# ABP Authorization + +> **Docs**: https://abp.io/docs/latest/framework/fundamentals/authorization + +## Permission Definition +Define permissions in `*.Application.Contracts` project: + +```csharp +public static class BookStorePermissions +{ + public const string GroupName = "BookStore"; + + public static class Books + { + public const string Default = GroupName + ".Books"; + public const string Create = Default + ".Create"; + public const string Edit = Default + ".Edit"; + public const string Delete = Default + ".Delete"; + } +} +``` + +Register in provider: +```csharp +public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore")); + + var booksPermission = bookStoreGroup.AddPermission( + BookStorePermissions.Books.Default, + L("Permission:Books")); + + booksPermission.AddChild( + BookStorePermissions.Books.Create, + L("Permission:Books.Create")); + + booksPermission.AddChild( + BookStorePermissions.Books.Edit, + L("Permission:Books.Edit")); + + booksPermission.AddChild( + BookStorePermissions.Books.Delete, + L("Permission:Books.Delete")); + } + + private static LocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} +``` + +## Using Permissions + +### Declarative (Attribute) +```csharp +[Authorize(BookStorePermissions.Books.Create)] +public virtual async Task CreateAsync(CreateBookDto input) +{ + // Only users with Books.Create permission can execute +} +``` + +### Programmatic Check +```csharp +public class BookAppService : ApplicationService +{ + public async Task DoSomethingAsync() + { + // Check and throw if not granted + await CheckPolicyAsync(BookStorePermissions.Books.Edit); + + // Or check without throwing + if (await IsGrantedAsync(BookStorePermissions.Books.Delete)) + { + // Has permission + } + } +} +``` + +### Allow Anonymous Access +```csharp +[AllowAnonymous] +public virtual async Task GetPublicBookAsync(Guid id) +{ + // No authentication required +} +``` + +## Current User +Access authenticated user info via `CurrentUser` property (available in base classes like `ApplicationService`, `DomainService`, `AbpController`): + +```csharp +public class BookAppService : ApplicationService +{ + public async Task DoSomethingAsync() + { + // CurrentUser is available from base class - no injection needed + var userId = CurrentUser.Id; + var userName = CurrentUser.UserName; + var email = CurrentUser.Email; + var isAuthenticated = CurrentUser.IsAuthenticated; + var roles = CurrentUser.Roles; + var tenantId = CurrentUser.TenantId; + } +} + +// In other services, inject ICurrentUser +public class MyService : ITransientDependency +{ + private readonly ICurrentUser _currentUser; + public MyService(ICurrentUser currentUser) => _currentUser = currentUser; +} +``` + +### Ownership Validation +```csharp +public async Task UpdateMyBookAsync(Guid bookId, UpdateBookDto input) +{ + var book = await _bookRepository.GetAsync(bookId); + + if (book.CreatorId != CurrentUser.Id) + { + throw new AbpAuthorizationException(); + } + + // Update book... +} +``` + +## Multi-Tenancy Permissions +Control permission availability per tenant side: + +```csharp +bookStoreGroup.AddPermission( + BookStorePermissions.Books.Default, + L("Permission:Books"), + multiTenancySide: MultiTenancySides.Tenant // Only for tenants +); +``` + +Options: `MultiTenancySides.Host`, `Tenant`, or `Both` + +## Feature-Dependent Permissions +```csharp +booksPermission.RequireFeatures("BookStore.PremiumFeature"); +``` + +## Permission Management +Grant/revoke permissions programmatically: + +```csharp +public class MyService : ITransientDependency +{ + private readonly IPermissionManager _permissionManager; + + public async Task GrantPermissionToUserAsync(Guid userId, string permissionName) + { + await _permissionManager.SetForUserAsync(userId, permissionName, true); + } + + public async Task GrantPermissionToRoleAsync(string roleName, string permissionName) + { + await _permissionManager.SetForRoleAsync(roleName, permissionName, true); + } +} +``` + +## Security Best Practices +- Never trust client input for user identity +- Use `CurrentUser` property (from base class) or inject `ICurrentUser` +- Validate ownership in application service methods +- Filter queries by current user when appropriate +- Don't expose sensitive fields in DTOs diff --git a/.github/skills/abp-cli/SKILL.md b/.github/skills/abp-cli/SKILL.md new file mode 100644 index 0000000000..da08280b39 --- /dev/null +++ b/.github/skills/abp-cli/SKILL.md @@ -0,0 +1,89 @@ +--- +name: abp-cli +description: ABP CLI commands - generate-proxy, install-libs, add-package-ref, new-module, install-module, abp update, abp clean, abp suite generate. Use when the user asks how to run ABP CLI commands, generate proxies, install libraries, or use ABP Suite. +--- + +# ABP CLI Commands + +> **Full documentation**: https://abp.io/docs/latest/cli +> Use `abp help [command]` for detailed options. + +## Generate Client Proxies + +```bash +# URL flag: `-u` (short) or `--url` (long). Use whichever your team prefers, but keep it consistent. +# +# Angular (host must be running) +abp generate-proxy -t ng + +# C# client proxies +abp generate-proxy -t csharp -u https://localhost:44300 + +# Integration services only (microservices) +abp generate-proxy -t csharp -u https://localhost:44300 -st integration + +# JavaScript +abp generate-proxy -t js -u https://localhost:44300 +``` + +## Install Client-Side Libraries + +```bash +# Install NPM packages for MVC/Blazor Server +abp install-libs +``` + +## Add Package Reference + +```bash +# Add project reference with module dependency +abp add-package-ref Acme.BookStore.Domain +abp add-package-ref Acme.BookStore.Domain -t Acme.BookStore.Application +``` + +## Module Operations + +```bash +# Create new module in solution +abp new-module Acme.OrderManagement -t module:ddd + +# Install published module +abp install-module Volo.Blogging + +# Add ABP NuGet package +abp add-package Volo.Abp.Caching.StackExchangeRedis +``` + +## Update & Clean + +```bash +abp update # Update all ABP packages +abp update --version 8.0.0 # Specific version +abp clean # Delete bin/obj folders +``` + +## ABP Suite (CRUD Generation) + +Generate CRUD pages from entity JSON (created via Suite UI): + +```bash +abp suite generate --entity .suite/entities/Book.json --solution ./Acme.BookStore.sln +``` + +> **Note**: Entity JSON files are created when you generate an entity via ABP Suite UI. They are stored in `.suite/entities/` folder. +> **Suite docs**: https://abp.io/docs/latest/suite + +## Quick Reference + +| Task | Command | +|------|---------| +| Angular proxies | `abp generate-proxy -t ng` | +| C# proxies | `abp generate-proxy -t csharp -u URL` | +| Install JS libs | `abp install-libs` | +| Add reference | `abp add-package-ref PackageName` | +| Create module | `abp new-module ModuleName` | +| Install module | `abp install-module ModuleName` | +| Update packages | `abp update` | +| Clean solution | `abp clean` | +| Suite CRUD | `abp suite generate -e entity.json -s solution.sln` | +| Get help | `abp help [command]` | diff --git a/.github/skills/abp-core/SKILL.md b/.github/skills/abp-core/SKILL.md new file mode 100644 index 0000000000..b1f7bca91b --- /dev/null +++ b/.github/skills/abp-core/SKILL.md @@ -0,0 +1,190 @@ +--- +name: abp-core +description: Core ABP Framework conventions - module system, DI registration, base classes (ApplicationService, DomainService), IClock, BusinessException, localization, async patterns. Use when working on any ABP project, asking about ABP fundamentals, or unsure which skill applies. +--- + +# ABP Core Conventions + +> **Documentation**: https://abp.io/docs/latest +> **API Reference**: https://abp.io/docs/api/ + +## Key Rules + +- Use `IClock` / `Clock.Now` instead of `DateTime.Now` / `DateTime.UtcNow` +- Use `ITransientDependency` / `ISingletonDependency` instead of `AddScoped/AddTransient/AddSingleton` +- Use `IRepository` instead of injecting `DbContext` directly +- Check base class properties (`Clock`, `CurrentUser`, `GuidGenerator`, `L`) before injecting services +- Use `BusinessException` with namespaced error codes for domain rule violations + +## Module System +Every ABP application/module has a module class that configures services: + +```csharp +[DependsOn( + typeof(AbpDddDomainModule), + typeof(AbpEntityFrameworkCoreModule) +)] +public class MyAppModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Service registration and configuration + } +} +``` + +> **Note**: Middleware configuration (`OnApplicationInitialization`) should only be done in the final host application, not in reusable modules. + +## Dependency Injection Conventions + +### Automatic Registration +ABP automatically registers services implementing marker interfaces: +- `ITransientDependency` → Transient lifetime +- `ISingletonDependency` → Singleton lifetime +- `IScopedDependency` → Scoped lifetime + +Classes inheriting from `ApplicationService`, `DomainService`, `AbpController` are also auto-registered. + +### Repository Usage +You can use the generic `IRepository` for simple CRUD operations. Define custom repository interfaces only when you need custom query methods: + +```csharp +// Simple CRUD - Generic repository is fine +public class BookAppService : ApplicationService +{ + private readonly IRepository _bookRepository; // ✅ OK for simple operations +} + +// Custom queries needed - Define custom interface +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); // Custom query +} + +public class BookAppService : ApplicationService +{ + private readonly IBookRepository _bookRepository; // ✅ Use custom when needed +} +``` + +### Exposing Services +```csharp +[ExposeServices(typeof(IMyService))] +public class MyService : IMyService, ITransientDependency { } +``` + +## Important Base Classes + +| Base Class | Purpose | +|------------|---------| +| `Entity` | Basic entity with ID | +| `AggregateRoot` | DDD aggregate root | +| `DomainService` | Domain business logic | +| `ApplicationService` | Use case orchestration | +| `AbpController` | REST API controller | + +ABP base classes already inject commonly used services as properties. Before injecting a service, check if it's already available: + +| Property | Available In | Description | +|----------|--------------|-------------| +| `GuidGenerator` | All base classes | Generate GUIDs | +| `Clock` | All base classes | Current time (use instead of `DateTime`) | +| `CurrentUser` | All base classes | Authenticated user info | +| `CurrentTenant` | All base classes | Multi-tenancy context | +| `L` (StringLocalizer) | `ApplicationService`, `AbpController` | Localization | +| `AuthorizationService` | `ApplicationService`, `AbpController` | Permission checks | +| `FeatureChecker` | `ApplicationService`, `AbpController` | Feature availability | +| `DataFilter` | All base classes | Data filtering (soft-delete, tenant) | +| `UnitOfWorkManager` | `ApplicationService`, `DomainService` | Unit of work management | +| `LoggerFactory` | All base classes | Create loggers | +| `Logger` | All base classes | Logging (auto-created) | +| `LazyServiceProvider` | All base classes | Lazy service resolution | + +**Useful methods from base classes:** +- `CheckPolicyAsync()` - Check permission and throw if not granted +- `IsGrantedAsync()` - Check permission without throwing + +## Async Best Practices +- Use async all the way - never use `.Result` or `.Wait()` +- All async methods should end with `Async` suffix +- ABP automatically handles `CancellationToken` in most cases (e.g., from `HttpContext.RequestAborted`) +- Only pass `CancellationToken` explicitly when implementing custom cancellation logic + +## Time Handling +Never use `DateTime.Now` or `DateTime.UtcNow` directly. Use ABP's `IClock` service: + +```csharp +// In classes inheriting from base classes (ApplicationService, DomainService, etc.) +public class BookAppService : ApplicationService +{ + public void DoSomething() + { + var now = Clock.Now; // ✅ Already available as property + } +} + +// In other services - inject IClock +public class MyService : ITransientDependency +{ + private readonly IClock _clock; + + public MyService(IClock clock) => _clock = clock; + + public void DoSomething() + { + var now = _clock.Now; // ✅ Correct + // var now = DateTime.Now; // ❌ Wrong - not testable, ignores timezone settings + } +} +``` + +> **Tip**: Before injecting a service, check if it's already available as a property in your base classes. + +## Business Exceptions +Use `BusinessException` for domain rule violations with namespaced error codes: + +```csharp +throw new BusinessException("MyModule:BookNameAlreadyExists") + .WithData("Name", bookName); +``` + +Configure localization mapping: +```csharp +Configure(options => +{ + options.MapCodeNamespace("MyModule", typeof(MyModuleResource)); +}); +``` + +## Localization +- In base classes (`ApplicationService`, `AbpController`, etc.): Use `L["Key"]` - this is the `IStringLocalizer` property +- In other services: Inject `IStringLocalizer` +- Always localize user-facing messages and exceptions + +**Localization file location**: `*.Domain.Shared/Localization/{ResourceName}/{lang}.json` + +```json +// Example: MyProject.Domain.Shared/Localization/MyProject/en.json +{ + "culture": "en", + "texts": { + "Menu:Home": "Home", + "Welcome": "Welcome", + "BookName": "Book Name" + } +} +``` + +## ❌ Never Use (ABP Anti-Patterns) + +| Don't Use | Use Instead | +|-----------|-------------| +| Minimal APIs | ABP Controllers or Auto API Controllers | +| MediatR | Application Services | +| `DbContext` directly in App Services | `IRepository` | +| `AddScoped/AddTransient/AddSingleton` | `ITransientDependency`, `ISingletonDependency` | +| `DateTime.Now` | `IClock` / `Clock.Now` | +| Custom UnitOfWork | ABP's `IUnitOfWorkManager` | +| Manual HTTP calls from UI | ABP client proxies (`generate-proxy`) | +| Hardcoded role checks | Permission-based authorization | +| Business logic in Controllers | Application Services | diff --git a/.github/skills/abp-ddd/SKILL.md b/.github/skills/abp-ddd/SKILL.md new file mode 100644 index 0000000000..885324130d --- /dev/null +++ b/.github/skills/abp-ddd/SKILL.md @@ -0,0 +1,248 @@ +--- +name: abp-ddd +description: ABP DDD patterns - Entities, Aggregate Roots, value objects, Repositories, Domain Services, Domain Events, Specifications. Use when designing domain layer, creating entities, repositories, or domain services in ABP projects. +--- + +# ABP DDD Patterns + +> **Docs**: https://abp.io/docs/latest/framework/architecture/domain-driven-design + +## Anti-Patterns to Avoid + +- **Anemic entities**: public setters with no behavior — use private setters + methods that enforce invariants +- **Repository for child entities**: only aggregate roots get repositories — access child entities through their root +- **Generating GUID in entity constructor**: use `IGuidGenerator` from outside and pass `id` parameter +- **Navigation properties to other aggregates**: reference by `Id` only, never add full navigation properties across aggregates +- **Domain service depending on current user**: accept values from the application layer instead + +## Rich Domain Model vs Anemic Domain Model + +ABP promotes **Rich Domain Model** pattern where entities contain both data AND behavior: + +| Anemic (Anti-pattern) | Rich (Recommended) | +|----------------------|-------------------| +| Entity = data only | Entity = data + behavior | +| Logic in services | Logic in entity methods | +| Public setters | Private setters with methods | +| No validation in entity | Entity enforces invariants | + +**Encapsulation is key**: Protect entity state by using private setters and exposing behavior through methods. + +## Entities + +### Entity Example (Rich Model) +```csharp +public class OrderLine : Entity +{ + public Guid ProductId { get; private set; } + public int Count { get; private set; } + public decimal Price { get; private set; } + + protected OrderLine() { } // For ORM + + internal OrderLine(Guid id, Guid productId, int count, decimal price) : base(id) + { + ProductId = productId; + SetCount(count); // Validates through method + Price = price; + } + + public void SetCount(int count) + { + if (count <= 0) + throw new BusinessException("Orders:InvalidCount"); + Count = count; + } +} +``` + +## Aggregate Roots + +Aggregate roots are consistency boundaries that: +- Own their child entities +- Enforce business rules +- Publish domain events + +```csharp +public class Order : AggregateRoot +{ + public string OrderNumber { get; private set; } + public Guid CustomerId { get; private set; } + public OrderStatus Status { get; private set; } + public ICollection Lines { get; private set; } + + protected Order() { } // For ORM + + public Order(Guid id, string orderNumber, Guid customerId) : base(id) + { + OrderNumber = Check.NotNullOrWhiteSpace(orderNumber, nameof(orderNumber)); + CustomerId = customerId; + Status = OrderStatus.Created; + Lines = new List(); + } + + public void AddLine(Guid lineId, Guid productId, int count, decimal price) + { + // Business rule: Can only add lines to created orders + if (Status != OrderStatus.Created) + throw new BusinessException("Orders:CannotModifyOrder"); + + Lines.Add(new OrderLine(lineId, productId, count, price)); + } + + public void Complete() + { + if (Status != OrderStatus.Created) + throw new BusinessException("Orders:CannotCompleteOrder"); + + Status = OrderStatus.Completed; + + // Publish events for side effects + AddLocalEvent(new OrderCompletedEvent(Id)); // Same transaction + AddDistributedEvent(new OrderCompletedEto { OrderId = Id }); // Cross-service + } +} +``` + +### Domain Events +- `AddLocalEvent()` - Handled within same transaction, can access full entity +- `AddDistributedEvent()` - Handled asynchronously, use ETOs (Event Transfer Objects) + +### Entity Best Practices +- **Encapsulation**: Private setters, public methods that enforce rules +- **Primary constructor**: Enforce invariants, accept `id` parameter +- **Protected parameterless constructor**: Required for ORM +- **Initialize collections**: In primary constructor +- **Virtual members**: For ORM proxy compatibility +- **Reference by Id**: Don't add navigation properties to other aggregates +- **Don't generate GUID in constructor**: Use `IGuidGenerator` externally + +## Repository Pattern + +### When to Use Custom Repository +- **Generic repository** (`IRepository`): Sufficient for simple CRUD operations +- **Custom repository**: Only when you need custom query methods + +### Interface (Domain Layer) +```csharp +// Define custom interface only when custom queries are needed +public interface IOrderRepository : IRepository +{ + Task FindByOrderNumberAsync(string orderNumber, bool includeDetails = false); + Task> GetListByCustomerAsync(Guid customerId, bool includeDetails = false); +} +``` + +### Repository Best Practices +- **One repository per aggregate root only** - Never create repositories for child entities +- Child entities must be accessed/modified only through their aggregate root +- Creating repositories for child entities breaks data consistency (bypasses aggregate root's business rules) +- In ABP, use `AddDefaultRepositories()` without `includeAllEntities: true` to enforce this +- Define custom repository only when custom queries are needed +- ABP handles `CancellationToken` automatically; add parameter only for explicit cancellation control +- Single entity methods: `includeDetails = true` by default +- List methods: `includeDetails = false` by default +- Don't return projection classes +- Interface in Domain, implementation in data layer + +```csharp +// ✅ Correct: Repository for aggregate root (Order) +public interface IOrderRepository : IRepository { } + +// ❌ Wrong: Repository for child entity (OrderLine) +// OrderLine should only be accessed through Order aggregate +public interface IOrderLineRepository : IRepository { } // Don't do this! +``` + +## Domain Services + +Use domain services for business logic that: +- Spans multiple aggregates +- Requires repository queries to enforce rules + +```csharp +public class OrderManager : DomainService +{ + private readonly IOrderRepository _orderRepository; + private readonly IProductRepository _productRepository; + + public OrderManager( + IOrderRepository orderRepository, + IProductRepository productRepository) + { + _orderRepository = orderRepository; + _productRepository = productRepository; + } + + public async Task CreateAsync(string orderNumber, Guid customerId) + { + // Business rule: Order number must be unique + var existing = await _orderRepository.FindByOrderNumberAsync(orderNumber); + if (existing != null) + { + throw new BusinessException("Orders:OrderNumberAlreadyExists") + .WithData("OrderNumber", orderNumber); + } + + return new Order(GuidGenerator.Create(), orderNumber, customerId); + } + + public async Task AddProductAsync(Order order, Guid productId, int count) + { + var product = await _productRepository.GetAsync(productId); + order.AddLine(productId, count, product.Price); + } +} +``` + +### Domain Service Best Practices +- Use `*Manager` suffix naming +- No interface by default (create only if needed) +- Accept/return domain objects, not DTOs +- Don't depend on authenticated user - pass values from application layer +- Use base class properties (`GuidGenerator`, `Clock`) instead of injecting these services + +## Domain Events + +### Local Events +```csharp +// In aggregate +AddLocalEvent(new OrderCompletedEvent(Id)); + +// Handler +public class OrderCompletedEventHandler : ILocalEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(OrderCompletedEvent eventData) + { + // Handle within same transaction + } +} +``` + +### Distributed Events (ETO) +For inter-module/microservice communication: +```csharp +// In Domain.Shared +[EventName("Orders.OrderCompleted")] +public class OrderCompletedEto +{ + public Guid OrderId { get; set; } + public string OrderNumber { get; set; } +} +``` + +## Specifications + +Reusable query conditions: +```csharp +public class CompletedOrdersSpec : Specification +{ + public override Expression> ToExpression() + { + return o => o.Status == OrderStatus.Completed; + } +} + +// Usage +var orders = await _orderRepository.GetListAsync(new CompletedOrdersSpec()); +``` diff --git a/.github/skills/abp-dependency-rules/SKILL.md b/.github/skills/abp-dependency-rules/SKILL.md new file mode 100644 index 0000000000..025e6b707f --- /dev/null +++ b/.github/skills/abp-dependency-rules/SKILL.md @@ -0,0 +1,150 @@ +--- +name: abp-dependency-rules +description: ABP project layer dependency rules - which projects can reference which, domain/application/infrastructure separation, cross-layer violations to avoid. Use when reviewing project structure, adding new project references, or checking if a dependency direction is correct. +--- + +# ABP Dependency Rules + +## Core Principles (All Templates) + +These principles apply regardless of solution structure: + +1. **Domain logic never depends on infrastructure** (no DbContext in domain/application) +2. **Use abstractions** (interfaces) for dependencies +3. **Higher layers depend on lower layers**, never the reverse +4. **Data access through repositories**, not direct DbContext + +## Layered Template Structure + +> **Note**: This section applies to layered templates (app, module). Single-layer and microservice templates have different structures. + +``` +Domain.Shared → Constants, enums, localization keys + ↑ + Domain → Entities, repository interfaces, domain services + ↑ +Application.Contracts → App service interfaces, DTOs + ↑ + Application → App service implementations + ↑ + HttpApi → REST controllers (optional) + ↑ + Host → Final application with DI and middleware +``` + +### Layered Dependency Direction + +| Project | Can Reference | Referenced By | +|---------|---------------|---------------| +| Domain.Shared | Nothing | All | +| Domain | Domain.Shared | Application, Data layer | +| Application.Contracts | Domain.Shared | Application, HttpApi, Clients | +| Application | Domain, Contracts | Host | +| EntityFrameworkCore/MongoDB | Domain | Host only | +| HttpApi | Contracts only | Host | + +## Critical Rules + +### ❌ Never Do +```csharp +// Application layer accessing DbContext directly +public class BookAppService : ApplicationService +{ + private readonly MyDbContext _dbContext; // ❌ WRONG +} + +// Domain depending on application layer +public class BookManager : DomainService +{ + private readonly IBookAppService _appService; // ❌ WRONG +} + +// HttpApi depending on Application implementation +public class BookController : AbpController +{ + private readonly BookAppService _bookAppService; // ❌ WRONG - Use interface +} +``` + +### ✅ Always Do +```csharp +// Application layer using repository abstraction +public class BookAppService : ApplicationService +{ + private readonly IBookRepository _bookRepository; // ✅ CORRECT +} + +// Domain service using domain abstractions +public class BookManager : DomainService +{ + private readonly IBookRepository _bookRepository; // ✅ CORRECT +} + +// HttpApi depending on contracts only +public class BookController : AbpController +{ + private readonly IBookAppService _bookAppService; // ✅ CORRECT +} +``` + +## Repository Pattern Enforcement + +### Interface Location +```csharp +// In Domain project +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); +} +``` + +### Implementation Location +```csharp +// In EntityFrameworkCore project +public class BookRepository : EfCoreRepository, IBookRepository +{ + // Implementation +} + +// In MongoDB project +public class BookRepository : MongoDbRepository, IBookRepository +{ + // Implementation +} +``` + +## Multi-Application Scenarios + +When you have multiple applications (e.g., Admin + Public API): + +### Vertical Separation +``` +MyProject.Admin.Application - Admin-specific services +MyProject.Public.Application - Public-specific services +MyProject.Domain - Shared domain (both reference this) +``` + +### Rules +- Admin and Public application layers **MUST NOT** reference each other +- Share domain logic, not application logic +- Each vertical can have its own DTOs even if similar + +## Enforcement Checklist (Layered Templates) + +When adding a new feature: +1. **Entity changes?** → Domain project +2. **Constants/enums?** → Domain.Shared project +3. **Repository interface?** → Domain project (only if custom queries needed) +4. **Repository implementation?** → EntityFrameworkCore/MongoDB project +5. **DTOs and service interface?** → Application.Contracts project +6. **Service implementation?** → Application project +7. **API endpoint?** → HttpApi project (if not using auto API controllers) + +## Common Violations to Watch + +| Violation | Impact | Fix | +|-----------|--------|-----| +| DbContext in Application | Breaks DB independence | Use repository | +| Entity in DTO | Exposes internals | Map to DTO | +| IQueryable in interface | Breaks abstraction | Return concrete types | +| Cross-module app service call | Tight coupling | Use events or domain | diff --git a/.github/skills/abp-development-flow/SKILL.md b/.github/skills/abp-development-flow/SKILL.md new file mode 100644 index 0000000000..ad6abe3373 --- /dev/null +++ b/.github/skills/abp-development-flow/SKILL.md @@ -0,0 +1,261 @@ +--- +name: abp-development-flow +description: ABP development workflow - step-by-step guide for adding new entities, migrations, application services, localization, permissions, and tests. Use when adding new features or entities to an ABP project. +--- + +# ABP Development Workflow + +> **Tutorials**: https://abp.io/docs/latest/tutorials + +## Adding a New Entity (Full Flow) + +### 1. Domain Layer +Create entity (location varies by template: `*.Domain/Entities/` for layered, `Entities/` for single-layer/microservice): + +```csharp +public class Book : AggregateRoot +{ + public string Name { get; private set; } + public decimal Price { get; private set; } + public Guid AuthorId { get; private set; } + + protected Book() { } + + public Book(Guid id, string name, decimal price, Guid authorId) : base(id) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name)); + SetPrice(price); + AuthorId = authorId; + } + + public void SetPrice(decimal price) + { + Price = Check.Range(price, nameof(price), 0, 9999); + } +} +``` + +### 2. Domain.Shared +Add constants and enums in `*.Domain.Shared/`: + +```csharp +public static class BookConsts +{ + public const int MaxNameLength = 128; +} + +public enum BookType +{ + Novel, + Science, + Biography +} +``` + +### 3. Repository Interface (Optional) +Define custom repository in `*.Domain/` only if you need custom query methods. For simple CRUD, use generic `IRepository` directly: + +```csharp +// Only if custom queries are needed +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); +} +``` + +### 4. EF Core Configuration +In `*.EntityFrameworkCore/`: + +**DbContext:** +```csharp +public DbSet Books { get; set; } +``` + +**OnModelCreating:** +```csharp +builder.Entity(b => +{ + b.ToTable(MyProjectConsts.DbTablePrefix + "Books", MyProjectConsts.DbSchema); + b.ConfigureByConvention(); + b.Property(x => x.Name).IsRequired().HasMaxLength(BookConsts.MaxNameLength); + b.HasIndex(x => x.Name); +}); +``` + +**Repository Implementation (only if custom interface defined):** +```csharp +public class BookRepository : EfCoreRepository, IBookRepository +{ + public BookRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync(string name) + { + return await (await GetDbSetAsync()) + .FirstOrDefaultAsync(b => b.Name == name); + } +} +``` + +### 5. Run Migration +See `abp-ef-core` skill for migration commands. Recommended: use `DbMigrator` project to apply migrations and seed data. + +### 6. Application.Contracts +Create DTOs and service interface: + +```csharp +// DTOs +public class BookDto : EntityDto +{ + public string Name { get; set; } + public decimal Price { get; set; } + public Guid AuthorId { get; set; } +} + +public class CreateBookDto +{ + [Required] + [StringLength(BookConsts.MaxNameLength)] + public string Name { get; set; } + + [Range(0, 9999)] + public decimal Price { get; set; } + + [Required] + public Guid AuthorId { get; set; } +} + +// Service Interface +public interface IBookAppService : IApplicationService +{ + Task GetAsync(Guid id); + Task> GetListAsync(PagedAndSortedResultRequestDto input); + Task CreateAsync(CreateBookDto input); +} +``` + +### 7. Object Mapping (Mapperly / AutoMapper) +ABP supports both Mapperly and AutoMapper. Prefer the provider already used in the solution. + +If the solution uses **Mapperly**, create a mapper in the Application project: + +```csharp +[Mapper] +public partial class BookMapper +{ + public partial BookDto MapToDto(Book book); + public partial List MapToDtoList(List books); +} +``` + +Register in module: +```csharp +context.Services.AddSingleton(); +``` + +### 8. Application Service +Implement service (using generic repository - use `IBookRepository` if you defined custom interface in step 3): + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + private readonly IRepository _bookRepository; // Or IBookRepository + private readonly BookMapper _bookMapper; + + public BookAppService( + IRepository bookRepository, + BookMapper bookMapper) + { + _bookRepository = bookRepository; + _bookMapper = bookMapper; + } + + public async Task GetAsync(Guid id) + { + var book = await _bookRepository.GetAsync(id); + return _bookMapper.MapToDto(book); + } + + [Authorize(MyProjectPermissions.Books.Create)] + public async Task CreateAsync(CreateBookDto input) + { + var book = new Book( + GuidGenerator.Create(), + input.Name, + input.Price, + input.AuthorId + ); + + await _bookRepository.InsertAsync(book); + return _bookMapper.MapToDto(book); + } +} +``` + +### 9. Add Localization +In `*.Domain.Shared/Localization/*/en.json`: + +```json +{ + "Book": "Book", + "Books": "Books", + "BookName": "Name", + "BookPrice": "Price" +} +``` + +### 10. Add Permissions (if needed) +```csharp +public static class MyProjectPermissions +{ + public static class Books + { + public const string Default = "MyProject.Books"; + public const string Create = Default + ".Create"; + } +} +``` + +### 11. Add Tests +```csharp +public class BookAppService_Tests : MyProjectApplicationTestBase +{ + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Create_Book() + { + var result = await _bookAppService.CreateAsync(new CreateBookDto + { + Name = "Test Book", + Price = 19.99m + }); + + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("Test Book"); + } +} +``` + +## Checklist for New Features + +- [ ] Entity created with proper constructors +- [ ] Constants in Domain.Shared +- [ ] Custom repository interface in Domain (only if custom queries needed) +- [ ] EF Core configuration added +- [ ] Custom repository implementation (only if interface defined) +- [ ] Migration generated and applied (use DbMigrator) +- [ ] Mapperly mapper created and registered +- [ ] DTOs created in Application.Contracts +- [ ] Service interface defined +- [ ] Service implementation with authorization +- [ ] Localization keys added +- [ ] Permissions defined (if applicable) +- [ ] Tests written diff --git a/.github/skills/abp-ef-core/SKILL.md b/.github/skills/abp-ef-core/SKILL.md new file mode 100644 index 0000000000..d255042b83 --- /dev/null +++ b/.github/skills/abp-ef-core/SKILL.md @@ -0,0 +1,262 @@ +--- +name: abp-ef-core +description: ABP Entity Framework Core - DbContext, entity configuration, EfCoreRepository implementation, migrations (dotnet ef migrations add), data seeding. Use when working in EntityFrameworkCore projects, adding migrations, or implementing EF Core repositories. +--- + +# ABP Entity Framework Core + +> **Docs**: https://abp.io/docs/latest/framework/data/entity-framework-core + +## Never Do + +| Don't | Do Instead | +|-------|-----------| +| Skip `b.ConfigureByConvention()` | Always call it first in entity config | +| `AddDefaultRepositories(includeAllEntities: true)` | Use `AddDefaultRepositories()` only for aggregate roots | +| Inject `DbContext` in application/domain services | Use `IRepository` or custom repository interface | +| Use `DbContext` directly outside the EF Core project | Access via `GetDbContextAsync()` inside repository only | + +## DbContext Configuration + +```csharp +[ConnectionStringName("Default")] +public class MyProjectDbContext : AbpDbContext +{ + public DbSet Books { get; set; } + public DbSet Authors { get; set; } + + public MyProjectDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + // Configure all entities + builder.ConfigureMyProject(); + } +} +``` + +## Entity Configuration + +```csharp +public static class MyProjectDbContextModelCreatingExtensions +{ + public static void ConfigureMyProject(this ModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + builder.Entity(b => + { + b.ToTable(MyProjectConsts.DbTablePrefix + "Books", MyProjectConsts.DbSchema); + b.ConfigureByConvention(); // ABP conventions (audit, soft-delete, etc.) + + // Property configurations + b.Property(x => x.Name) + .IsRequired() + .HasMaxLength(BookConsts.MaxNameLength); + + b.Property(x => x.Price) + .HasColumnType("decimal(18,2)"); + + // Indexes + b.HasIndex(x => x.Name); + + // Relationships + b.HasOne() + .WithMany() + .HasForeignKey(x => x.AuthorId) + .OnDelete(DeleteBehavior.Restrict); + }); + } +} +``` + +## Repository Implementation + +```csharp +public class BookRepository : EfCoreRepository, IBookRepository +{ + public BookRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync( + string name, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + var dbSet = await GetDbSetAsync(); + + return await dbSet + .IncludeDetails(includeDetails) + .FirstOrDefaultAsync( + b => b.Name == name, + GetCancellationToken(cancellationToken)); + } + + public async Task> GetListByAuthorAsync( + Guid authorId, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var dbSet = await GetDbSetAsync(); + + return await dbSet + .IncludeDetails(includeDetails) + .Where(b => b.AuthorId == authorId) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public override async Task> WithDetailsAsync() + { + return (await GetQueryableAsync()) + .Include(b => b.Reviews); + } +} +``` + +## Extension Method for Include +```csharp +public static class BookEfCoreQueryableExtensions +{ + public static IQueryable IncludeDetails( + this IQueryable queryable, + bool include = true) + { + if (!include) + { + return queryable; + } + + return queryable + .Include(b => b.Reviews); + } +} +``` + +## Migration Commands + +```bash +# Navigate to EF Core project +cd src/MyProject.EntityFrameworkCore + +# Add migration +dotnet ef migrations add MigrationName + +# Apply migration (choose one): +dotnet run --project ../MyProject.DbMigrator # Recommended - also seeds data +dotnet ef database update # EF Core command only + +# Remove last migration (if not applied) +dotnet ef migrations remove + +# Generate SQL script +dotnet ef migrations script +``` + +> **Note**: ABP templates include `IDesignTimeDbContextFactory` in the EF Core project, so `-s` (startup project) parameter is not needed. + +## Module Configuration + +```csharp +[DependsOn(typeof(AbpEntityFrameworkCoreModule))] +public class MyProjectEntityFrameworkCoreModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAbpDbContext(options => + { + // Add default repositories for aggregate roots only (DDD best practice) + options.AddDefaultRepositories(); + // ⚠️ Avoid includeAllEntities: true - it creates repositories for child entities, + // allowing them to be modified without going through the aggregate root, + // which breaks data consistency + }); + + Configure(options => + { + options.UseSqlServer(); // or UseNpgsql(), UseMySql(), etc. + }); + } +} +``` + +## Best Practices + +### Repositories for Aggregate Roots Only +Don't use `includeAllEntities: true` in `AddDefaultRepositories()`. This creates repositories for child entities, allowing direct modification without going through the aggregate root - breaking DDD data consistency rules. + +```csharp +// ✅ Correct - Only aggregate roots get repositories +options.AddDefaultRepositories(); + +// ❌ Avoid - Creates repositories for ALL entities including child entities +options.AddDefaultRepositories(includeAllEntities: true); +``` + +### Always Call ConfigureByConvention +```csharp +builder.Entity(b => +{ + b.ConfigureByConvention(); // Don't forget this! + // Other configurations... +}); +``` + +### Use Table Prefix +```csharp +public static class MyProjectConsts +{ + public const string DbTablePrefix = "App"; + public const string DbSchema = null; // Or "myschema" +} +``` + +### Performance Tips +- Add explicit indexes for frequently queried fields +- Use `AsNoTracking()` for read-only queries +- Avoid N+1 queries with `.Include()` or specifications +- ABP handles cancellation automatically; use `GetCancellationToken(cancellationToken)` only in custom repository methods +- Consider query splitting for complex queries with multiple collections + +### Accessing Raw DbContext +```csharp +public async Task CustomOperationAsync() +{ + var dbContext = await GetDbContextAsync(); + + // Raw SQL + await dbContext.Database.ExecuteSqlRawAsync( + "UPDATE Books SET IsPublished = 1 WHERE AuthorId = {0}", + authorId + ); +} +``` + +## Data Seeding + +```csharp +public class MyProjectDataSeedContributor : IDataSeedContributor, ITransientDependency +{ + private readonly IRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public async Task SeedAsync(DataSeedContext context) + { + if (await _bookRepository.GetCountAsync() > 0) + { + return; + } + + await _bookRepository.InsertAsync( + new Book(_guidGenerator.Create(), "Sample Book", 19.99m, Guid.Empty), + autoSave: true + ); + } +} +``` diff --git a/.github/skills/abp-infrastructure/SKILL.md b/.github/skills/abp-infrastructure/SKILL.md new file mode 100644 index 0000000000..3d48675bfc --- /dev/null +++ b/.github/skills/abp-infrastructure/SKILL.md @@ -0,0 +1,243 @@ +--- +name: abp-infrastructure +description: ABP infrastructure services - ISettingProvider, IFeatureChecker, IDistributedCache, ILocalEventBus, IDistributedEventBus, IBackgroundJobManager, localization resource. Use when working with settings, feature flags, caching, event bus, or background jobs in ABP. +--- + +# ABP Infrastructure Services + +> **Docs**: https://abp.io/docs/latest/framework/infrastructure + +## Settings + +### Define Settings +```csharp +public class MySettingDefinitionProvider : SettingDefinitionProvider +{ + public override void Define(ISettingDefinitionContext context) + { + context.Add( + new SettingDefinition("MyApp.MaxItemCount", "10"), + new SettingDefinition("MyApp.EnableFeature", "false"), + new SettingDefinition("MyApp.SecretKey", isEncrypted: true) + ); + } +} +``` + +### Read Settings +```csharp +public class MyService : ITransientDependency +{ + private readonly ISettingProvider _settingProvider; + + public async Task DoSomethingAsync() + { + var maxCount = await _settingProvider.GetAsync("MyApp.MaxItemCount"); + var isEnabled = await _settingProvider.IsTrueAsync("MyApp.EnableFeature"); + } +} +``` + +### Setting Value Providers (Priority Order) +1. User settings (highest) +2. Tenant settings +3. Global settings +4. Configuration (appsettings.json) +5. Default value (lowest) + +## Features + +### Define Features +```csharp +public class MyFeatureDefinitionProvider : FeatureDefinitionProvider +{ + public override void Define(IFeatureDefinitionContext context) + { + var myGroup = context.AddGroup("MyApp"); + + myGroup.AddFeature( + "MyApp.PdfReporting", + defaultValue: "false", + valueType: new ToggleStringValueType() + ); + + myGroup.AddFeature( + "MyApp.MaxProductCount", + defaultValue: "10", + valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1000)) + ); + } +} +``` + +### Check Features +```csharp +[RequiresFeature("MyApp.PdfReporting")] +public async Task GetPdfReportAsync() +{ + // Only executes if feature is enabled +} + +// Or programmatically +if (await _featureChecker.IsEnabledAsync("MyApp.PdfReporting")) +{ + // Feature is enabled for current tenant +} + +var maxCount = await _featureChecker.GetAsync("MyApp.MaxProductCount"); +``` + +## Distributed Caching + +### Typed Cache +```csharp +public class BookService : ITransientDependency +{ + private readonly IDistributedCache _cache; + private readonly IClock _clock; + + public BookService(IDistributedCache cache, IClock clock) + { + _cache = cache; + _clock = clock; + } + + public async Task GetAsync(Guid bookId) + { + return await _cache.GetOrAddAsync( + bookId.ToString(), + async () => await GetBookFromDatabaseAsync(bookId), + () => new DistributedCacheEntryOptions + { + AbsoluteExpiration = _clock.Now.AddHours(1) + } + ); + } +} + +[CacheName("Books")] +public class BookCacheItem +{ + public string Name { get; set; } + public decimal Price { get; set; } +} +``` + +## Event Bus + +### Local Events (Same Process) +```csharp +// Event class +public class OrderCreatedEvent +{ + public Order Order { get; set; } +} + +// Handler +public class OrderCreatedEventHandler : ILocalEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(OrderCreatedEvent eventData) + { + // Handle within same transaction + } +} + +// Publish +await _localEventBus.PublishAsync(new OrderCreatedEvent { Order = order }); +``` + +### Distributed Events (Cross-Service) +```csharp +// Event Transfer Object (in Domain.Shared) +[EventName("MyApp.Order.Created")] +public class OrderCreatedEto +{ + public Guid OrderId { get; set; } + public string OrderNumber { get; set; } +} + +// Handler +public class OrderCreatedEtoHandler : IDistributedEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(OrderCreatedEto eventData) + { + // Handle distributed event + } +} + +// Publish +await _distributedEventBus.PublishAsync(new OrderCreatedEto { ... }); +``` + +### When to Use Which +- **Local**: Within same module/bounded context +- **Distributed**: Cross-module or microservice communication + +## Background Jobs + +### Define Job +```csharp +public class EmailSendingArgs +{ + public string EmailAddress { get; set; } + public string Subject { get; set; } + public string Body { get; set; } +} + +public class EmailSendingJob : AsyncBackgroundJob, ITransientDependency +{ + private readonly IEmailSender _emailSender; + + public EmailSendingJob(IEmailSender emailSender) + { + _emailSender = emailSender; + } + + public override async Task ExecuteAsync(EmailSendingArgs args) + { + await _emailSender.SendAsync(args.EmailAddress, args.Subject, args.Body); + } +} +``` + +### Enqueue Job +```csharp +await _backgroundJobManager.EnqueueAsync( + new EmailSendingArgs + { + EmailAddress = "user@example.com", + Subject = "Hello", + Body = "..." + }, + delay: TimeSpan.FromMinutes(5) // Optional delay +); +``` + +## Localization + +### Define Resource +```csharp +[LocalizationResourceName("MyModule")] +public class MyModuleResource { } +``` + +### JSON Structure +```json +{ + "culture": "en", + "texts": { + "HelloWorld": "Hello World!", + "Menu:Books": "Books" + } +} +``` + +### Usage +- In `ApplicationService`: Use `L["Key"]` property (already available from base class) +- In other services: Inject `IStringLocalizer` + +> **Tip**: ABP base classes already provide commonly used services as properties. Check before injecting: +> - `StringLocalizer` (L), `Clock`, `CurrentUser`, `CurrentTenant`, `GuidGenerator` +> - `AuthorizationService`, `FeatureChecker`, `DataFilter` +> - `LoggerFactory`, `Logger` +> - Methods like `CheckPolicyAsync()` for authorization checks diff --git a/.github/skills/abp-microservice/SKILL.md b/.github/skills/abp-microservice/SKILL.md new file mode 100644 index 0000000000..e122789728 --- /dev/null +++ b/.github/skills/abp-microservice/SKILL.md @@ -0,0 +1,209 @@ +--- +name: abp-microservice +description: ABP Microservice solution template - service structure, Integration Services ([IntegrationService]), inter-service HTTP proxies, distributed events with Outbox/Inbox, Entity Cache, RabbitMQ/Redis/YARP setup. Use when working with the ABP microservice solution template or inter-service communication patterns. +--- + +# ABP Microservice Solution Template + +> **Docs**: https://abp.io/docs/latest/solution-templates/microservice + +## Solution Structure + +``` +MyMicroservice/ +├── apps/ # UI applications +│ ├── web/ # Web application +│ ├── public-web/ # Public website +│ └── auth-server/ # Authentication server (OpenIddict) +├── gateways/ # BFF pattern - one gateway per UI +│ └── web-gateway/ # YARP reverse proxy +├── services/ # Microservices +│ ├── administration/ # Permissions, settings, features +│ ├── identity/ # Users, roles +│ └── [your-services]/ # Your business services +└── etc/ + ├── docker/ # Docker compose for local infra + └── helm/ # Kubernetes deployment +``` + +## Microservice Structure (NOT Layered!) + +Each microservice has simplified structure - everything in one project: + +``` +services/ordering/ +├── OrderingService/ # Main project +│ ├── Entities/ +│ ├── Services/ +│ ├── IntegrationServices/ # For inter-service communication +│ ├── Data/ # DbContext (implements IHasEventInbox, IHasEventOutbox) +│ └── OrderingServiceModule.cs +├── OrderingService.Contracts/ # Interfaces, DTOs, ETOs (shared) +└── OrderingService.Tests/ +``` + +## Inter-Service Communication + +### 1. Integration Services (Synchronous HTTP) + +For synchronous calls, use **Integration Services** - NOT regular application services. + +#### Step 1: Provider Service - Create Integration Service + +```csharp +// In CatalogService.Contracts project +[IntegrationService] +public interface IProductIntegrationService : IApplicationService +{ + Task> GetProductsByIdsAsync(List ids); +} + +// In CatalogService project +[IntegrationService] +public class ProductIntegrationService : ApplicationService, IProductIntegrationService +{ + public async Task> GetProductsByIdsAsync(List ids) + { + var products = await _productRepository.GetListAsync(p => ids.Contains(p.Id)); + return ObjectMapper.Map, List>(products); + } +} +``` + +#### Step 2: Provider Service - Expose Integration Services + +```csharp +// In CatalogServiceModule.cs +Configure(options => +{ + options.ExposeIntegrationServices = true; +}); +``` + +#### Step 3: Consumer Service - Add Package Reference + +Add reference to provider's Contracts project (via ABP Studio or manually): +- Right-click OrderingService → Add Package Reference → Select `CatalogService.Contracts` + +#### Step 4: Consumer Service - Generate Proxies + +```bash +# Run ABP CLI in consumer service folder +abp generate-proxy -t csharp -u http://localhost:44361 -m catalog --without-contracts +``` + +Or use ABP Studio: Right-click service → ABP CLI → Generate Proxy → C# + +#### Step 5: Consumer Service - Register HTTP Client Proxies + +```csharp +// In OrderingServiceModule.cs +[DependsOn(typeof(CatalogServiceContractsModule))] // Add module dependency +public class OrderingServiceModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Register static HTTP client proxies + context.Services.AddStaticHttpClientProxies( + typeof(CatalogServiceContractsModule).Assembly, + "CatalogService"); + } +} +``` + +#### Step 6: Consumer Service - Configure Remote Service URL + +```json +// appsettings.json +"RemoteServices": { + "CatalogService": { + "BaseUrl": "http://localhost:44361" + } +} +``` + +#### Step 7: Use Integration Service + +```csharp +public class OrderAppService : ApplicationService +{ + private readonly IProductIntegrationService _productIntegrationService; + + public async Task> GetListAsync() + { + var orders = await _orderRepository.GetListAsync(); + var productIds = orders.Select(o => o.ProductId).Distinct().ToList(); + + // Call remote service via generated proxy + var products = await _productIntegrationService.GetProductsByIdsAsync(productIds); + // ... + } +} +``` + +> **Why Integration Services?** Application services are for UI - they have different authorization, validation, and optimization needs. Integration services are designed specifically for inter-service communication. + +**When to use:** Need immediate response, data required to complete current operation (e.g., get product details to display in order list). + +### 2. Distributed Events (Asynchronous) + +Use RabbitMQ-based events for loose coupling. + +**When to use:** +- Notifying other services about state changes (e.g., "order placed", "stock updated") +- Operations that don't need immediate response +- When services should remain independent and decoupled + +```csharp +// Define ETO in Contracts project +[EventName("Product.StockChanged")] +public class StockCountChangedEto +{ + public Guid ProductId { get; set; } + public int NewCount { get; set; } +} + +// Publish +await _distributedEventBus.PublishAsync(new StockCountChangedEto { ... }); + +// Subscribe in another service +public class StockChangedHandler : IDistributedEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(StockCountChangedEto eventData) { ... } +} +``` + +DbContext must implement `IHasEventInbox`, `IHasEventOutbox` for Outbox/Inbox pattern. + +## Performance: Entity Cache + +For frequently accessed data from other services, use Entity Cache: + +```csharp +// Register +context.Services.AddEntityCache(); + +// Use - auto-invalidates on entity changes +private readonly IEntityCache _productCache; + +public async Task GetProductAsync(Guid id) +{ + return await _productCache.GetAsync(id); +} +``` + +## Pre-Configured Infrastructure + +- **RabbitMQ** - Distributed events with Outbox/Inbox +- **Redis** - Distributed cache and locking +- **YARP** - API Gateway +- **OpenIddict** - Auth server + +## Best Practices + +- **Choose communication wisely** - Synchronous for queries needing immediate data, asynchronous for notifications and state changes +- **Use Integration Services** - Not application services for inter-service calls +- **Cache remote data** - Use Entity Cache or IDistributedCache for frequently accessed data +- **Share only Contracts** - Never share implementations +- **Idempotent handlers** - Events may be delivered multiple times +- **Database per service** - Each service owns its database diff --git a/.github/skills/abp-module/SKILL.md b/.github/skills/abp-module/SKILL.md new file mode 100644 index 0000000000..def061f3cb --- /dev/null +++ b/.github/skills/abp-module/SKILL.md @@ -0,0 +1,234 @@ +--- +name: abp-module +description: ABP reusable Module solution template - EF Core + MongoDB dual support, virtual methods for extensibility, DbTablePrefix, module options pattern, entity extension, separate connection string. Use when building or reviewing reusable ABP modules that will be distributed or consumed by other solutions. +--- + +# ABP Module Solution Template + +> **Docs**: https://abp.io/docs/latest/solution-templates/application-module + +This template is for developing reusable ABP modules. Key requirement: **extensibility** - consumers must be able to override and customize module behavior. + +## Solution Structure + +``` +MyModule/ +├── src/ +│ ├── MyModule.Domain.Shared/ # Constants, enums, localization +│ ├── MyModule.Domain/ # Entities, repository interfaces, domain services +│ ├── MyModule.Application.Contracts/ # DTOs, service interfaces +│ ├── MyModule.Application/ # Service implementations +│ ├── MyModule.EntityFrameworkCore/ # EF Core implementation +│ ├── MyModule.MongoDB/ # MongoDB implementation +│ ├── MyModule.HttpApi/ # REST controllers +│ ├── MyModule.HttpApi.Client/ # Client proxies +│ ├── MyModule.Web/ # MVC/Razor Pages UI +│ └── MyModule.Blazor/ # Blazor UI +├── test/ +│ └── MyModule.Tests/ +└── host/ + └── MyModule.HttpApi.Host/ # Test host application +``` + +## Database Independence + +Support both EF Core and MongoDB: + +### Repository Interface (Domain) +```csharp +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); + Task> GetListByAuthorAsync(Guid authorId); +} +``` + +### EF Core Implementation +```csharp +public class BookRepository : EfCoreRepository, IBookRepository +{ + public async Task FindByNameAsync(string name) + { + var dbSet = await GetDbSetAsync(); + return await dbSet.FirstOrDefaultAsync(b => b.Name == name); + } +} +``` + +### MongoDB Implementation +```csharp +public class BookRepository : MongoDbRepository, IBookRepository +{ + public async Task FindByNameAsync(string name) + { + var queryable = await GetQueryableAsync(); + return await queryable.FirstOrDefaultAsync(b => b.Name == name); + } +} +``` + +## Table/Collection Prefix + +Allow customization to avoid naming conflicts: + +```csharp +// Domain.Shared +public static class MyModuleDbProperties +{ + public static string DbTablePrefix { get; set; } = "MyModule"; + public static string DbSchema { get; set; } = null; + + public const string ConnectionStringName = "MyModule"; +} +``` + +Usage: +```csharp +builder.Entity(b => +{ + b.ToTable(MyModuleDbProperties.DbTablePrefix + "Books", MyModuleDbProperties.DbSchema); +}); +``` + +## Module Options + +Provide configuration options: + +```csharp +// Domain +public class MyModuleOptions +{ + public bool EnableFeatureX { get; set; } = true; + public int MaxItemCount { get; set; } = 100; +} +``` + +Usage in module: +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + options.EnableFeatureX = true; + }); +} +``` + +Usage in service: +```csharp +public class MyService : ITransientDependency +{ + private readonly MyModuleOptions _options; + + public MyService(IOptions options) + { + _options = options.Value; + } +} +``` + +## Extensibility Points + +### Virtual Methods (Critical for Modules!) +When developing a reusable module, **all public and protected methods must be virtual** to allow consumers to override behavior: + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + // ✅ Public methods MUST be virtual + public virtual async Task CreateAsync(CreateBookDto input) + { + var book = await CreateBookEntityAsync(input); + await _bookRepository.InsertAsync(book); + return _bookMapper.MapToDto(book); + } + + // ✅ Use protected virtual for helper methods (not private) + protected virtual Task CreateBookEntityAsync(CreateBookDto input) + { + return Task.FromResult(new Book( + GuidGenerator.Create(), + input.Name, + input.Price + )); + } + + // ❌ WRONG for modules - private methods cannot be overridden + // private Book CreateBook(CreateBookDto input) { ... } +} +``` + +This allows module consumers to: +- Override specific methods without copying entire class +- Extend functionality while preserving base behavior +- Customize module behavior for their needs + +### Entity Extension +Support object extension system: +```csharp +public class MyModuleModuleExtensionConfigurator +{ + public static void Configure() + { + OneTimeRunner.Run(() => + { + ObjectExtensionManager.Instance.Modules() + .ConfigureMyModule(module => + { + module.ConfigureBook(book => + { + book.AddOrUpdateProperty("CustomProperty"); + }); + }); + }); + } +} +``` + +## Localization + +```csharp +// Domain.Shared +[LocalizationResourceName("MyModule")] +public class MyModuleResource +{ +} + +// Module configuration +Configure(options => +{ + options.Resources + .Add("en") + .AddVirtualJson("/Localization/MyModule"); +}); +``` + +## Permission Definition + +```csharp +public class MyModulePermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var myGroup = context.AddGroup( + MyModulePermissions.GroupName, + L("Permission:MyModule")); + + myGroup.AddPermission( + MyModulePermissions.Books.Default, + L("Permission:Books")); + } +} +``` + +## Best Practices + +1. **Virtual methods** - All public/protected methods must be `virtual` for extensibility +2. **Protected virtual helpers** - Use `protected virtual` instead of `private` for helper methods +3. **Database agnostic** - Support both EF Core and MongoDB +4. **Configurable** - Use options pattern for customization +5. **Localizable** - Use localization for all user-facing text +6. **Table prefix** - Allow customization to avoid conflicts +7. **Separate connection string** - Support dedicated database +8. **No dependencies on host** - Module should be self-contained +9. **Test with host app** - Include a host application for testing diff --git a/.github/skills/abp-multi-tenancy/SKILL.md b/.github/skills/abp-multi-tenancy/SKILL.md new file mode 100644 index 0000000000..3ad892ef15 --- /dev/null +++ b/.github/skills/abp-multi-tenancy/SKILL.md @@ -0,0 +1,161 @@ +--- +name: abp-multi-tenancy +description: ABP Multi-Tenancy - IMultiTenant interface, CurrentTenant, CurrentTenant.Change(), DataFilter.Disable(IMultiTenant), tenant resolution order, database-per-tenant. Use when working with multi-tenant features, tenant-specific data isolation, or switching tenant context. +--- + +# ABP Multi-Tenancy + +> **Docs**: https://abp.io/docs/latest/framework/architecture/multi-tenancy + +## Making Entities Multi-Tenant + +Implement `IMultiTenant` interface to make entities tenant-aware: + +```csharp +public class Product : AggregateRoot, IMultiTenant +{ + public Guid? TenantId { get; set; } // Required by IMultiTenant + + public string Name { get; private set; } + public decimal Price { get; private set; } + + protected Product() { } + + public Product(Guid id, string name, decimal price) : base(id) + { + Name = name; + Price = price; + // TenantId is automatically set from CurrentTenant.Id + } +} +``` + +**Key points:** +- `TenantId` is **nullable** - `null` means entity belongs to Host +- ABP **automatically filters** queries by current tenant +- ABP **automatically sets** `TenantId` when creating entities + +## Accessing Current Tenant + +Use `CurrentTenant` property (available in base classes) or inject `ICurrentTenant`: + +```csharp +public class ProductAppService : ApplicationService +{ + public async Task DoSomethingAsync() + { + // Available from base class + var tenantId = CurrentTenant.Id; // Guid? - null for host + var tenantName = CurrentTenant.Name; // string? + var isAvailable = CurrentTenant.IsAvailable; // true if Id is not null + } +} + +// In other services +public class MyService : ITransientDependency +{ + private readonly ICurrentTenant _currentTenant; + public MyService(ICurrentTenant currentTenant) => _currentTenant = currentTenant; +} +``` + +## Switching Tenant Context + +Use `CurrentTenant.Change()` to temporarily switch tenant (useful in host context): + +```csharp +public class ProductManager : DomainService +{ + private readonly IRepository _productRepository; + + public async Task GetProductCountAsync(Guid? tenantId) + { + // Switch to specific tenant + using (CurrentTenant.Change(tenantId)) + { + return await _productRepository.GetCountAsync(); + } + // Automatically restored to previous tenant after using block + } + + public async Task DoHostOperationAsync() + { + // Switch to host context + using (CurrentTenant.Change(null)) + { + // Operations here are in host context + } + } +} +``` + +> **Important**: Always use `Change()` with a `using` statement. + +## Disabling Multi-Tenant Filter + +To query all tenants' data (only works with single database): + +```csharp +public class ProductManager : DomainService +{ + public async Task GetAllProductCountAsync() + { + // DataFilter is available from base class + using (DataFilter.Disable()) + { + return await _productRepository.GetCountAsync(); + // Returns count from ALL tenants + } + } +} +``` + +> **Note**: This doesn't work with separate databases per tenant. + +## Database Architecture Options + +| Approach | Description | Use Case | +|----------|-------------|----------| +| Single Database | All tenants share one database | Simple, cost-effective | +| Database per Tenant | Each tenant has dedicated database | Data isolation, compliance | +| Hybrid | Mix of shared and dedicated | Flexible, premium tenants | + +Connection strings are configured per tenant in Tenant Management module. + +## Best Practices + +1. **Always implement `IMultiTenant`** for tenant-specific entities +2. **Never manually filter by `TenantId`** - ABP does it automatically +3. **Don't change `TenantId` after creation** - it moves entity between tenants +4. **Use `Change()` scope carefully** - nested scopes are supported +5. **Test both host and tenant contexts** - ensure proper data isolation +6. **Consider nullable `TenantId`** - entity may be host-only or shared + +## Enabling Multi-Tenancy + +```csharp +Configure(options => +{ + options.IsEnabled = true; // Enabled by default in ABP templates +}); +``` + +Check `MultiTenancyConsts.IsEnabled` in your solution for centralized control. + +## Tenant Resolution + +ABP resolves current tenant from (in order): +1. Current user's claims +2. Query string (`?__tenant=...`) +3. Route (`/{__tenant}/...`) +4. HTTP header (`__tenant`) +5. Cookie (`__tenant`) +6. Domain/subdomain (if configured) + +For subdomain-based resolution: +```csharp +Configure(options => +{ + options.AddDomainTenantResolver("{0}.mydomain.com"); +}); +``` diff --git a/.github/skills/abp-mvc/SKILL.md b/.github/skills/abp-mvc/SKILL.md new file mode 100644 index 0000000000..f7e4cc0bff --- /dev/null +++ b/.github/skills/abp-mvc/SKILL.md @@ -0,0 +1,257 @@ +--- +name: abp-mvc +description: ABP MVC and Razor Pages UI - AbpPageModel, abp tag helpers (abp-card, abp-dynamic-form, abp-modal), JavaScript abp.ajax/abp.auth/abp.notify, DataTables integration, bundle/minification. Use when working on MVC or Razor Pages UI in ABP projects. +--- + +# ABP MVC / Razor Pages UI + +> **Docs**: https://abp.io/docs/latest/framework/ui/mvc-razor-pages/overall + +## Razor Page Model +```csharp +public class IndexModel : AbpPageModel +{ + private readonly IBookAppService _bookAppService; + + public List Books { get; set; } + + public IndexModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnGetAsync() + { + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + Books = result.Items.ToList(); + } +} +``` + +## Razor Page View +```html +@page +@model IndexModel + + + + + +

@L["Books"]

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

@L["Books"]

+ +@* With parameters *@ +

@L["WelcomeMessage", Model.UserName]

+``` + +## JavaScript API +```javascript +// Localization +var text = abp.localization.getResource('BookStore')('Books'); + +// Authorization +if (abp.auth.isGranted('BookStore.Books.Create')) { + // Show create button +} + +// Settings +var maxCount = abp.setting.get('BookStore.MaxItemCount'); + +// Ajax with automatic error handling +abp.ajax({ + url: '/api/app/book', + type: 'POST', + data: JSON.stringify(bookData) +}).then(function(result) { + // Success +}); + +// Notifications +abp.notify.success('Book created successfully!'); +abp.notify.error('An error occurred!'); + +// Confirmation +abp.message.confirm('Are you sure?').then(function(confirmed) { + if (confirmed) { + // User confirmed + } +}); +``` + +## DataTables Integration +```javascript +var dataTable = $('#BooksTable').DataTable( + abp.libs.datatables.normalizeConfiguration({ + serverSide: true, + paging: true, + ajax: abp.libs.datatables.createAjax(bookService.getList), + columnDefs: [ + { + title: l('Name'), + data: 'name' + }, + { + title: l('Price'), + data: 'price', + render: function(data) { + return data.toFixed(2); + } + }, + { + title: l('Actions'), + rowAction: { + items: [ + { + text: l('Edit'), + visible: abp.auth.isGranted('BookStore.Books.Edit'), + action: function(data) { + editModal.open({ id: data.record.id }); + } + }, + { + text: l('Delete'), + visible: abp.auth.isGranted('BookStore.Books.Delete'), + confirmMessage: function(data) { + return l('BookDeletionConfirmationMessage', data.record.name); + }, + action: function(data) { + bookService.delete(data.record.id).then(function() { + abp.notify.success(l('SuccessfullyDeleted')); + dataTable.ajax.reload(); + }); + } + } + ] + } + } + ] + }) +); +``` + +## Modal Pages +**CreateModal.cshtml:** +```html +@page +@model CreateModalModel + + + + + + + + + + +``` + +**CreateModal.cshtml.cs:** +```csharp +public class CreateModalModel : AbpPageModel +{ + [BindProperty] + public CreateBookDto Book { get; set; } + + private readonly IBookAppService _bookAppService; + + public CreateModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnPostAsync() + { + await _bookAppService.CreateAsync(Book); + return NoContent(); + } +} +``` + +## Bundle & Minification +```csharp +Configure(options => +{ + options.StyleBundles.Configure( + StandardBundles.Styles.Global, + bundle => bundle.AddFiles("/styles/my-styles.css") + ); +}); +``` diff --git a/.github/skills/abp-testing/SKILL.md b/.github/skills/abp-testing/SKILL.md new file mode 100644 index 0000000000..bd41ef4a32 --- /dev/null +++ b/.github/skills/abp-testing/SKILL.md @@ -0,0 +1,269 @@ +--- +name: abp-testing +description: ABP testing patterns - integration tests over unit tests, GetRequiredService, IDataSeedContributor, Shouldly assertions, AddAlwaysAllowAuthorization, NSubstitute mocking, WithUnitOfWorkAsync. Use when writing or reviewing tests for application services, domain services, or repositories in ABP projects. +--- + +# ABP Testing Patterns + +> **Docs**: https://abp.io/docs/latest/testing + +## Test Project Structure + +| Project | Purpose | Base Class | +|---------|---------|------------| +| `*.Domain.Tests` | Domain logic, entities, domain services | `*DomainTestBase` | +| `*.Application.Tests` | Application services | `*ApplicationTestBase` | +| `*.EntityFrameworkCore.Tests` | Repository implementations | `*EntityFrameworkCoreTestBase` | + +## Integration Test Approach + +ABP recommends integration tests over unit tests: +- Tests run with real services and database (SQLite in-memory) +- No mocking of internal services +- Each test gets a fresh database instance + +## Application Service Test + +```csharp +public class BookAppService_Tests : MyProjectApplicationTestBase +{ + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Books() + { + // Act + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + + // Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "Test Book"); + } + + [Fact] + public async Task Should_Create_Book() + { + // Arrange + var input = new CreateBookDto + { + Name = "New Book", + Price = 19.99m + }; + + // Act + var result = await _bookAppService.CreateAsync(input); + + // Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New Book"); + result.Price.ShouldBe(19.99m); + } + + [Fact] + public async Task Should_Not_Create_Book_With_Invalid_Name() + { + // Arrange + var input = new CreateBookDto + { + Name = "", // Invalid + Price = 10m + }; + + // Act & Assert + await Should.ThrowAsync(async () => + { + await _bookAppService.CreateAsync(input); + }); + } +} +``` + +## Domain Service Test + +```csharp +public class BookManager_Tests : MyProjectDomainTestBase +{ + private readonly BookManager _bookManager; + private readonly IBookRepository _bookRepository; + + public BookManager_Tests() + { + _bookManager = GetRequiredService(); + _bookRepository = GetRequiredService(); + } + + [Fact] + public async Task Should_Create_Book() + { + // Act + var book = await _bookManager.CreateAsync("Test Book", 29.99m); + + // Assert + book.ShouldNotBeNull(); + book.Name.ShouldBe("Test Book"); + book.Price.ShouldBe(29.99m); + } + + [Fact] + public async Task Should_Not_Allow_Duplicate_Book_Name() + { + // Arrange + await _bookManager.CreateAsync("Existing Book", 10m); + + // Act & Assert + var exception = await Should.ThrowAsync(async () => + { + await _bookManager.CreateAsync("Existing Book", 20m); + }); + + exception.Code.ShouldBe("MyProject:BookNameAlreadyExists"); + } +} +``` + +## Test Naming Convention + +Use descriptive names: +```csharp +// Pattern: Should_ExpectedBehavior_When_Condition +public async Task Should_Create_Book_When_Input_Is_Valid() +public async Task Should_Throw_BusinessException_When_Name_Already_Exists() +public async Task Should_Return_Empty_List_When_No_Books_Exist() +``` + +## Arrange-Act-Assert (AAA) + +```csharp +[Fact] +public async Task Should_Update_Book_Price() +{ + // Arrange + var bookId = await CreateTestBookAsync(); + var newPrice = 39.99m; + + // Act + var result = await _bookAppService.UpdateAsync(bookId, new UpdateBookDto + { + Price = newPrice + }); + + // Assert + result.Price.ShouldBe(newPrice); +} +``` + +## Assertions with Shouldly + +ABP uses Shouldly library: +```csharp +result.ShouldNotBeNull(); +result.Name.ShouldBe("Expected Name"); +result.Price.ShouldBeGreaterThan(0); +result.Items.ShouldContain(x => x.Id == expectedId); +result.Items.ShouldBeEmpty(); +result.Items.Count.ShouldBe(5); + +// Exception assertions +await Should.ThrowAsync(async () => +{ + await _service.DoSomethingAsync(); +}); + +var ex = await Should.ThrowAsync(async () => +{ + await _service.DoSomethingAsync(); +}); +ex.Code.ShouldBe("MyProject:ErrorCode"); +``` + +## Test Data Seeding + +```csharp +public class MyProjectTestDataSeedContributor : IDataSeedContributor, ITransientDependency +{ + public static readonly Guid TestBookId = Guid.Parse("..."); + + private readonly IBookRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public async Task SeedAsync(DataSeedContext context) + { + await _bookRepository.InsertAsync( + new Book(TestBookId, "Test Book", 19.99m, Guid.Empty), + autoSave: true + ); + } +} +``` + +## Disabling Authorization in Tests + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddAlwaysAllowAuthorization(); +} +``` + +## Mocking External Services + +Use NSubstitute when needed: +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var emailSender = Substitute.For(); + emailSender.SendAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + context.Services.AddSingleton(emailSender); +} +``` + +## Testing with Specific User + +```csharp +[Fact] +public async Task Should_Get_Current_User_Books() +{ + // Login as specific user + await WithUnitOfWorkAsync(async () => + { + using (CurrentUser.Change(TestData.UserId)) + { + var result = await _bookAppService.GetMyBooksAsync(); + result.Items.ShouldAllBe(b => b.CreatorId == TestData.UserId); + } + }); +} +``` + +## Testing Multi-Tenancy + +```csharp +[Fact] +public async Task Should_Filter_Books_By_Tenant() +{ + using (CurrentTenant.Change(TestData.TenantId)) + { + var result = await _bookAppService.GetListAsync(new GetBookListDto()); + // Results should be filtered by tenant + } +} +``` + +## Best Practices + +- Each test should be independent +- Don't share state between tests +- Use meaningful test data +- Test edge cases and error conditions +- Keep tests focused on single behavior +- Use test data seeders for common data +- Avoid testing framework internals diff --git a/applications/Unity.GrantManager/.github/skills/code-review/SKILL.md b/.github/skills/code-review/SKILL.md similarity index 100% rename from applications/Unity.GrantManager/.github/skills/code-review/SKILL.md rename to .github/skills/code-review/SKILL.md diff --git a/applications/Unity.GrantManager/.github/skills/debug-issue/SKILL.md b/.github/skills/debug-issue/SKILL.md similarity index 100% rename from applications/Unity.GrantManager/.github/skills/debug-issue/SKILL.md rename to .github/skills/debug-issue/SKILL.md diff --git a/applications/Unity.GrantManager/.github/skills/generate-docs/SKILL.md b/.github/skills/generate-docs/SKILL.md similarity index 100% rename from applications/Unity.GrantManager/.github/skills/generate-docs/SKILL.md rename to .github/skills/generate-docs/SKILL.md diff --git a/applications/Unity.GrantManager/.github/skills/refactor-code/SKILL.md b/.github/skills/refactor-code/SKILL.md similarity index 100% rename from applications/Unity.GrantManager/.github/skills/refactor-code/SKILL.md rename to .github/skills/refactor-code/SKILL.md diff --git a/applications/Unity.GrantManager/.github/skills/setup-component/SKILL.md b/.github/skills/setup-component/SKILL.md similarity index 100% rename from applications/Unity.GrantManager/.github/skills/setup-component/SKILL.md rename to .github/skills/setup-component/SKILL.md diff --git a/applications/Unity.GrantManager/.github/skills/write-tests/SKILL.md b/.github/skills/write-tests/SKILL.md similarity index 100% rename from applications/Unity.GrantManager/.github/skills/write-tests/SKILL.md rename to .github/skills/write-tests/SKILL.md diff --git a/applications/Unity.GrantManager/.vscode/settings.json b/applications/Unity.GrantManager/.vscode/settings.json new file mode 100644 index 0000000000..9b600c9663 --- /dev/null +++ b/applications/Unity.GrantManager/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "github.copilot.chat.commitMessageGeneration.instructions": [ + { + "text": "Format commit message starting with [AB#] where is extracted from the branch name (e.g., from 'feature/AB#32037-...' extract '32037' as ID), followed by a short description. Aim for 50 characters after the prefix when possible but prioritize clarity." + } + ] +} \ No newline at end of file