Skip to content

Commit 98cf992

Browse files
committed
Improve the copilot-instructions.md by moving details about adding plugins into a skill
1 parent b169260 commit 98cf992

3 files changed

Lines changed: 185 additions & 46 deletions

File tree

.github/copilot-instructions.md

Lines changed: 35 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
- **Key Patterns:**
1111
- AppBoot plugin system with isolated LoadContexts per module
1212
- Hide external frameworks (EF Core) from business logic (Modules) via abstraction (IRepository/IUnitOfWork)
13-
- **Modules:** `Sales`, `ProductsManagement`, `PersonsManagement`, `Notifications`, `Export`
13+
- **Modules:** `Sales`, `ProductsManagement`, `PersonsManagement`, `Notifications`, `Export`. Other modules may be added
1414

1515
---
1616

@@ -25,6 +25,7 @@ Modules/
2525
├─ Contracts/ # Pure interfaces/DTOs - NO dependencies, NO logic
2626
├─ {Module}/ # e.g., Sales, ProductsManagement
2727
│ ├─ {Module}.Services/ # Business logic - [Service] attribute for DI
28+
│ ├─ {Module}.Services.UnitTests/ # Unit tests
2829
│ ├─ {Module}.DataModel/ # Entities only - NO logic, NO EF references
2930
│ ├─ {Module}.DbContext/ # EF DbContext (DO NOT MODIFY - generated)
3031
│ └─ {Module}.ConsoleCommands/ # Console commands via IConsoleCommand
@@ -76,40 +77,17 @@ using (IUnitOfWork uof = repository.CreateUnitOfWork())
7677
- **Never** use `DbContext` directly in Services layer (enforced by project references)
7778
- `IUnitOfWork` inherits from `IRepository`. It reads data to be modified and tracks changes for `SaveChanges()`.
7879

79-
### 3) Plugin Loading (Program.cs)
80-
Modules are loaded dynamically at runtime:
81-
```csharp
82-
options
83-
.AddPlugin("Sales.Services", "Sales.DbContext", "Sales.ConsoleCommands")
84-
.AddPlugin("Notifications.Services");
85-
```
86-
- First parameter: primary assembly name (must have `EnableDynamicLoading=true`)
87-
- Additional params: dependent assemblies in same LoadContext
88-
- Specify all assemblies that are not referenced by any other assembly, grouped by module. Plugin == Module.
89-
- One LoadContext is created for each defined plugin.
90-
- Convention: `{ModuleName}.{AssemblySuffix}` matches folder structure. Note: the `Modules` and `Infra` folders are NOT part of the namespace, as it is a physical organization only.
91-
- See `UI/ConsoleUi/Program.cs` for full bootstrap example
92-
93-
### 4) Module Initialization
94-
Modules implement `IModule` for startup logic:
95-
```csharp
96-
[Service(typeof(IModule), ServiceLifetime.Singleton)]
97-
class SalesServicesModule(INotificationService notificationService) : IModule
98-
{
99-
public void Initialize(IHost host)
100-
{
101-
notificationService.NotifyAlive(this);
102-
}
103-
}
104-
```
105-
- `Initialize()` called once at app startup, on `Main()` function
80+
### 3) Plugin Loading & Module Initialization
81+
Modules load dynamically via `.AddPlugin("{Module}.Services", "{Module}.DbContext", ...)` in `UI/ConsoleUi/Program.cs`; each implements `IModule` for startup logic.
10682

107-
### 5) Entity Interceptors
83+
> For registration rules, `IModule` pattern, and step-by-step setup, use the **add-module-plugin** skill.
84+
85+
### 4) Entity Interceptors
10886
Hook into EF lifecycle via `IEntityInterceptor<T>` or `IEntityInterceptor`:
10987
- Registered automatically via `[Service]` attribute
11088
- Applied by DataAccess layer (no direct EF SaveChanges calls)
11189

112-
#### 5.1.) Specific Entity Interceptor
90+
#### 4.1.) Specific Entity Interceptor
11391

11492
- Use `IEntityInterceptor<T>` to register interceptors that will be applied to a specific entity type ONLY.
11593
- Implement by inheriting from `EntityInterceptor<T>` and overriding methods like `OnSave()`, `OnDelete()`, etc.
@@ -125,7 +103,7 @@ class SalesOrderCalculationsInterceptor : EntityInterceptor<SalesOrderHeader>
125103
}
126104
```
127105

128-
#### 5.2.) Global Entity Interceptor
106+
#### 4.2.) Global Entity Interceptor
129107

130108
- Use `IEntityInterceptor` to register interceptors that will be applied to ALL entities.
131109
- Implement by inheriting from `GlobalEntityInterceptor` and overriding methods like `OnSave()`, `OnDelete()`, etc.
@@ -155,12 +133,9 @@ dotnet run --project UI/ConsoleUi # Run console app
155133
```
156134

157135
### Plugin Build Dependencies
158-
**Problem:** Plugin assemblies (e.g., `Sales.Services`) aren't referenced directly by host, so VS may not build them.
136+
Plugin assemblies are not referenced directly, so `dotnet build` skips them unless build-order dependencies are declared in `AppInfraDemo.sln`.
159137

160-
**Solution:** Add to Visual Studio build dependencies:
161-
- Right-click solution → **Project Build Dependencies**
162-
- Add plugin projects as dependencies of `UI/ConsoleUi`
163-
- For multi-assembly plugins (e.g., `Sales.Services` + `Sales.DbContext`), add dependent asseblies as dependencies of the primary plugin assembly only (e.g., `Sales.Services`)
138+
> For the full pattern and GUID lookup steps, use the **add-module-plugin** skill.
164139
165140
### Project Configuration
166141
- Plugin assemblies which are dynamically loaded, as no other assemblies references them, have `<EnableDynamicLoading>true</EnableDynamicLoading>`
@@ -183,7 +158,7 @@ dotnet run --project UI/ConsoleUi # Run console app
183158
## Protected Areas (DO NOT MODIFY)
184159
- `Infra/**` - Framework code, touch only via extension methods/adapters
185160
- `*/DbContext/**` - EF-generated or scaffolded, modify via migrations
186-
- `*.csproj` files - Avoid manual edits (managed by SDK/tooling)
161+
- `*.csproj` files - Avoid manual edits to existing project files unless adding a new module or a test project (see `add-module-plugin` or `unit-testing` skills).
187162

188163
If modification requested in these areas, suggest:
189164
- Extension methods (for Infra)
@@ -224,19 +199,37 @@ Checklist:
224199
| Add service | `[Service(typeof(IFoo))]` on implementation in `*.Services/` |
225200
| Read data | Inject `IRepository`, use `GetEntities<T>()` |
226201
| Write data | `using var uof = repository.CreateUnitOfWork()` |
227-
| Add module | Create folder under `Modules/`, add to `Program.cs` `.AddPlugin()` |
228202
| Console command | Implement `IConsoleCommand` in `*.ConsoleCommands/` |
229203
| Share types | Add to `Modules/Contracts/{Module}/` (interfaces/DTOs only) |
230204

231205
---
232206

207+
## Skills
208+
209+
Domain-specific guidance documents located in `.github/skills/`. When a task matches a skill's domain, read its `SKILL.md` file for detailed instructions.
210+
211+
| Skill | Purpose |
212+
|-------|---------|
213+
| **add-module-plugin** | Step-by-step guide for adding new modules: folder structure, plugin registration, `IModule` initialization, build-order dependencies in `.sln` |
214+
| **unit-testing** | Test patterns using xUnit, NSubstitute, FluentAssertions: AAA structure, fake naming (`Stub`/`Mock`), collection assertions, `GetTarget` helpers |
215+
216+
Usage pattern in this file: `> For [topic], use the **skill-name** skill.`
217+
218+
---
219+
233220
## Tests
234-
- Unit tests in corresponded test project named as `{Assembly}.UnitTests` example: `Infra/AppBoot.UnitTests` (xUnit)
235-
- Test plugin loading with `AssembliesLoaderTests.cs` examples
221+
236222
- Run: `dotnet test`
237223

238-
### Unit Tests Naming Convention
239-
- `{MethodName}_{Scenario}_{ExpectedResult}` example: `CalculateTotal_OrderHasItems_ReturnsSum`
224+
> For Unit Test structure, naming, and fake patterns, use the **unit-testing** skill.`
240225
241226
### Integration Tests Naming Convention
242227
- `When{Scenario}_Then{ExpectedResult}` example: `WhenCreatingOrder_ThenOrderIsPersisted`
228+
229+
## Commit Messages
230+
231+
Always use this template for commit messages:
232+
233+
```
234+
[AI:{AgentType}, HUMAN:-, MODEL: {ModelNameAndVersion}] (#{TicketNumber}) {ShortDescription}
235+
```
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
---
2+
name: add-module-plugin
3+
description: "Step-by-step guide for adding a new module as a plugin: folder structure, plugin registration in Program.cs, module initialization, and build-order dependencies in AppInfraDemo.sln."
4+
version: 1.0.0
5+
language: C#
6+
framework: .NET 10.0
7+
---
8+
9+
# Add Module as Plugin Skill
10+
11+
## Overview
12+
13+
A **plugin** is one isolated `AssemblyLoadContext`.
14+
It has one primary assembly (the first argument in `.AddPlugin(...)`) and zero or more co-loaded assemblies (additional arguments in `.AddPlugin(...)` that are not referenced by any other assembly).
15+
Logically, may contain more **modules** if multiple `IModule` implementations are present, but we follow a one-plugin-per-module convention for simplicity.
16+
Follow the steps below in order when adding a new module.
17+
18+
---
19+
20+
## Step 1 — Folder & Project Structure
21+
22+
Create the standard sub-projects under `Modules/{Module}/`:
23+
24+
```
25+
Modules/{Module}/
26+
├─ {Module}.DataModel/ # Entities only — no logic, no EF references
27+
├─ {Module}.Services/ # Business logic — [Service] attribute for DI
28+
├─ {Module}.DbContext/ # EF DbContext (scaffolded/generated)
29+
└─ {Module}.ConsoleCommands/ # Optional — IConsoleCommand implementations
30+
```
31+
32+
Project configuration rules:
33+
- `{Module}.Services`, `{Module}.ConsoleCommands` → set `<EnableDynamicLoading>true</EnableDynamicLoading>`
34+
- `{Module}.DbContext` → use `<PrivateAssets>all</PrivateAssets>` on EF packages (prevents leaking EF to Services)
35+
- `{Module}.DataModel` → standard class library, no special flags
36+
37+
---
38+
39+
## Step 2 — Plugin Registration in Program.cs
40+
41+
Register the new module in `UI/ConsoleUi/Program.cs` via `.AddPlugin()`:
42+
43+
```csharp
44+
options
45+
.AddPlugin("Sales.Services", "Sales.DbContext", "Sales.ConsoleCommands")
46+
.AddPlugin("Notifications.Services")
47+
.AddPlugin("{Module}.Services", "{Module}.DbContext", "{Module}.ConsoleCommands"); // new
48+
```
49+
50+
Rules:
51+
- **First argument** — primary assembly name; must have `<EnableDynamicLoading>true</EnableDynamicLoading>`
52+
- **Additional arguments** — all co-loaded assemblies in the same `LoadContext` that are not referenced by any other assembly; must also have `<EnableDynamicLoading>true</EnableDynamicLoading>`
53+
- One `LoadContext` is created per `.AddPlugin(...)`
54+
- Naming convention: `{ModuleName}.{AssemblySuffix}``Modules/` and `Infra/` folders are physical only, not part of the namespace
55+
56+
---
57+
58+
## Step 3 — Module Initialization
59+
60+
Implement `IModule` in `{Module}.Services` for startup logic:
61+
62+
```csharp
63+
[Service(typeof(IModule), ServiceLifetime.Singleton)]
64+
internal sealed class {Module}ServicesModule(INotificationService notificationService) : IModule
65+
{
66+
public void Initialize(IHost host)
67+
{
68+
notificationService.NotifyAlive(this);
69+
}
70+
}
71+
```
72+
73+
- `Initialize()` is called once at app startup from `Main()`
74+
- Keep it lightweight — wire up cross-module notifications, warm caches, etc.
75+
- Inject only `Contracts` interfaces (no cross-module service types)
76+
77+
### Initialization Order
78+
79+
By default the order of `IModule.Initialize()` calls is non-deterministic.
80+
To control the order use `[Priority(int)]` attribute from `AppBoot/DependencyInjection` on the `IModule` implementation.
81+
82+
---
83+
84+
## Step 4 — Build-Order Dependencies in AppInfraDemo.sln
85+
86+
Plugin assemblies have `<EnableDynamicLoading>true</EnableDynamicLoading>` and are **not** referenced directly, so `dotnet build AppInfraDemo.sln` skips them unless build-order dependencies are declared explicitly.
87+
88+
Add `ProjectSection(ProjectDependencies) = postProject` blocks inside the relevant `Project(...)` entries in `AppInfraDemo.sln`.
89+
90+
### Rules
91+
92+
- The **primary** plugin assembly must be declared as a Project Dependency of `ConsoleUi`
93+
- Each co-loaded assembly (the additional params in `.AddPlugin(...)`) must be declared as a Project Dependency of the **primary** assembly
94+
95+
### Pattern
96+
97+
**`ConsoleUi` → primary plugin assemblies**
98+
99+
```
100+
Project("{FAE04EC0-...}") = "ConsoleUi", "UI\ConsoleUi\ConsoleUi.csproj", "{GUID-ConsoleUi}"
101+
ProjectSection(ProjectDependencies) = postProject
102+
{GUID-Sales.Services} = {GUID-Sales.Services}
103+
{GUID-Notifications.Services} = {GUID-Notifications.Services}
104+
{GUID-ProductsManagement.Services} = {GUID-ProductsManagement.Services}
105+
{GUID-PersonsManagement.Services} = {GUID-PersonsManagement.Services}
106+
{GUID-Export.Services} = {GUID-Export.Services}
107+
EndProjectSection
108+
EndProject
109+
```
110+
111+
**Primary plugin assembly → its co-loaded assemblies**
112+
113+
```
114+
Project("{FAE04EC0-...}") = "Sales.Services", "Modules\Sales\Sales.Services\Sales.Services.csproj", "{GUID-Sales.Services}"
115+
ProjectSection(ProjectDependencies) = postProject
116+
{GUID-Sales.DbContext} = {GUID-Sales.DbContext}
117+
{GUID-Sales.ConsoleCommands} = {GUID-Sales.ConsoleCommands}
118+
EndProjectSection
119+
EndProject
120+
```
121+
122+
If a plugin has no co-loaded assemblies (e.g., `.AddPlugin("Notifications.Services")`), no `ProjectDependencies` block is needed on that project.
123+
124+
### Finding GUIDs
125+
126+
GUIDs are on the `Project(...)` line of each entry in `AppInfraDemo.sln`:
127+
128+
```
129+
Project("{FAE04EC0-...}") = "Sales.DbContext", "Modules\Sales\Sales.DbContext\...", "{8ECFDB0C-9146-4C51-B8AF-3DC696492DAE}"
130+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
131+
use this GUID in ProjectDependencies
132+
```
133+
134+
### Via Visual Studio UI
135+
136+
Right-click the solution → **Project Build Dependencies** → select the dependant project and tick its dependencies. This writes the same `ProjectSection(ProjectDependencies)` blocks automatically.

.github/skills/unit-testing/CHEATSHEET.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ list.Should().ContainSingle(c => c.FirstName == "John")
5858
.Which.LastName.Should().Be("Doe");
5959
```
6060

61-
#### BeEquivalentTo — use when asserting the full collection matches
61+
#### BeEquivalentTo — use when asserting the full collection matches and order doesn't matter
6262

6363
```csharp
6464
// ✅ Full match, order-insensitive by default
@@ -87,6 +87,16 @@ list.Should().BeEquivalentTo(expected,
8787
.Excluding(c => c.CreatedDate)
8888
);
8989

90+
// ❌ Multiple assertions on individual items — brittle and verbose, order-dependent
91+
result.Should().HaveCount(3);
92+
result[0].CustomerName.Should().Be("Alpha Corp");
93+
result[0].OldestOverdueOrderDate.Should().Be(DateTime.Today.AddDays(-10));
94+
result[1].CustomerName.Should().Be("Beta Corp");
95+
result[1].OldestOverdueOrderDate.Should().Be(DateTime.Today.AddDays(-5));
96+
result[2].CustomerName.Should().Be("Gamma Corp");
97+
result[2].OldestOverdueOrderDate.Should().Be(DateTime.Today.AddDays(-3));
98+
99+
90100
// ❌ Over-specified — any unrelated property change breaks the test
91101
list.Should().BeEquivalentTo(new Customer
92102
{
@@ -101,9 +111,9 @@ list.Should().BeEquivalentTo(new Customer
101111
|---|---|
102112
| One item exists matching conditions | `ContainSingle(predicate)` |
103113
| At least one item matches | `Contain(predicate)` |
104-
| Exact collection match, all properties | `BeEquivalentTo(expected)` |
105-
| Exact collection match, selected properties | `BeEquivalentTo(expected, options => options.Including(...))` |
106-
| Ignore generated/audit properties | `BeEquivalentTo(expected, options => options.Excluding(...))` |
114+
| Order-insensitive collection match, all properties | `BeEquivalentTo(expected)` |
115+
| Order-insensitive collection match, selected properties | `BeEquivalentTo(expected, options => options.Including(...))` |
116+
| Order-insensitive collection match, ignoring generated/audit properties | `BeEquivalentTo(expected, options => options.Excluding(...))` |
107117
| Never | `list[0].Property` |
108118

109119
---

0 commit comments

Comments
 (0)