Skip to content

Commit bf5b082

Browse files
authored
Revise and expand extension documentation (#30)
Major updates to extension docs: clarified architecture fit, composition, and adoption playbooks; expanded installation and usage instructions; improved compatibility, pipeline, and integration guidance for caching and multitenancy (core, ASP.NET Core, EF Core); added new reference pages and troubleshooting links; updated contributing guidelines and roadmap references.
1 parent c9e74a4 commit bf5b082

31 files changed

+1053
-526
lines changed

docs/concepts/architecture-fit.md

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
1-
# Architecture Fit
1+
# Architecture fit
22

3-
How extensions align with Jason Taylor's Clean Architecture template.
3+
CleanArchitecture.Extensions packages are designed to fit the Jason Taylor Clean Architecture template without changing its structure. The goal is to keep boundaries intact while adding cross-cutting behavior through composition.
44

5-
- Keep the template untouched: extensions plug in via packages, configuration, middleware/behaviors—not by forking or editing the upstream template.
6-
- Preserve boundaries: respect domain/application/infrastructure/UI separation and dependency direction.
7-
- Prefer composition: use pipeline behaviors, decorators, filters, and adapters instead of modifying core layers.
8-
- Match conventions: mirror naming, folder structure, and coding style from the reference `JasonTaylorCleanArchitecture` copy.
9-
- Stay optional: every extension should be opt-in with clear defaults and minimal required configuration.
5+
## Design principles
6+
7+
- **No template fork**: packages integrate through DI, middleware, and MediatR behaviors.
8+
- **Layered boundaries**: Application depends on abstractions, Infrastructure provides adapters, and the host wires everything up.
9+
- **Opt-in by default**: each extension is optional and can be removed without structural changes.
10+
- **Convention-aligned**: naming, registration patterns, and pipeline order match the template.
11+
12+
## Placement guidance
13+
14+
| Layer | Typical responsibilities | Extension examples |
15+
| --- | --- | --- |
16+
| Application | Behaviors and abstractions | `QueryCachingBehavior`, `TenantEnforcementBehavior`, `ICurrentTenant` |
17+
| Infrastructure | Adapters and enforcement | `MemoryCacheAdapter`, `TenantSaveChangesInterceptor` |
18+
| Host (Web/API) | Middleware and endpoint filters | `TenantResolutionMiddleware`, `TenantEnforcementEndpointFilter` |
19+
20+
## Template integration points
21+
22+
- `src/Application/DependencyInjection.cs`: register MediatR behaviors.
23+
- `src/Infrastructure/DependencyInjection.cs`: register adapter services (caching, EF Core, etc.).
24+
- `src/Web/Program.cs`: add middleware and endpoint filters.
25+
26+
## Removal strategy
27+
28+
- Remove the NuGet package reference.
29+
- Remove `AddCleanArchitecture...` registrations and pipeline behaviors.
30+
- Delete any configuration sections and validation stores that were introduced.

docs/concepts/composition.md

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,45 @@
1-
# Composition & Invariants
1+
# Composition and invariants
22

3-
Principles for combining extensions safely.
3+
Use the extensions together without losing clarity or breaking the template's ordering.
44

5-
- Isolation: each extension should have clear dependencies and avoid implicit cross-talk.
6-
- Pipelines first: prefer mediatr behaviors, filters, and decorators to hook in cross-cutting concerns.
7-
- Config clarity: document required settings, defaults, and compat matrices; fail fast on invalid configs.
8-
- Observability: emit structured logs/events for extension lifecycle (init, errors, important decisions).
9-
- Compatibility: declare supported .NET versions and CleanArchitecture template versions per extension page.
10-
- Exit strategy: provide guidance for disabling/removing an extension cleanly.
5+
## Invariants
6+
7+
- **Explicit wiring**: every behavior or middleware is registered intentionally.
8+
- **No hidden dependencies**: cross-extension integration uses explicit opt-in hooks.
9+
- **Fail fast**: invalid configuration should be detected early (startup or first request).
10+
- **Keep handlers clean**: cross-cutting concerns live in pipeline behaviors, filters, or adapters.
11+
12+
## Recommended composition order
13+
14+
### MediatR pipeline (Application)
15+
16+
A common order that aligns with the template is:
17+
18+
1. Logging pre-processors
19+
2. Exception handling
20+
3. Authorization
21+
4. Validation
22+
5. Multitenancy validation/enforcement
23+
6. Caching behavior
24+
7. Performance logging
25+
26+
Adjust for your template, but keep validation/enforcement before caching to avoid caching invalid requests.
27+
28+
### HTTP middleware (Web/API)
29+
30+
- Correlation and localization (if used)
31+
- Tenant resolution
32+
- Authentication/authorization
33+
- MVC/minimal API endpoints
34+
35+
If you rely on claim-based tenant resolution, place authentication before the tenant resolution middleware.
36+
37+
## Cross-extension integration
38+
39+
- **Multitenancy + caching**: call `AddCleanArchitectureMultitenancyCaching` to include tenant IDs in cache keys.
40+
- **Multitenancy + EF Core**: ensure `TenantSaveChangesInterceptor` is registered and your DbContext uses tenant-aware model customization.
41+
42+
## Compatibility notes
43+
44+
- Keep `MultitenancyOptions` and EF Core options aligned (tenant ID property name, global entities, isolation mode).
45+
- Use consistent tenant identifiers across resolution sources (header, route, host) to avoid ambiguity.

docs/contributing/index.md

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,39 @@
11
# Contributing
22

3-
We keep the upstream Jason Taylor template pristine and ship everything as opt-in packages. Contributions must follow that contract and stay in sync with the docs and samples.
3+
We keep the upstream template pristine and ship everything as opt-in packages. Contributions must follow that contract and stay in sync with docs and tests.
44

55
## Principles
6-
- Template-first: mirror Jason Taylor’s conventions (folder layout, naming, MediatR pipeline ordering). Do **not** modify `JasonTaylorCleanArchitecture/`.
7-
- Extension = opt-in: minimal required config, no surprises; keep dependencies light.
8-
- Docs/samples-first: every behavior change lands with updated docs under `docs/` and, where applicable, a sample under `samples/`.
9-
- Tests-close-to-code: add/adjust tests in `tests/` for the package you touch.
6+
7+
- **Template-first**: mirror Jason Taylor's conventions. Do not modify the upstream template.
8+
- **Opt-in by default**: minimal required configuration, clear defaults, easy removal.
9+
- **Docs and tests**: every behavior change updates docs under `docs/` and tests under `tests/`.
10+
- **Separation of concerns**: Application stays clean; Infrastructure and host adapt via composition.
1011

1112
## Workflow
12-
1. Pick the design doc: read the matching `HighLevelDocs/Domain*/CleanArchitecture.Extensions.*.md` before coding.
13-
2. Branch in this repo; keep changes inside `CleanArchitecture.Extensions/` solution (src/tests/samples/docs/build).
14-
3. Implement + test: add or update unit/integration tests for your changes.
15-
4. Update docs: extension page, recipes, reference, and roadmap if scope changes. Keep nav links valid.
16-
5. Preview docs locally (optional):
17-
```powershell
18-
python -m venv .venv
19-
. .venv/Scripts/Activate.ps1
20-
pip install -r docs/requirements.txt
21-
mkdocs serve
22-
```
23-
6. Run relevant samples/tests where applicable and note any manual steps in your PR description.
24-
25-
## Style (docs)
26-
- Follow the documentation strategy templates (overview → when to use → compat → install → usage → troubleshooting → samples/tests).
27-
- Use fenced code blocks with language tags (`bash`, `powershell`, `csharp`, `json`).
28-
- Prefer snippets from source to avoid drift; keep examples short and runnable.
29-
- Keep compatibility info current (template version, target frameworks, dependencies).
30-
- Format external links as Markdown links, for example `- [Roadmap](https://cleanarchitecture-extensions.github.io/CleanArchitecture.Extensions/roadmap/)` (avoid `Label: URL`).
13+
14+
1) Read the relevant design doc in `HighLevelDocs/Domain*/CleanArchitecture.Extensions.*.md`.
15+
2) Make changes inside `CleanArchitecture.Extensions/` (src/tests/samples/docs/build).
16+
3) Update docs and ensure navigation links remain valid.
17+
4) Add or update tests for the package you touched.
18+
19+
## Documentation style
20+
21+
- Follow the extension template: overview, when to use, compat, install, usage, troubleshooting.
22+
- Use fenced code blocks with language tags (`csharp`, `powershell`, `json`).
23+
- Prefer short, runnable snippets and keep them under ~30 lines.
24+
- Use Markdown links for external URLs (e.g., `[Quickstart](https://...)`).
25+
26+
## Testing expectations
27+
28+
- Add unit tests for behaviors and configuration logic.
29+
- Add integration tests for EF Core and ASP.NET Core adapters when behavior changes.
30+
- Keep test names aligned with existing naming conventions.
31+
32+
## Local docs preview
33+
34+
```powershell
35+
python -m venv .venv
36+
. .venv/Scripts/Activate.ps1
37+
pip install -r docs/requirements.txt
38+
mkdocs serve
39+
```

docs/extensions/caching.md

Lines changed: 117 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,166 @@
11
# Extension: Caching
22

33
## Overview
4-
Cache abstractions, key conventions, and a MediatR query caching behavior for Clean Architecture applications. Ships memory and distributed adapters, deterministic key generation, and options for stampede protection and TTL tuning without leaking infrastructure into handlers.
4+
5+
CleanArchitecture.Extensions.Caching provides cache abstractions, deterministic key generation, and a MediatR query caching behavior. It is provider-agnostic and can target in-memory or distributed caches without leaking infrastructure concerns into handlers.
56

67
## When to use
78

8-
- You want query read-through caching without embedding cache calls in handlers.
9-
- You need deterministic, namespace/tenant-aware cache keys and provider-agnostic entry options.
10-
- You plan to start with in-memory caching for dev/test and swap to distributed stores (Redis via `IDistributedCache`) later.
9+
- You want transparent query caching without embedding cache calls in handlers.
10+
- You need deterministic, namespace-aware cache keys.
11+
- You want to start with memory caching in development and switch to distributed cache in production.
1112

12-
## Prereqs & Compatibility
13+
## Prereqs and compatibility
1314

14-
- Target frameworks: `net10.0`.
15-
- Dependencies: MediatR `13.1.0`, `Microsoft.Extensions.Caching.Abstractions`, `Microsoft.Extensions.Caching.Memory` (defaults); distributed adapter uses `IDistributedCache` (MemoryDistributedCache by default).
16-
- Pipeline fit: register `QueryCachingBehavior<,>` after authorization and request checks, and before performance logging to avoid skewing timing warnings.
15+
- Target framework: `net10.0`.
16+
- Dependencies: MediatR `13.1.0`, `Microsoft.Extensions.Caching.*` `10.0.0`.
1717

1818
## Install
1919

20-
```bash
21-
dotnet add src/YourProject/YourProject.csproj package CleanArchitecture.Extensions.Caching
20+
```powershell
21+
dotnet add src/Application/Application.csproj package CleanArchitecture.Extensions.Caching
22+
dotnet add src/Infrastructure/Infrastructure.csproj package CleanArchitecture.Extensions.Caching
2223
```
2324

24-
## Usage
25-
26-
### Register caching and pipeline behavior
25+
## Register services
2726

2827
```csharp
2928
using CleanArchitecture.Extensions.Caching;
3029
using CleanArchitecture.Extensions.Caching.Options;
31-
using MediatR;
3230

33-
services.AddCleanArchitectureCaching(options =>
31+
builder.Services.AddCleanArchitectureCaching(options =>
3432
{
3533
options.DefaultNamespace = "MyApp";
36-
options.MaxEntrySizeBytes = 256 * 1024; // optional
37-
}, queryOptions =>
34+
options.MaxEntrySizeBytes = 256 * 1024;
35+
}, behaviorOptions =>
3836
{
39-
queryOptions.DefaultTtl = TimeSpan.FromMinutes(5);
40-
// Default predicate caches types whose names end with "Query"; override to use a marker instead:
41-
// queryOptions.CachePredicate = req => req is IQueryMarker;
37+
behaviorOptions.DefaultTtl = TimeSpan.FromMinutes(5);
4238
});
39+
```
4340

44-
services.AddMediatR(cfg =>
41+
## Add the MediatR behavior
42+
43+
```csharp
44+
builder.Services.AddMediatR(cfg =>
4545
{
4646
cfg.RegisterServicesFromAssemblyContaining<Program>();
47-
cfg.AddCleanArchitectureCachingPipeline(); // place after request checks
47+
cfg.AddCleanArchitectureCachingPipeline();
4848
});
4949
```
5050

51-
### Configure cache keys and TTLs
51+
## How query caching works
5252

53-
- Keys follow `{namespace}:{tenant?}:{resource}:{hash}` via `ICacheKeyFactory` and `ICacheScope`. Override `ResourceNameSelector`/`HashFactory` in `QueryCachingBehaviorOptions` for custom resource naming or hashing (e.g., when parameters should be normalized).
54-
- Default TTL comes from `QueryCachingBehaviorOptions.DefaultTtl`; override per request type with `TtlByRequestType[typeof(MyQuery)] = TimeSpan.FromSeconds(30);`.
55-
- `CachePredicate` controls which requests are cacheable. By default it caches request types whose names end with "Query"; override to use markers or explicit type checks. `ResponseCachePredicate` can skip caching for responses you do not want stored.
53+
`QueryCachingBehavior<TRequest, TResponse>` applies cache-aside semantics:
5654

57-
### Choose an adapter
55+
- The default predicate caches request types whose names end with `Query` (case-insensitive).
56+
- The cache key uses the request type name as the resource and a SHA256 hash of the request payload.
57+
- Cache hits short-circuit the handler; cache misses store the handler result.
5858

59-
- Memory (default): registered as `ICache` by `AddCleanArchitectureCaching`, uses `MemoryCacheAdapter` with stampede locking and jitter.
60-
- Distributed: resolve `DistributedCacheAdapter` or replace `ICache` registration:
59+
Configure request selection and TTLs via `QueryCachingBehaviorOptions`:
6160

6261
```csharp
63-
services.AddCleanArchitectureCaching();
64-
services.AddStackExchangeRedisCache(opts => opts.Configuration = "..."); // or other IDistributedCache
65-
services.AddSingleton<ICache, DistributedCacheAdapter>(); // override default
62+
builder.Services.AddCleanArchitectureCaching(
63+
configureQueryCaching: options =>
64+
{
65+
options.CachePredicate = request => request is ICacheableQuery; // your own marker interface
66+
options.DefaultTtl = TimeSpan.FromMinutes(2);
67+
options.TtlByRequestType[typeof(GetUserQuery)] = TimeSpan.FromSeconds(30);
68+
options.CacheNullValues = false;
69+
});
6670
```
6771

68-
### Entry options and stampede settings
72+
## Cache keys and scopes
6973

70-
- `CachingOptions.DefaultEntryOptions` sets absolute/sliding expiration, priority, and size hints.
71-
- `CachingOptions.StampedePolicy` controls locking timeout and jitter for both adapters.
72-
- `CacheEntryOptions` can be passed per call or mapped by request type inside the behavior.
74+
- Key format: `{namespace}:{tenant?}:{resource}:{hash}`.
75+
- `DefaultCacheKeyFactory` hashes the request payload as JSON (deterministic SHA256).
76+
- `ICacheScope` supplies the namespace and optional tenant segment.
7377

74-
### Response-aware caching
78+
If you customize keys, keep them deterministic and stable across versions.
7579

76-
Use `QueryCachingBehaviorOptions.ResponseCachePredicate` to skip caching responses you want to exclude (for example, error payloads or partial results).
80+
## Choose a cache adapter
7781

78-
## Key components
82+
The default `ICache` implementation is `MemoryCacheAdapter`.
7983

80-
- `ICache`, `CacheEntryOptions`, `CacheStampedePolicy`, `CacheKey`, `ICacheKeyFactory`, `ICacheScope`, `ICacheSerializer`.
81-
- `MemoryCacheAdapter`, `DistributedCacheAdapter` (for `IDistributedCache`).
82-
- `QueryCachingBehavior<TRequest,TResponse>` with configurable TTLs, hash selection, predicate, and response filtering.
84+
!!! note
85+
The memory adapter is process-local. In a multi-instance deployment, use a distributed cache.
86+
87+
To use a distributed cache, register `IDistributedCache` and swap the adapter:
88+
89+
```csharp
90+
using CleanArchitecture.Extensions.Caching.Adapters;
91+
using Microsoft.Extensions.Caching.StackExchangeRedis;
92+
93+
builder.Services.AddCleanArchitectureCaching();
94+
builder.Services.AddStackExchangeRedisCache(options =>
95+
{
96+
options.Configuration = "<redis-connection-string>";
97+
});
98+
99+
builder.Services.AddSingleton<ICache, DistributedCacheAdapter>();
100+
```
101+
102+
## Serialization
103+
104+
The default serializer is `SystemTextJsonCacheSerializer`. Replace it when needed:
105+
106+
```csharp
107+
using CleanArchitecture.Extensions.Caching.Serialization;
108+
109+
builder.Services.AddSingleton<ICacheSerializer>(sp =>
110+
new SystemTextJsonCacheSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)));
111+
```
83112

84-
## Pipeline ordering
113+
## Stampede protection and entry options
85114

86-
- Recommended: Authorization → Request checks → **QueryCachingBehavior** → Performance/Logging → Handlers (align with the template order you already use).
87-
- Place caching after request checks to avoid caching invalid requests and before performance logging to exclude cache hits from handler timing warnings.
115+
- `CachingOptions.StampedePolicy` controls locking, timeouts, and jitter.
116+
- `CachingOptions.DefaultEntryOptions` defines expiration, priority, and size hints.
117+
118+
```csharp
119+
builder.Services.AddCleanArchitectureCaching(options =>
120+
{
121+
options.StampedePolicy = new CacheStampedePolicy
122+
{
123+
EnableLocking = true,
124+
LockTimeout = TimeSpan.FromSeconds(3),
125+
Jitter = TimeSpan.FromMilliseconds(50)
126+
};
127+
});
128+
```
88129

89130
## Invalidation guidance
90131

91-
- Cache-aside pattern: explicit `ICache.Remove` or `ICache.RemoveAsync` on command success or domain event handlers.
92-
- Include versioning and tenant segments in keys to avoid collisions; adjust namespace when making breaking DTO changes.
132+
Caching is read-through; invalidation is explicit. On command success or domain events, remove keys:
133+
134+
```csharp
135+
await cache.RemoveAsync(cacheScope.Create("GetUserQuery", hash));
136+
```
137+
138+
Keep key conventions stable and consider bumping the namespace for breaking DTO changes.
139+
140+
## Multitenancy integration
141+
142+
If you use multitenancy, call `AddCleanArchitectureMultitenancyCaching` to include tenant IDs in cache keys:
143+
144+
```csharp
145+
builder.Services.AddCleanArchitectureCaching();
146+
builder.Services.AddCleanArchitectureMultitenancyCaching();
147+
```
148+
149+
## Observability
150+
151+
- `QueryCachingBehavior` logs cache hits and misses at `Debug` level.
152+
- Adapters log warnings on oversized payloads or deserialization failures.
153+
154+
## Troubleshooting
155+
156+
- Cache is never hit: ensure the request type matches the cache predicate and the behavior is registered.
157+
- Missing tenant in keys: call `AddCleanArchitectureMultitenancyCaching` after caching registration.
158+
- Large payloads: raise `MaxEntrySizeBytes` or skip caching via `ResponseCachePredicate`.
93159

94-
## Backlog / Next Iteration
160+
## Samples and tests
95161

96-
- Add PII/classification guardrails so sensitive payloads can be blocked or redirected to encrypted storage.
97-
- Provide an optional encrypting serializer wrapper for distributed caches with guidance for key management.
98-
- Expose instrumentation hooks (hits, misses, latency) without forcing a specific metrics provider.
99-
- Document and/or implement schema-versioned key strategies to support DTO shape changes safely.
162+
See the caching tests under `tests/` for behavior coverage and usage patterns.
100163

101-
## Testing
164+
## Reference
102165

103-
- Use the default memory adapter for Application tests; distributed adapter can use `MemoryDistributedCache` for deterministic runs.
166+
- [Caching options](../reference/caching-options.md)

0 commit comments

Comments
 (0)