-
-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0cbbe97
commit 93f3b33
Showing
17 changed files
with
383 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,80 @@ | ||
# Entity | ||
|
||
An entity is an object that has an identity and is mutable. Each entity is uniquely identified by an ID rather than by its properties; therefore, two entities can be considered equal if both of them have the same ID even though they have different properties. | ||
|
||
You can define an entity with Cronus using the `Entity<TAggregateRoot, TEntityState>` base class. To publish an event from an entity, use the `Apply(IEvent @event)` method provided by the base class. | ||
|
||
```csharp | ||
// TODO: give a relevant example | ||
public class ExampleEntity : Entity<Example, ExampleEntityState> | ||
{ | ||
public ExampleEntity(Example root, IEntityId entityId, ExampleEntityTitle title) | ||
: base(root, entityId) | ||
{ | ||
state.Title = title; | ||
} | ||
|
||
public void DoSomething() | ||
{ | ||
Apply(new SomethingHappend(state.EntityId)); | ||
} | ||
|
||
public void DoSomethingElse() | ||
{ | ||
Apply(new SomethingElseHappend(state.EntityId)); | ||
} | ||
} | ||
``` | ||
|
||
{% hint style="info" %} | ||
Set the initial state of the entity using the constructor. The event responsible for creating the entity is being published by the root/parent to modify its state. That means that you can not \(and should not\) subscribe to that event in the entity state using `When(Event e)`. | ||
{% endhint %} | ||
|
||
## Entity state | ||
|
||
The entity state keeps current data of the entity and is responsible for changing it based on events raised only by the same entity. | ||
|
||
Use the abstract helper class `EntityState<TEntityId>` to create an entity state. It can be accessed in the entity using the `state`field provided by the base class. Also, you can implement the `IEntityState` interface by yourself in case inheritance is not a viable option. | ||
|
||
To change the state of an entity, create event-handler methods for each event with a method signature `public void When(Event e) { ... }`. | ||
|
||
```csharp | ||
// TODO: give a relevant example | ||
public class ExampleEntityState : EntityState<ExampleEntityId> | ||
{ | ||
public override ExampleEntityId EntityId { get; set; } | ||
|
||
public ExampleEntityTitle Title { get; set; } | ||
|
||
public void When(SomethingHappend e) | ||
{ | ||
|
||
} | ||
|
||
public void When(SomethingElseHappend e) | ||
{ | ||
|
||
} | ||
} | ||
``` | ||
|
||
## Entity id | ||
|
||
All entity ids must implement the `IEntityId` interface. Since Cronus uses [URNs](https://en.wikipedia.org/wiki/Uniform_Resource_Name) for ids that will require implementing the [URN specification](https://tools.ietf.org/html/rfc8141) as well. If you don't want to do that, you can use the provided helper base class `EntityId<TAggregateRootId>`. | ||
|
||
```csharp | ||
// TODO: give a relevant example | ||
[DataContract(Name = "5154f78a-cd72-43f0-a445-a5d3fa44a461")] | ||
public class ExampleEntityId : EntityId<ConcertId> | ||
{ | ||
ExampleEntityId() { } | ||
|
||
public ExampleEntityId(string idBase, ConcertId rootId) : base(idBase, rootId, "exampleentity") | ||
{ | ||
} | ||
} | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Handlers | ||
|
58 changes: 58 additions & 0 deletions
58
docs/cronus-framework/domain-modeling/handlers/application-services.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# Application Services | ||
|
||
This is a handler where commands are received and delivered to the addressed Aggregate. We call these handlers _ApplicationService_. This is the _write side_ in CQRS. | ||
|
||
An application service is a command handler for a specific aggregate. One aggregate has one application service whose purpose is to handle commands and invoke the aggregate root's correct methods passing the command's payload. It mediates between Domain and infrastructure and it shields any domain model from the "outside". Only the Application Service interacts with the domain model. | ||
|
||
You can create an application service with Cronus by using the `AggregateRootApplicationService` base class. Specifying which commands the application service can handle is done using the `ICommandHandler<T>` interface. | ||
|
||
`AggregateRootApplicationService` provides a property of type `IAggregateRepository` that you can use to load and save the aggregate state. There is also a helper method `Update(IAggregateRootId id, Action update)` that loads and aggregate based on the provided id invokes the action and saves the new state if there are any changes. | ||
|
||
```csharp | ||
public class ConcertAppService : AggregateRootApplicationService<Concert>, | ||
ICommandHandler<AnnounceConcert>, | ||
ICommandHandler<RegisterPerformer> | ||
{ | ||
... | ||
|
||
public void Handle(AnnounceConcert command) | ||
{ | ||
if (Repository.TryLoad<Concert>(command.Id, out _)) | ||
return; | ||
|
||
var concert = new Concert(...); | ||
Repository.Save(concert); | ||
} | ||
|
||
public void Handle(RegisterPerformer command) | ||
{ | ||
Update(command.Id, x => x.RegisterPerformer(...)); | ||
} | ||
|
||
... | ||
} | ||
``` | ||
|
||
## Best Practices | ||
|
||
{% hint style="success" %} | ||
**You can/should/must...** | ||
|
||
* an application service **can** load an aggregate root from the event store | ||
* an application service **can** save new aggregate root events to the event store | ||
* an application service **can** establish calls to the read model \(not a common practice but sometimes needed\) | ||
* an application service **can** establish calls to external services | ||
* you **can** do dependency orchestration | ||
* an application service **must** be stateless | ||
* an application service **must** update only one aggregate root. Yes, you can create one aggregate and update another one but think twice before doing so. | ||
{% endhint %} | ||
|
||
{% hint style="warning" %} | ||
**You should not...** | ||
|
||
* an application service **should not** update more than one aggregate root in a single command/handler | ||
* you **should not** place domain logic inside an application service | ||
* you **should not** use an application service to send emails, push notifications etc. Use a port or a gateway instead | ||
* an application service **should not** update the read model | ||
{% endhint %} | ||
|
18 changes: 18 additions & 0 deletions
18
docs/cronus-framework/domain-modeling/handlers/gateways.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Gateways | ||
|
||
Compared to Port, which can dispatch a command, a Gateway can do the same but it also has a persistent state. A scenario could be sending commands to external BC, such as push notifications, emails etc. There is no need to event source this state and its perfectly fine if this state is wiped. Example: iOS push notifications badge. This state should be used only for infrastructure needs and never for business cases. Compared to Projection, which tracks events, projects their data, and are not allowed to send any commands at all, a Gateway can store and track metadata required by external systems. Furthermore, Gateways are restricted and not touched when events are replayed. | ||
|
||
## Communication Guide Table | ||
|
||
| Triggered by | Description | | ||
| :--- | :--- | | ||
| Event | Domain events represent business changes which have already happened | | ||
|
||
## Best Practices | ||
|
||
{% hint style="success" %} | ||
**You can/should/must...** | ||
|
||
* a gateway **can** send new commands | ||
{% endhint %} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Ports | ||
|
||
Port is the mechanism to establish communication between aggregates. Usually this involves one aggregate who triggered an event and one aggregate which needs to react. | ||
|
||
If you feel the need to do more complex interactions, it is advised to use Saga. The reason for this is that ports do not provide a transparent view of the business flow because they do not have persistent state. | ||
|
||
## Communication Guide Table | ||
|
||
| Triggered by | Description | | ||
| :--- | :--- | | ||
| Event | Domain events represent business changes which have already happened | | ||
|
||
## Best Practices | ||
|
||
{% hint style="success" %} | ||
**You can/should/must...** | ||
|
||
* a port can send a command | ||
{% endhint %} | ||
|
||
|
||
|
30 changes: 30 additions & 0 deletions
30
docs/cronus-framework/domain-modeling/handlers/projections.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# Projections | ||
|
||
Projection tracks events and project their data for specific purposes. | ||
|
||
## Communication Guide Table | ||
|
||
| Triggered by | Description | | ||
| :--- | :--- | | ||
| Event | Domain events represent business changes which have already happened | | ||
|
||
## Best Practices | ||
|
||
{% hint style="success" %} | ||
**You can/should/must...** | ||
|
||
* a projection **must** be idempotent | ||
* a projection **must not** issue new commands or events | ||
{% endhint %} | ||
|
||
{% hint style="warning" %} | ||
**You should not...** | ||
|
||
* a projection **should not** query other projections. All the data of a projection must be collected from the Events' data | ||
* a projection **should not** establish calls to external systems | ||
{% endhint %} | ||
|
||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
--- | ||
description: Sometimes called a Process Manager | ||
--- | ||
|
||
# Sagas | ||
|
||
When we have a workflow, which involves several aggregates it is recommended to have the whole process described in a single place such as а Saga/ProcessManager. | ||
|
||
## Communication Guide Table | ||
|
||
| Triggered by | Description | | ||
| :--- | :--- | | ||
| Event | Domain events represent business changes which have already happened | | ||
|
||
## Best Practices | ||
|
||
{% hint style="success" %} | ||
**You can/should/must...** | ||
|
||
* a saga **can** send new commands | ||
{% endhint %} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Triggers | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Messages | ||
|
55 changes: 55 additions & 0 deletions
55
docs/cronus-framework/domain-modeling/messages/commands.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# Commands | ||
|
||
A command is used to dispatch domain model changes. It can be accepted or rejected depending on the domain model invariants. | ||
|
||
## Communication Guide Table | ||
|
||
| Triggered by | Description | | ||
| :---: | :--- | | ||
| UI | It is NOT common practice to send commands directly from the UI. Usually the UI communicates with web APIs | | ||
| API | APIs sit in the middle between UI and Server translating web requests into commands | | ||
| External System | It is NOT common practice to send commands directly from the External System. Usually the External System communicates with web APIs. | | ||
| Port | Ports are a simple way for an aggregate to communicate with other aggregates. | | ||
| Saga | Sagas are a simple way for an aggregate to do complex communication with other aggregates. | | ||
| | | | ||
|
||
## Best Practices | ||
|
||
{% hint style="success" %} | ||
**You can/should/must...** | ||
|
||
* a command **must** be immutable | ||
* a command **must** clearly state a business intent with a name in imperative form | ||
* a command **can** be rejected due to domain validation, error or other reason | ||
* a command **must** update only one Aggregate | ||
{% endhint %} | ||
|
||
## Examples | ||
|
||
```csharp | ||
public class DeactivateAccount : ICommand | ||
{ | ||
DeactivateAccount() {} | ||
public DeactivateAccount(AccountId id, Reason reason) | ||
{ | ||
Id = id; | ||
Reason = reason; | ||
} | ||
|
||
public AccountId Id { get; private set; } | ||
public Reason ReasonToDeactivate { get; private set; } | ||
} | ||
|
||
[DataContract(Name = "24c59143-b95e-4fd6-8bbf-8d5efffe3185")] | ||
public class AccountId : StringTenantId | ||
{ | ||
protected AccountId() { } | ||
public AccountId(string id, string tenant) : base(id, "account", tenant) { } | ||
public AccountId(IUrn urn) : base(urn, "account") { } | ||
} | ||
|
||
public class Reason : ValueObject<Reason>{...} | ||
``` | ||
|
||
|
||
|
Oops, something went wrong.