Skip to content

Commit 513d065

Browse files
author
vp
committed
docs: Command/Query docs changes
1 parent 634c989 commit 513d065

2 files changed

Lines changed: 109 additions & 78 deletions

File tree

docs/features-application-commands-queries.md

Lines changed: 98 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88

99
### Background
1010

11-
The [Command Query Separation](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation#:~:text=Command%2Dquery%20separation%20(CQS),the%20caller%2C%20but%20not%20both.) (CQS) principle, introduced by Bertrand Meyer, divides operations into commands, which modify system state and queries, which retrieve data without side effects. This separation enhances code clarity, predictability and maintainability by ensuring methods have distinct roles. By moving away from bloated application services that centralize all logic, commands and queries encapsulate specific business operations in smaller, focused units. This reduces the number of dependencies injected into each handler, improves testability by allowing isolated testing and promotes a cleaner architecture.
11+
The [Command Query Separation](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation#:~:text=Command%2Dquery%20separation%20(CQS),the%20caller%2C%20but%20not%20both.) (CQS) principle, introduced by Bertrand Meyer, divides operations into commands, which modify system state, and queries, which retrieve data without side effects. This separation enhances code clarity, predictability and maintainability by ensuring methods have distinct roles. By moving away from bloated application services that centralize all logic, commands and queries encapsulate specific business operations in smaller, focused units. This reduces the number of dependencies injected into each handler, improves testability by allowing isolated testing and promotes a cleaner architecture.
1212

13-
- **Commands**: Perform state-changing actions (e.g., creating a customer). They typically return `Result<Unit>` for actions with no meaningful return or `Result<T>` for minimal data like an ID.
14-
- **Queries**: Retrieve data without altering state (e.g., fetching a customer). They return `Result<T>` with the requested data and are idempotent.
13+
- **Commands**: Perform state-changing actions such as creating or updating data. They typically return `Result<Unit>` for actions with no meaningful return or `Result<T>` for minimal data such as an identifier or summary model.
14+
- **Queries**: Retrieve data without altering state. They return `Result<T>` with the requested data and are idempotent.
1515

16-
In Domain-Driven Design (DDD), commands and queries align with application services, encapsulating business logic and data access. The `Requester` feature in bITDevKit implements CQS using a mediator-like pattern, dispatching requests to handlers with type-safe `Result<T>` outcomes and extensible pipeline behaviors (e.g., validation, retries). This reduces coupling, as callers are unaware of handler implementations, minimizes dependency injection in handlers, and enables consistent handling of cross-cutting concerns, making the codebase more modular and testable.
16+
In Domain-Driven Design (DDD), commands and queries align with application services, encapsulating business logic and data access. The `Requester` feature in bITDevKit implements CQS using a mediator-like pattern, dispatching requests to handlers with type-safe `Result<T>` outcomes and extensible pipeline behaviors such as validation, retries, and timeouts. This reduces coupling, as callers are unaware of handler implementations, minimizes dependency injection in handlers, and enables consistent handling of cross-cutting concerns, making the codebase more modular and testable.
1717

1818
Many handlers also depend on the shared mapping abstraction to translate between request models, domain objects, and response DTOs; see [Common Mapping](./common-mapping.md).
1919

@@ -28,11 +28,11 @@ Many handlers also depend on the shared mapping abstraction to translate between
2828

2929
The `Requester` system provides:
3030

31-
- **Requests**: Inherit from `RequestBase<TResponse>`, defining inputs and outputs.
32-
- **Handlers**: Implement `RequestHandlerBase<TRequest, TResponse>`, returning `Result<TResponse>`.
31+
- **Requests**: Source-generated command and query types authored as `partial` classes with `[Command]` or `[Query]`.
32+
- **Handlers**: Business logic written inline with a single instance `[Handle]` method.
3333
- **Dispatching**: Via `IRequester.SendAsync()`, routing requests through a pipeline of behaviors.
3434

35-
Behaviors (e.g., `ValidationPipelineBehavior`, `RetryPipelineBehavior`) handle concerns without altering business logic.
35+
Behaviors such as `ValidationPipelineBehavior` and `RetryPipelineBehavior` handle concerns without altering business logic.
3636

3737
### Flow Diagram
3838

@@ -61,118 +61,140 @@ sequenceDiagram
6161

6262
## Setup
6363

64-
Register the `Requester` in the dependency injection (DI) container (e.g., in `CoreModule.cs`):
64+
Register the `Requester` in the dependency injection container:
6565

6666
```csharp
6767
services.AddRequester()
68-
.AddHandlers() // Scans for handlers
69-
.WithBehavior<ValidationPipelineBehavior<,>>() // Validates requests
70-
.WithBehavior<RetryPipelineBehavior<,>>(); // Adds retries
68+
.AddHandlers()
69+
.WithBehavior<ValidationPipelineBehavior<,>>()
70+
.WithBehavior<RetryPipelineBehavior<,>>();
71+
```
72+
73+
Add the code generation package to the project that contains the commands and queries:
74+
75+
```xml
76+
<PackageReference Include="BridgingIT.DevKit.Common.Utilities.CodeGen"
77+
Version="x.y.z"
78+
PrivateAssets="all" />
7179
```
7280

7381
## Basic Usage
7482

7583
### Defining a Command
7684

77-
Commands modify state and return `Result<T>` (e.g., a DTO or `Unit`).
85+
Commands modify state and return `Result<Unit>` or `Result<T>`.
7886

7987
```csharp
80-
public class CustomerCreateCommand(CustomerModel model) : RequestBase<CustomerModel>
88+
[Command]
89+
public partial class CustomerCreateCommand
8190
{
82-
public CustomerModel Model { get; set; } = model;
91+
public string FirstName { get; init; }
92+
93+
public string LastName { get; init; }
8394

84-
public class Validator : AbstractValidator<CustomerCreateCommand>
95+
public string Email { get; init; }
96+
97+
[Handle]
98+
private async Task<Result<CustomerModel>> HandleAsync(
99+
IMapper mapper,
100+
IGenericRepository<Customer> repository,
101+
CancellationToken cancellationToken)
85102
{
86-
public Validator()
87-
{
88-
this.RuleFor(c => c.Model).NotNull();
89-
this.RuleFor(c => c.Model.FirstName).NotNull().NotEmpty().WithMessage("Must not be empty.");
90-
this.RuleFor(c => c.Model.LastName).NotNull().NotEmpty().WithMessage("Must not be empty.");
91-
this.RuleFor(c => c.Model.Email).NotNull().NotEmpty().WithMessage("Must not be empty.");
92-
}
103+
var customer = mapper.Map<CustomerCreateCommand, Customer>(this);
104+
await repository.InsertAsync(customer, cancellationToken);
105+
106+
return Success(mapper.Map<Customer, CustomerModel>(customer));
93107
}
94108
}
95109
```
96110

97-
### Command Handler
111+
### Validating a Command
98112

99-
Handlers implement business logic, often using repositories.
113+
For simple cases, place validation directly on the properties:
100114

101115
```csharp
102-
[HandlerRetry(2, 100)] // Retry twice with 100ms delay
103-
[HandlerTimeout(500)] // Timeout after 500ms
104-
public class CustomerCreateCommandHandler(
105-
ILoggerFactory loggerFactory,
106-
IMapper mapper,
107-
IGenericRepository<Customer> repository)
108-
: RequestHandlerBase<CustomerCreateCommand, CustomerModel>(loggerFactory)
116+
[Command]
117+
public partial class CustomerRenameCommand
109118
{
110-
protected override async Task<Result<CustomerModel>> HandleAsync(
111-
CustomerCreateCommand request,
112-
SendOptions options,
113-
CancellationToken cancellationToken)
119+
[ValidateNotEmptyGuid("CustomerId is required.")]
120+
public string CustomerId { get; init; }
121+
122+
[ValidateNotEmpty("Display name is required.")]
123+
[ValidateLength(3, 100, "Display name must be between 3 and 100 characters.")]
124+
public string DisplayName { get; init; }
125+
126+
[Handle]
127+
private Result<Unit> Handle()
114128
{
115-
var customer = mapper.Map<CustomerModel, Customer>(request.Model);
116-
return await repository.InsertResultAsync(customer, cancellationToken: cancellationToken)
117-
.Tap(_ => Console.WriteLine("AUDIT"))
118-
.Map(mapper.Map<Customer, CustomerModel>);
129+
return Success();
119130
}
120131
}
121132
```
122133

123-
### Defining a Query
124-
125-
Queries retrieve data and return `Result<T>`.
134+
For more complex rules, the `[Validate]` marker can be used:
126135

127136
```csharp
128-
public class CustomerFindOneQuery(string customerId) : RequestBase<CustomerModel>
137+
[Command]
138+
public partial class CustomerImportCommand
129139
{
130-
public string CustomerId { get; } = customerId;
140+
[ValidateNotEmpty("At least one email address is required.")]
141+
[ValidateEachNotEmpty("Email entries cannot be empty.")]
142+
public List<string> Emails { get; init; }
131143

132-
public class Validator : AbstractValidator<CustomerFindOneQuery>
144+
[Validate]
145+
private static void Validate(InlineValidator<CustomerImportCommand> validator)
133146
{
134-
public Validator()
135-
{
136-
this.RuleFor(c => c.CustomerId).NotNull().NotEmpty().WithMessage("Must not be empty.");
137-
}
147+
validator.RuleFor(x => x.Emails) // regular fluent validation
148+
.Must(x => x.Count <= 100).WithMessage("A maximum of 100 email addresses is allowed.");
149+
}
150+
151+
[Handle]
152+
private Result<Unit> Handle()
153+
{
154+
return Success();
138155
}
139156
}
140157
```
141158

142-
### Query Handler
159+
### Defining a Query
160+
161+
Queries retrieve data and return `Result<T>`.
143162

144163
```csharp
145-
[HandlerRetry(2, 100)]
146-
[HandlerTimeout(500)]
147-
public class CustomerFindOneQueryHandler(
148-
IMapper mapper,
149-
IGenericRepository<Customer> repository)
150-
: RequestHandlerBase<CustomerFindOneQuery, CustomerModel>
164+
[Query]
165+
public partial class CustomerFindOneQuery
151166
{
152-
protected override async Task<Result<CustomerModel>> HandleAsync(
153-
CustomerFindOneQuery request,
154-
SendOptions options,
155-
CancellationToken cancellationToken) =>
156-
await repository.FindOneResultAsync(CustomerId.Create(request.CustomerId), cancellationToken: cancellationToken)
157-
.Tap(_ => Console.WriteLine("AUDIT"))
158-
.Map(mapper.Map<Customer, CustomerModel>);
167+
[ValidateNotEmptyGuid("CustomerId is required.")]
168+
public string CustomerId { get; }
169+
170+
[Handle]
171+
private async Task<Result<CustomerModel>> HandleAsync(
172+
IMapper mapper,
173+
IGenericRepository<Customer> repository,
174+
CancellationToken cancellationToken)
175+
{
176+
var customer = await repository.FindOneAsync(CustomerId, cancellationToken: cancellationToken);
177+
178+
return customer != null
179+
? Success(mapper.Map<Customer, CustomerModel>(customer))
180+
: Failure($"Customer with ID {CustomerId} was not found.");
181+
}
159182
}
160183
```
161184

162185
### Dispatching
163186

164-
Inject and use the `IRequester`:
187+
Inject and use `IRequester`:
165188

166189
```csharp
167190
var requester = serviceProvider.GetRequiredService<IRequester>();
168191

169-
// Command
170-
var command = new CustomerCreateCommand(new CustomerModel
192+
var command = new CustomerCreateCommand
171193
{
172194
FirstName = "John",
173195
LastName = "Doe",
174196
Email = "john.doe@example.com"
175-
});
197+
};
176198

177199
var commandResult = await requester.SendAsync(command);
178200
if (commandResult.IsSuccess)
@@ -184,7 +206,6 @@ else
184206
Console.WriteLine($"Errors: {string.Join(", ", commandResult.Errors.Select(e => e.Message))}");
185207
}
186208

187-
// Query
188209
var query = new CustomerFindOneQuery("some-guid");
189210
var queryResult = await requester.SendAsync(query);
190211
if (queryResult.IsSuccess)
@@ -193,4 +214,12 @@ if (queryResult.IsSuccess)
193214
}
194215
```
195216

196-
See [features-requester-notifier.md](./features-requester-notifier.md) for more details.
217+
### Notes
218+
219+
- The response type is inferred from the `Result<T>` returned by `[Handle]`.
220+
- `Success(...)` and `Failure(...)` can be used directly inside `[Handle]`.
221+
- DI services can be declared as parameters on `[Handle]` and are resolved automatically.
222+
- `CancellationToken` and `SendOptions` can also be declared as `[Handle]` parameters when needed.
223+
- Handler policy attributes such as retry, timeout, authorization, and transactions can be applied at the command or query definition.
224+
225+
See [features-requester-notifier.md](./features-requester-notifier.md) for more details (Appendix D: Source-Generated Commands and Queries).

docs/features-requester-notifier.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,20 +1494,20 @@ The `Requester` and `Notifier` systems share similarities with the popular **Med
14941494

14951495
---
14961496

1497-
# Appendix C: Generic Handlers in Requester and Notifier
1497+
## Appendix C: Generic Handlers in Requester and Notifier
14981498

1499-
## Overview
1499+
### Overview
15001500

15011501
Generic handlers in the `Requester` and `Notifier` systems enable handling of generic request/notification types (e.g., `GenericRequest<TData>`, `GenericNotification<TData>`) with a single handler, reducing code duplication. They are registered using `AddGenericHandlers`, which discovers open generic handlers, validates constraints, and registers closed handlers (e.g., `GenericDataProcessor<UserData>`).
15021502

1503-
## Key Features
1503+
### Key Features
15041504

15051505
- **Automatic Discovery**: `AddGenericHandlers` scans assemblies for open generic handlers and discovers type arguments based on constraints.
15061506
- **Constraint Validation**: Ensures type arguments meet constraints (e.g., `where TData : class, IDataItem`).
15071507

1508-
## Setup with `AddGenericHandlers`
1508+
### Setup with `AddGenericHandlers`
15091509

1510-
### Requester
1510+
#### Requester
15111511

15121512
```csharp
15131513
services.AddRequester()
@@ -1516,7 +1516,7 @@ services.AddRequester()
15161516
.WithBehavior<ValidationPipelineBehavior<,>>();
15171517
```
15181518

1519-
### Notifier
1519+
#### Notifier
15201520

15211521
```csharp
15221522
services.AddNotifier()
@@ -1527,9 +1527,9 @@ services.AddNotifier()
15271527

15281528
`AddGenericHandlers` discovers open generic handlers (e.g., `GenericDataProcessor<TData>`), finds type arguments (e.g., `UserData`, `OrderData`) that satisfy constraints, and registers closed handlers.
15291529

1530-
## Examples
1530+
### Examples
15311531

1532-
### Generic Request Handler (Requester)
1532+
#### Generic Request Handler (Requester)
15331533

15341534
```csharp
15351535
public class ProcessDataRequest<TData> : RequestBase<string>
@@ -1566,7 +1566,7 @@ var userRequest = new ProcessDataRequest<UserData> { Data = new UserData { Id =
15661566
var result = await requester.SendAsync(userRequest); // "Processed: user123"
15671567
```
15681568

1569-
### Generic Notification Handler (Notifier)
1569+
#### Generic Notification Handler (Notifier)
15701570

15711571
```csharp
15721572
public class GenericNotification<TData> : NotificationBase
@@ -1601,6 +1601,8 @@ var userNotification = new GenericNotification<UserData> { Data = new UserData {
16011601
var result = await notifier.PublishAsync(userNotification); // Logs: "Handled: user123"
16021602
```
16031603

1604+
---
1605+
16041606
## Appendix D: Source-Generated Commands and Queries
16051607

16061608
The source-generated authoring model lets you write a command or query as a single partial type. This is a convenient way to define simple requests without needing separate request and handler classes. The source generator creates the necessary boilerplate code, allowing you to focus on the business logic.

0 commit comments

Comments
 (0)