Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ Pre-1.0: the public API can still change between previews.

Releases are cut from this file. The `release` workflow reads the section matching the pushed tag and uses it as the GitHub Release body, so a tag with no matching section fails the build before anything reaches nuget.org. Before tagging, rename `[Unreleased]` to the version you are shipping and give it a date.

## [Unreleased]

### Added

- `IRequestFlowValidationRule`: any package or application can add its own checks to the startup validation pass with `AddValidationRule<T>()` and report into the same exception as the built-in checks. A rule reads the whole registration picture, including the `AllowUnhandledRequests` and `DisallowUnusedStages` opt-ins, off `RequestFlowValidationContext`. A rule that throws is reported as `RF0107` and the rules after it still run. [validation-rules.md](docs/validation-rules.md) covers writing, registering, and testing one.
- `RequestFlowModelBuilder` builds a `RequestFlowModel` by hand and `BuildContext` wraps one in the context a rule receives, which is how a rule is unit tested without a container.
- Startup validation rejects a request type that implements more than one `IRequest<TResponse>` contract (`RF0106`). The extra contract used to pass the freeze and fail every dispatch under it with `ResponseTypeMismatchException`.
- `AddCqrs` rejects a request classified as both a command and a query (`CQRS0001`).
- `ProblemCodes` and `CqrsProblemCodes` are public and ship in the abstractions packages, so an assembly referencing only those can match `ProblemCodes.UnhandledRequest` instead of a literal string.
- The model reports lifetimes as `RequestFlowLifetime` rather than the container's `ServiceLifetime`, since `RequestFlow.Abstractions` takes no dependency on the DI package. Handlers and stages also report the contract they implement as `ContractType`, so a kind contributed on top of a core contract, such as `ICommandHandler`, comes through under its own. Every list on the model is read-only.

### Changed

- `RequestFlowValidationException.Problems` holds `RequestFlowValidationProblem` values (stable code, message, offending type) instead of strings, and the public constructor takes the same list, so a call site passing strings no longer compiles. Message lines now read `RF0101: ...`, so anything matching on the old text breaks. Repeated registrations collapse into one problem each, where a request with three handlers used to report two identical lines.
- Stage declarations that alias one stage class report a single `RF0104` naming every declaration in the collision, instead of a line per colliding pair. `RF0104` also no longer fires for two closings of one stage class on a request with more than one handler: that request already fails on `RF0101`, and the collision surfaces once the duplicate handler is gone.

## [1.0.0-preview.5] - 2026-08-07

### Added
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ Contracts live in their own packages so your domain layer, and any future add-on
- [Stages](https://github.com/illia1f/RequestFlow/blob/main/docs/stages.md): wrapping handlers, execution order, which requests a stage reaches, filters
- [Service lifetimes](https://github.com/illia1f/RequestFlow/blob/main/docs/lifetimes.md): what RequestFlow registers, with which lifetime, and what you can change
- [Exceptions](https://github.com/illia1f/RequestFlow/blob/main/docs/exceptions.md): every exception RequestFlow throws, when it surfaces, and how to fix it
- [Validation rules](https://github.com/illia1f/RequestFlow/blob/main/docs/validation-rules.md): contributing custom checks to startup validation, the model rules see, built-in problem codes
- [Sample](https://github.com/illia1f/RequestFlow/blob/main/samples/README.md): a minimal API using commands, queries, stages, and two validation rules of its own

## Contributing

Expand Down
5 changes: 4 additions & 1 deletion RequestFlow.slnx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<Solution>
<Folder Name="/samples/" />
<Folder Name="/samples/">
<Project Path="samples/Orders.Api/Orders.Api.csproj" />
<Project Path="samples/Orders.Api.Violations/Orders.Api.Violations.csproj" />
</Folder>
<Folder Name="/src/">
<Project Path="src/RequestFlow.Abstractions/RequestFlow.Abstractions.csproj" Id="27f5d4e3-f8fc-491c-b221-d870df727eae" />
<Project Path="src/RequestFlow.Cqrs.Abstractions/RequestFlow.Cqrs.Abstractions.csproj" Id="bc85fd84-7040-4b88-b157-78df2303eb95" />
Expand Down
65 changes: 40 additions & 25 deletions docs/exceptions.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions docs/lifetimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ What RequestFlow registers, with which lifetime, and what you can change.
| Handlers (`IRequestHandler<TRequest, TResponse>`, `IRequestHandler<TRequest>`) | Transient | Yes, `WithScopedHandlers`, per `AddRequestFlow` call |
| Stages (`IRequestStage<TRequest, TResponse>`, `IRequestStage<TRequest>`) | Transient | Yes, `AsSingleton` or `AsScoped`, per `AddStage` call |
| `IRequestDispatcher` | Scoped | Yes, `WithTransientDispatcher` |
| Validation rules (`IRequestFlowValidationRule`) | Singleton, added by `AddValidationRule` and by `AddCqrs` | Not through those calls; register your own descriptor for another lifetime |
| Dispatch map (internal handler lookup) | Singleton, built the first time the dispatcher is resolved | No |

The lifetimes in the first two rows are readable at the freeze. A validation rule of your own reads the lifetime of every handler and stage off its model, so a house rule such as "no singleton stages here" can fail the start instead of waiting for a code review ([validation-rules.md](validation-rules.md#the-model)).

A rule is resolved while the dispatch map is built, so it comes from the root provider, and a provider that validates scopes throws there. A scoped rule throws ``Cannot resolve scoped service 'System.Collections.Generic.IEnumerable`1[RequestFlow.IRequestFlowValidationRule]' from root provider``, which names the enumerable rather than the rule, so look for the descriptor you registered scoped. A rule whose constructor takes a scoped dependency throws `Cannot consume scoped service` instead, naming the dependency and `RequestFlow.IRequestFlowValidationRule`, and `ValidateOnBuild` reports that one at `BuildServiceProvider`. [validation-rules.md](validation-rules.md) covers writing and registering one.

## Configuring handler lifetime

Handlers are transient by default. Every call into the handler gets a fresh instance, so a handler can hold mutable state without leaking it into the next dispatch. The bottom of a stage chain resolves on each entry, so a retry stage that runs the chain twice reaches a second instance rather than the one that failed. Call `WithScopedHandlers` when handlers share per-request dependencies such as a `DbContext`. It chains with the registration methods:
Expand Down
11 changes: 11 additions & 0 deletions docs/registration.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,14 @@ IServiceProvider provider = services.BuildServiceProvider().ValidateRequestFlow(
```

All problems are reported in one `RequestFlowValidationException`, not one at a time (see [exceptions.md](exceptions.md)). The container's own `ValidateOnBuild` cannot catch these problems; [lifetimes.md](lifetimes.md) explains why and covers validation timing in detail.

## Checks of your own

`AddRequestFlow` returns a builder, and `AddValidationRule` puts a check of yours in the same startup pass:

```csharp
services.AddRequestFlow(o => o.RegisterHandlersFromAssemblyContaining<Program>())
.AddValidationRule<RequestNameRule>();
```

The rule sees every registered request, handler, and stage, and reports into the same exception as the built-in checks. [validation-rules.md](validation-rules.md) covers writing, registering, and testing one.
2 changes: 1 addition & 1 deletion docs/stages.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,5 +299,5 @@ Stage problems surface with every other registration problem, in the one `Reques
- The stage type implements `IRequestStage<TRequest, TResponse>` or `IRequestStage<TRequest>` and is a concrete class.
- An open generic stage uses its own type parameters as its contract's request, so it can close over the requests it dispatches with.
- A partially closed generic is rejected; register the open definition or a fully closed type.
- No stage type is registered twice, and no two declarations reach one request as the same stage class.
- No stage type is registered twice, and no two declarations reach one request as the same stage class. A request with more than one handler has one chain per handler, so only declarations resolving to one closed type count as a collision there.
- With `DisallowUnusedStages`, every stage reaches at least one request.
Loading