Skip to content

Commit bb8bc64

Browse files
authored
Merge pull request #1125 from eventflow/improve-docs-faq
Improve documentation for FAQs
2 parents c6b2e28 + 63cf5d6 commit bb8bc64

File tree

1 file changed

+62
-27
lines changed
  • Documentation/additional

1 file changed

+62
-27
lines changed

Documentation/additional/faq.md

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,78 @@ title: FAQ
66

77
## How can I ensure that only specific users can execute commands?
88

9-
You can either replace the implementation of `ICommandBus` with your own implementation, or add a decorator that adds the authentication logic.
9+
EventFlow deliberately keeps the command pipeline thin. The default `CommandBus.PublishAsync` resolves the single `ICommandHandler<,,,>` that matches a command and forwards the call to `IAggregateStore.UpdateAsync`. No authorization hooks are executed for you.
10+
11+
You therefore have to enforce authorization either close to the domain or by decorating the command bus:
12+
13+
- Inject any ambient context (for example an `ICurrentUser`) into your command handlers and return a failed `IExecutionResult` or throw a `DomainError` if the caller is not allowed to proceed.
14+
- Replace the `ICommandBus` registration with a decorator that checks permissions before delegating to the inner bus. EventFlow registers the bus with `TryAddTransient<ICommandBus, CommandBus>()`, so a subsequent `services.Replace(...)` takes over cleanly. One possible decorator looks like this:
15+
16+
```csharp
17+
public sealed class SecuredCommandBus : ICommandBus
18+
{
19+
private readonly ICommandBus _inner;
20+
private readonly ICommandAuthorizer _authorizer;
21+
private readonly ICurrentUser _user;
22+
23+
public SecuredCommandBus(ICommandBus inner, ICommandAuthorizer authorizer, ICurrentUser user)
24+
{
25+
_inner = inner;
26+
_authorizer = authorizer;
27+
_user = user;
28+
}
29+
30+
public Task<TExecutionResult> PublishAsync<TAggregate, TIdentity, TExecutionResult>(
31+
ICommand<TAggregate, TIdentity, TExecutionResult> command,
32+
CancellationToken cancellationToken)
33+
where TAggregate : IAggregateRoot<TIdentity>
34+
where TIdentity : IIdentity
35+
where TExecutionResult : IExecutionResult
36+
{
37+
if (!_authorizer.CanExecute(command, _user))
38+
{
39+
throw DomainError.With(
40+
"Command {0} is not allowed for {1}",
41+
command.GetType().Name,
42+
_user.Id);
43+
}
44+
45+
return _inner.PublishAsync(command, cancellationToken);
46+
}
47+
}
48+
```
49+
50+
```csharp
51+
services.AddEventFlow(options => { /* configure aggregates, commands, ... */ });
52+
53+
services.Replace(ServiceDescriptor.Transient<ICommandBus>(sp =>
54+
new SecuredCommandBus(
55+
ActivatorUtilities.CreateInstance<CommandBus>(sp),
56+
sp.GetRequiredService<ICommandAuthorizer>(),
57+
sp.GetRequiredService<ICurrentUser>())));
58+
```
59+
60+
This keeps sensitive logic centralized while still letting the built-in `CommandBus` discover handlers and persist aggregates.
1061

1162
## Why isn't there a "global sequence number" on domain events?
1263

13-
While this is easy to support in some event stores like MSSQL, it
14-
doesn't really make sense from a domain perspective. Greg Young also has
15-
this to say on the subject:
64+
Every `IDomainEvent` emitted by an aggregate exposes the `AggregateSequenceNumber` from `DomainEvent<TAggregate, TIdentity, TAggregateEvent>` and repeats the value in metadata under `MetadataKeys.AggregateSequenceNumber`. EventFlow guarantees ordering inside a single aggregate root and stops there, because cross-aggregate ordering is a projection concern rather than a domain invariant.
1665

17-
!!! quote
18-
Order is only assured per a handler within an aggregate root
19-
boundary. There is no assurance of order between handlers or between
20-
aggregates. Trying to provide those things leads to the dark side. >
21-
22-
[Greg Young](https://groups.yahoo.com/neo/groups/domaindrivendesign/conversations/topics/18453)
66+
Most event store integrations (e.g., `MsSqlEventPersistence`, `PostgresSqlEventPersistence`, `SQLiteEventPersistence`) maintain their own `GlobalSequenceNumber` internally so they can serve `IEventStore.LoadAllEventsAsync(...)`. That API returns an `AllEventsPage` with a `GlobalPosition` token you can persist if you are building a log-reading process. Once the events are materialized into `IDomainEvent` instances and dispatched to handlers, the global number is intentionally not exposed—reactive code should not rely on cross-aggregate ordering promises.
2367

68+
If you need an application-level notion of global ordering, capture the `GlobalPosition` when you read from `LoadAllEventsAsync` or store it alongside your read model state.
2469

2570
## Why doesn't EventFlow have a unit of work concept?
2671

27-
Short answer, you shouldn't need it. But Mike has a way better answer:
28-
29-
!!! quote
30-
In the Domain, everything flows in one direction: forward. When
31-
something bad happens, a correction is applied. The Domain doesn't
32-
care about the database and UoW is very coupled to the db. In my
33-
opinion, it's a pattern which is usable only with data access
34-
objects, and in probably 99% of the cases you won't be needing it.
35-
As with the Singleton, there are better ways but everything depends
36-
on proper domain design. > `Mike
72+
The aggregate itself is the unit of consistency in EventFlow. A command published through the `CommandBus` flows into `IAggregateStore.UpdateAsync`, which (1) rehydrates the aggregate by replaying its event stream, (2) executes your domain logic delegate, (3) commits the newly emitted events in a single call to `IEventPersistence.CommitEventsAsync`, and (4) publishes the resulting domain events to read stores, subscribers, and sagas. Because aggregate state comes entirely from events, there is no ambient change tracker to flush and a classic unit-of-work abstraction would not add any extra safety.
3773

38-
[Mogosanu](http://blog.sapiensworks.com/post/2014/06/04/Unit-Of-Work-is-the-new-Singleton.aspx)
74+
When you really need to coordinate with another resource (e.g., enlist additional SQL statements), plug into `IAggregateStoreResilienceStrategy` or move the extra work into a subscriber that runs after the events have been durably written.
3975

40-
If your case falls within the 1% case, write a decorator for the
41-
`ICommandBus` that starts a transaction, use MSSQL as event store and
42-
make sure your read models are stored in MSSQL as well.
76+
## Why are subscribers receiving events out of order?
4377

78+
EventFlow publishes events in stages through `DomainEventPublisher`:
4479

45-
## Why are subscribers receiving events out of order?
80+
- Read stores and synchronous subscribers run one event at a time and in order. `DispatchToEventSubscribers.DispatchToSynchronousSubscribersAsync` awaits each handler before moving on.
81+
- Asynchronous subscribers are intentionally different. Each `IDomainEvent` is wrapped in a `DispatchToAsynchronousEventSubscribersJob` and scheduled via `IJobScheduler`. The default `InstantJobScheduler` executes the jobs immediately, but `DomainEventPublisher` starts them with `Task.WhenAll(...)`, so completion order is not guaranteed and alternative schedulers (such as Hangfire) may execute them on other workers entirely.
4682

47-
It might be that your aggregates are emitting multiple events. Read about
48-
[subscribers and out of order events](../basics/subscribers.md#out-of-order-events).
83+
If your projection relies on strict ordering, keep it synchronous or persist enough information—such as the `AggregateSequenceNumber`—to detect and discard late arrivals.

0 commit comments

Comments
 (0)