Skip to content

Commit

Permalink
Updating cronus documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
GavrailShodov committed Nov 7, 2024
1 parent 375b7aa commit d00aabd
Show file tree
Hide file tree
Showing 11 changed files with 499 additions and 196 deletions.
31 changes: 31 additions & 0 deletions docs/cronus-framework/concepts/es.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,34 @@ description: ES

# Event Sourcing

## What is Event Sourcing

Event Sourcing is a foundational concept in the Cronus framework, emphasizing the storage of state changes as a sequence of events. This approach ensures that every modification to an application's state is captured and stored, facilitating a comprehensive history of state transitions.

## Key Principles of Event Sourcing in Cronus:

Immutable Events: Each event represents a discrete change in the system and is immutable, ensuring a reliable audit trail.

Event Storage: Events are persistently stored, allowing the system to reconstruct the current state by replaying these events.

State Reconstruction: The current state of an entity is derived by sequentially applying all relevant events, ensuring consistency and traceability.

## Benefits of Using Event Sourcing with Cronus:

{% hint style="success" %}
* Auditability: Maintains a complete history of all changes, facilitating debugging and compliance.
* Scalability: Efficiently handles high-throughput systems by focusing on event storage and processing.
* Flexibility: Supports complex business logic and workflows by modeling state changes as events.
{% endhint %}

## Implementing Event Sourcing in Cronus:

Define Events: Create events that represent meaningful changes in the domain. In Cronus, events are immutable and should be named in the past tense to reflect actions that have occurred.
ELDERS CRONUS

Persist Events: Store events in the event store, which serves as the single source of truth for the system's state.
ELDERS OSS

Rehydrate State: Reconstruct the current state of aggregates by replaying the sequence of events associated with them.

By adhering to these principles, Cronus enables developers to build robust, event-driven systems that are both scalable and maintainable.
59 changes: 23 additions & 36 deletions docs/cronus-framework/domain-modeling/entity.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,24 @@ An entity is an object that has an identity and is mutable. Each entity is uniqu
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 class Wallet : Entity<UserAggregate, WalletState>
{
public ExampleEntity(Example root, IEntityId entityId, ExampleEntityTitle title)
: base(root, entityId)
public Wallet(UserAggregate root, WalletId entityId, string name, decimal amount) : base(root, entityId)
{
state.Title = title;
state.EntityId = entityId;
state.Name = name;
state.Amount = amount;
}

public void DoSomething()
{
Apply(new SomethingHappend(state.EntityId));
}
public void AddMoney(decimal value, UserId userId)
{

public void DoSomethingElse()
{
Apply(new SomethingElseHappend(state.EntityId));
}
if (value > 0)
{
IEvent @event = new AddMoney(state.EntityId, userId, value, DateTimeOffset.UtcNow);
Apply(@event);
}
}
}
```

Expand All @@ -40,23 +39,13 @@ Use the abstract helper class `EntityState<TEntityId>` to create an entity state
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 class WalletState : EntityState<WalletId>
{
public override ExampleEntityId EntityId { get; set; }
public override WalletId EntityId { get; set; }

public ExampleEntityTitle Title { get; set; }
public string Name { get; set; }

public void When(SomethingHappend e)
{

}

public void When(SomethingElseHappend e)
{

}
public decimal Amount { get; set; }
}
```

Expand All @@ -65,16 +54,14 @@ public class ExampleEntityState : EntityState<ExampleEntityId>
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>
[DataContract(Name = "1d23c591-219f-491e-bfb1-a775fe2751b6")]
public class WalletId : EntityId<UserId>
{
ExampleEntityId() { }
protected override ReadOnlySpan<char> EntityName => "wallet";

public ExampleEntityId(string idBase, ConcertId rootId) : base(idBase, rootId, "exampleentity")
{
}
WalletId() { }

public WalletId(string id, UserId idBase) : base(id.AsSpan(), idBase) { }
}
```

42 changes: 28 additions & 14 deletions docs/cronus-framework/domain-modeling/handlers/ports.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
# Ports
# Ports in the Cronus Framework

[https://github.com/Elders/Cronus/issues/258](https://github.com/Elders/Cronus/issues/258)
In the Cronus framework, **Ports** facilitate communication between aggregates, enabling one aggregate to react to events triggered by another. This design promotes a decoupled architecture, allowing aggregates to interact through well-defined events without direct dependencies.

Port is the mechanism to establish communication between aggregates. Usually, this involves one aggregate that triggered an event and one aggregate which needs to react.
## Key Characteristics of Ports

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 a persistent state.
- **Event-Driven Communication:** Ports listen for domain events—representing business changes that have already occurred—and dispatch corresponding commands to other aggregates that need to respond.

## Communication Guide Table

| Triggered by | Description |
| ------------ | ------------------------------------------------------------------- |
| Event | Domain events represent business changes that have already happened |
- **Statelessness:** Ports do not maintain any persistent state. Their sole responsibility is to handle the routing of events to appropriate command handlers.

## Best Practices
## When to Use Ports

{% hint style="success" %}
**You can/should/must...**
Ports are ideal for straightforward interactions where an event from one aggregate necessitates a direct response from another. However, for more complex workflows involving multiple steps or requiring state persistence, implementing a **Saga** is recommended. Sagas provide a transparent view of the business process and manage the state across various interactions, ensuring consistency and reliability.

* a port can send a command
{% endhint %}
## Communication Guide Table

| Triggered by | Description |
|--------------|-------------------------------------------------------|
| Event | Domain events represent business changes that have already happened. |

By utilizing Ports appropriately, developers can design systems that are both modular and maintainable, adhering to the principles of Domain-Driven Design and Event Sourcing.

**Port example**

```csharp
[DataContract(Name = "a44e9a38-ab13-4f86-844a-86fefa925b53")]
public class AlertPort : IPort,
IEventHandler<UserCreated>
{
public Task HandleAsync(UserCreated @event)
{
//Implement your custom logic here
return Task.CompletedTask;
}
}
```
69 changes: 50 additions & 19 deletions docs/cronus-framework/domain-modeling/handlers/projections.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,32 @@ To create a projection, create a class for it that inherits `ProjectionDefinitio
Use the `IEventHandler<TEvent>` interface to indicate that the projection can handle events of the specified event type. Implement this interface for each event type your projection needs to handle.

```csharp
// TODO: give a relevant example
[DataContract(Name = "bae8bd10-9903-4960-95c4-b4fa4688a860")]
public class ExampleByIdProjection : ProjectionDefinition<ExampleByIdProjectionState, ExampleId>,
IEventHandler<ExampleCreated>
[DataContract(Name = "c94513d1-e5ee-4aae-8c0f-6e85b63a4e03")]
public class TaskProjection : ProjectionDefinition<TaskProjectionData, TaskId>,
IEventHandler<TaskCreated>
{
public ExampleByIdProjection()
{
Subscribe<ExampleCreated>(x => x.Id);
}
public TaskProjection()
{
Subscribe<TaskCreated>(x => new TaskId(x.Id.NID));
}

public void Handle(ExampleCreated @event)
{
State.Id = @event.Id;
State.Name = @event.Name;
}
public Task HandleAsync(TaskCreated @event)
{
Data task = new Data();

task.Id = @event.Id;
task.UserId = @event.UserId;
task.Name = @event.Name;
task.Timestamp = @event.Timestamp;

State.Tasks.Add(task);

return Task.CompletedTask;
}
public IEnumerable<Data> GetTaskByName(string name)
{
return State.Tasks.Where(x => x.Name.Equals(name));
}
}
```

Expand All @@ -36,15 +47,35 @@ Create a class for the projection state. The state of the projection gets serial
{% endcontent-ref %}

```csharp
// TODO: give a relevant example
[DataContract(Name = "ed879ae7-e238-43eb-99f0-3a39c6c935e0")]
public class ExampleByIdProjectionState
[DataContract(Name = "c135893e-b9e3-453a-b0e0-53545094ec5d")]
public class TaskProjectionData
{
public TaskProjectionData()
{
Tasks = new List<Data>();
}

[DataMember(Order = 1)]
public ExampleId Id { get; set; }
public List<Data> Tasks { get; set; }

[DataMember(Order = 2)]
public ExampleName Name { get; set; }
[DataContract(Name = "317b3cbb-593a-4ffc-8284-d5f5c599d8ae")]
public class Data
{
[DataMember(Order = 1)]
public TaskId Id { get; set; }

[DataMember(Order = 2)]
public UserId UserId { get; set; }

[DataMember(Order = 3)]
public string Name { get; set; }

[DataMember(Order = 4)]
public DateTimeOffset CreatedAt { get; set; }

[DataMember(Order = 5)]
public DateTimeOffset Timestamp { get; set; }
}
}
```

Expand Down
80 changes: 70 additions & 10 deletions docs/cronus-framework/domain-modeling/handlers/sagas.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,83 @@
description: Sometimes called a Process Manager
---

# Sagas
# Sagas in the Cronus Framework

[https://github.com/Elders/Cronus/issues/259](https://github.com/Elders/Cronus/issues/259)
In the Cronus framework, **Sagas**—also known as **Process Managers**—are designed to handle complex workflows that span multiple aggregates. They provide a centralized mechanism to coordinate and manage long-running business processes, ensuring consistency and reliability across the system.

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.
## Key Characteristics of Sagas

- **Event-Driven Coordination:** Sagas listen for domain events, which represent business changes that have already occurred, and react accordingly to drive the process forward.

- **State Management:** Unlike simple event handlers, Sagas maintain state to track the progress of the workflow, enabling them to handle complex scenarios and ensure that all steps are completed successfully.

- **Command Dispatching:** Sagas can send new commands to aggregates or other components, orchestrating the necessary actions to achieve the desired business outcome.

## When to Use Sagas

Sagas are particularly useful when dealing with processes that:

- Involve multiple aggregates or bounded contexts.

- Require coordination of several steps or actions.

- Need to handle compensating actions in case of failures to maintain consistency.

By encapsulating the workflow logic within a Saga, developers can manage complex business processes more effectively, ensuring that all parts of the system work together harmoniously.

## Communication Guide Table

| Triggered by | Description |
| :--- | :--- |
| Event | Domain events represent business changes which have already happened |
| Triggered by | Description |
|--------------|-------------------------------------------------------|
| Event | Domain events represent business changes that have already happened. |

## Best Practices

{% hint style="success" %}
**You can/should/must...**
- A Saga can send new commands to drive the process forward.

- Ensure that Sagas are idempotent to handle potential duplicate events gracefully.

- Maintain clear boundaries for each Saga to prevent unintended side effects.

**Saga example**

```csharp
[DataContract(Name = "d4eb8803-2cc7-48dd-9ca1-4512b8d9b88f")]
public class TaskSaga : Saga,
IEventHandler<UserCreated>,
ISagaTimeoutHandler<Message>

{
public TaskSaga(IPublisher<ICommand> commandPublisher, IPublisher<IScheduledMessage> timeoutRequestPublisher) : base(commandPublisher, timeoutRequestPublisher)
{
}

public Task HandleAsync(UserCreated @event)
{
var message = new Message();
message.Info = @event.Name + "was created yesterday.";
message.PublishAt = DateTimeOffset.UtcNow.AddDays(1).DateTime;
message.Timestamp = DateTimeOffset.UtcNow;

RequestTimeout<Message>(message);

return Task.CompletedTask;
}
public Task HandleAsync(Message sagaTimeout)
{
Console.WriteLine(sagaTimeout.Info);

return Task.CompletedTask;
}

}

* a saga **can** send new commands
{% endhint %}
[DataContract(Name = "543e8e28-0dcb-4d41-98de-f701e403dbb2")]
public class Message : IScheduledMessage
{
public string Info { get; set; }
public DateTime PublishAt { get; set; }
public DateTimeOffset Timestamp { get; set; }
}
```

Loading

0 comments on commit d00aabd

Please sign in to comment.