Skip to content

Commit e637cdc

Browse files
committed
Add documentation about cross project middleware
1 parent e662eea commit e637cdc

File tree

5 files changed

+251
-162
lines changed

5 files changed

+251
-162
lines changed

.github/copilot-instructions.md

Lines changed: 0 additions & 162 deletions
This file was deleted.

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ public class LoggingMiddleware
111111
}
112112
```
113113

114+
**Cross-Assembly Limitation**: Middleware must be defined in the same project as handlers. The source generator only has access to the current project's source code. Use linked files (`<Compile Include="..." Link="..." />` in `.csproj`) to share middleware across projects, and declare middleware classes as `internal` to avoid type conflicts.
115+
114116
### Execution Semantics
115117

116118
- **Invoke/InvokeAsync**: Requires EXACTLY one handler. Throws if zero or multiple handlers found.

docs/guide/middleware.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,128 @@ public class PartialMiddleware
363363
}
364364
```
365365

366+
## Cross-Assembly Middleware Limitation
367+
368+
### Understanding the Limitation
369+
370+
Middleware must be defined in the **same project** as your message handlers. This is because middleware is discovered and woven into handler wrappers at compile-time by the source generator, which only has access to the current project's source code.
371+
372+
**This will NOT work:**
373+
374+
```text
375+
Solution/
376+
├── Common.Middleware/ # Project A
377+
│ └── LoggingMiddleware.cs # ❌ Won't be discovered
378+
└── Orders.Handlers/ # Project B (references A)
379+
└── OrderHandler.cs # Handler generated without logging
380+
```
381+
382+
The source generator in `Orders.Handlers` cannot see the `LoggingMiddleware` source code from the referenced `Common.Middleware` project.
383+
384+
### Recommended Solution: Linked Files
385+
386+
The recommended approach is to use **linked files** to share middleware source code across multiple projects:
387+
388+
**Project Structure:**
389+
390+
```text
391+
Solution/
392+
├── Common.Middleware/
393+
│ └── Middleware/
394+
│ ├── LoggingMiddleware.cs
395+
│ ├── ValidationMiddleware.cs
396+
│ └── AuthorizationMiddleware.cs
397+
├── Orders.Handlers/
398+
│ ├── OrderHandler.cs
399+
│ └── Middleware/ # Linked files from Common.Middleware
400+
│ ├── LoggingMiddleware.cs (link)
401+
│ ├── ValidationMiddleware.cs (link)
402+
│ └── AuthorizationMiddleware.cs (link)
403+
└── Products.Handlers/
404+
├── ProductHandler.cs
405+
└── Middleware/ # Same linked files
406+
└── ...
407+
```
408+
409+
**Create linked files in your `.csproj`:**
410+
411+
```xml
412+
<ItemGroup>
413+
<!-- Link middleware files from Common.Middleware project -->
414+
<Compile Include="..\Common.Middleware\Middleware\LoggingMiddleware.cs" Link="Middleware\LoggingMiddleware.cs" />
415+
<Compile Include="..\Common.Middleware\Middleware\ValidationMiddleware.cs" Link="Middleware\ValidationMiddleware.cs" />
416+
<Compile Include="..\Common.Middleware\Middleware\AuthorizationMiddleware.cs" Link="Middleware\AuthorizationMiddleware.cs" />
417+
</ItemGroup>
418+
```
419+
420+
**Use `internal` to avoid conflicts:**
421+
422+
Since the same middleware source file is compiled into multiple assemblies, declare middleware classes as `internal` to prevent type conflicts:
423+
424+
```csharp
425+
// LoggingMiddleware.cs (in Common.Middleware)
426+
namespace Common.Middleware;
427+
428+
// ✅ Use internal to avoid conflicts across assemblies
429+
internal class LoggingMiddleware
430+
{
431+
private readonly ILogger<LoggingMiddleware> _logger;
432+
433+
public LoggingMiddleware(ILogger<LoggingMiddleware> logger)
434+
{
435+
_logger = logger;
436+
}
437+
438+
public void Before(object message)
439+
{
440+
_logger.LogInformation("Handling {MessageType}", message.GetType().Name);
441+
}
442+
443+
public void Finally(object message, Exception? ex)
444+
{
445+
if (ex != null)
446+
_logger.LogError(ex, "Failed handling {MessageType}", message.GetType().Name);
447+
else
448+
_logger.LogInformation("Completed {MessageType}", message.GetType().Name);
449+
}
450+
}
451+
```
452+
453+
### Alternative: Define Per-Project
454+
455+
If middleware is project-specific, define it directly in each handler project:
456+
457+
```csharp
458+
// Orders.Handlers/Middleware/OrderValidationMiddleware.cs
459+
namespace Orders.Handlers.Middleware;
460+
461+
internal class OrderValidationMiddleware
462+
{
463+
public HandlerResult Before(IOrderCommand command)
464+
{
465+
if (!IsValid(command))
466+
return HandlerResult.ShortCircuit(Result.Invalid("Invalid order command"));
467+
468+
return HandlerResult.Continue();
469+
}
470+
}
471+
```
472+
473+
### Why This Limitation Exists
474+
475+
The source generator analyzes your code at compile-time to create handler wrappers with middleware baked in for maximum performance. This compile-time approach:
476+
477+
- ✅ Eliminates runtime reflection
478+
- ✅ Provides strongly-typed middleware parameters
479+
- ✅ Enables interceptors for near-direct call performance
480+
- ❌ Requires middleware source in the same compilation
481+
482+
Future versions may support cross-assembly middleware discovery via metadata, but for now, linked files provide a clean workaround.
483+
484+
### Example: ModularMonolith Sample
485+
486+
See the `samples/ModularMonolithSample/` directory for a complete example of middleware in a modular architecture.
487+
366488
## Best Practices
367489

368490
### 1. Keep Middleware Focused

0 commit comments

Comments
 (0)