arrow-left

All pages
gitbookPowered by GitBook
1 of 20

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Bounded Context

https://github.com/Elders/Cronus/issues/275arrow-up-right

Imaginary example:

Imagine that you have to build an online store. Until now, the business has been operating locally in a big city and the business has been very successful. The idea is to make it possible for other people outside of the big city to have the same experience which will allow the business to expand and reach a wider customer audience. There are a few questions you have to ask the business or discover somehow from the domain experts.

Q: What are the key advantages over the direct competition?

A: We offer unique loyalty programs which enable good discounts to customers. In addition, we have a rich network of suppliers that gives a wide variety of goods to choose from.

Q: How the online store is going to generate profit?

A: Unlocking the loyalty program requires a paid monthly subscription.

Multitenancy

The Cronus framework supports multitenancy, enabling a single application instance to serve multiple tenants while ensuring data isolation and security for each. This design allows for efficient resource utilization and simplified maintenance across diverse client bases.

hashtag
Key Characteristics of Multitenancy in Cronus

  • Tenant Isolation: Each tenant's data and configurations are isolated, preventing unauthorized access and ensuring privacy.

  • Dynamic Tenant Management: Cronus allows for the addition or removal of tenants at runtime, facilitating scalability and adaptability to changing business needs.

  • Shared Infrastructure: While tenants share the same application infrastructure, their data and processes remain segregated, optimizing resource usage without compromising security.

hashtag
Implementing Multitenancy in Cronus

  1. Tenant Identification: Assign a unique identifier to each tenant to distinguish their data and operations within the system.

  2. Data Segregation: Utilize strategies such as separate databases, schemas, or tables with tenant-specific identifiers to ensure data isolation.

  3. Configuration Management: Maintain tenant-specific configurations to cater to individual requirements and preferences.

hashtag
Best Practices

  • Consistent Tenant Context: Ensure that the tenant context is consistently applied throughout the application to maintain data integrity and security.

  • Scalability Planning: Design the system to handle varying numbers of tenants, considering factors like data volume, performance, and resource allocation.

  • Monitoring and Auditing: Implement monitoring and auditing tools to track tenant-specific activities, aiding in compliance and troubleshooting.

By adhering to these practices, developers can leverage Cronus's multitenancy capabilities to build scalable, secure, and efficient applications that serve multiple clients effectively.

Access Control: Implement robust authentication and authorization mechanisms to enforce tenant boundaries and prevent cross-tenant data access.

Value Object

Value objects represent immutable and atomic data. They are distinguishable only by the state of their properties and do not have an identity or any identity tracking mechanism. Two value objects with the exact same properties can be considered equal. You can read more about value objects in thisarrow-up-right article.

To define a value object with Cronus, create a class that inherits the base helper class ValueObject<T>. Keep all related to the value object business rules and data within the class.

[DataContract(Name = "1b6187f0-88c7-46d5-a22d-b39301765412")]
public class Performer: ValueObject<Performer>
{
    Performer() {}

    public Performer(string name, string coverImage)
    {
        // null check
        Name = name;
        CoverImage = coverImage;
    }

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

    [DataMember(Order = 2)]
    public string CoverImage { get; private set; }
}

The base class ValueObject<T> implements the IEqualityComparer<T> and IEquatable<T> interfaces. When comparing two value objects of the same type the properties from the first are being compared with the properties of the second using reflection. The base class also overrides the == and != operators.

circle-info

If a value object contains a collection of items, make sure that the items are also value objects and the collection supports item-by-item comparison. Otherwise, you will have to override the default comparison algorithm.

Keep a parameterless constructor and specify a data contract for serialization.

Serializationchevron-right

Published Language

https://github.com/Elders/Cronus/issues/203arrow-up-right

Domain Modeling

Commands

A command is a simple immutable object that is sent to the domain to trigger a state change. There should be a single command handler for each command. It is recommended to use imperative verbs when naming commands together with the name of the aggregate they operate on.

Application Serviceschevron-right

It is possible for a command to get rejected if the data it holds is incorrect or inconsistent with the current state of the aggregate.

circle-check

You can/should/must...

  • a command must be immutable

  • a command should clearly state a business intent with a name in the imperative form

  • a command can be rejected due to domain validation, error or other reason

  • a command must update only one aggregate

hashtag
Defining a command

You can define a command with Cronus using the ICommand markup interface. All commands get serialized and deserialized, that's why you need to keep the parameterless constructor and specify data contracts.

circle-info

Cronus uses the ToString() method for logging, so you can override it to generate user-readable logs. Otherwise, the name of the command class will be used for log messages.

hashtag
Publishing a command

To publish a command, inject an instance ofIPublisher<ICommand> into your code and invoke the Publish() method passing the command. This method will return true if the command has been published successfully through the configured transport. You can also use one of the overrides of the Publish() method to delay or schedule a command.

Aggregate

Aggregates represent the business models explicitly. They are designed to fully match any needed requirements. Any change done to an instance of an aggregate goes through the aggregate root.

hashtag
Aggregate root

Creating an aggregate root with Cronus is as simple as writing a class that inheritsAggregateRoot<TState> and a class for the state of the aggregate root. To publish an event from an aggregate root use the Apply(IEvent @event) method provided by the base class.

hashtag
Aggregate root state

The aggregate root state keeps the current data of the aggregate root and is responsible for changing it based on events raised only by the root.

Use the abstract helper class AggregateRootState<TAggregateRoot, TAggregateRootId> to create an aggregate root state. It can be accessed in the aggregate root using the state field provided by the base class. Also, you can implement the IAggregateRootState interface by yourself in case inheritance is not a viable option.

To change the state of an aggregate root, create event-handler methods for each event with a method signature public void When(Event e) { ... }.

circle-info

You could read more about the state pattern and .

hashtag
Aggregate root id

All aggregate root ids must implement the IAggregateRootId interface. Since Cronus uses for ids that will require implementing the as well. If you don't want to do that, you can use the provided helper base class AggregateRootId.

Another option is to use the AggregateRootId<T> class. This will give you more flexibility in constructing instances of the id. Also, parsing URNs will return the specified type T instead of AggregateUrn.

public class Concert : AggregateRoot<ConcertState>
{
    Concert() {} // keep the private parameterless constructor
    
    public Concert(string name, Venue venue, DateTimeOffset startTime, TimeSpan duration)
    {
        // business logic for creating a concert
        Apply(new ConcertAnnounced(...));
    }

    public void RegisterPerformer(Performer performer)
    {
        // business logic for registering a performer
        Apply(new PerformerRegistered(...));
    }
    
    // ...
}
Serializationchevron-right
herearrow-up-right
herearrow-up-right
URNsarrow-up-right
URN specificationarrow-up-right
[DataContract(Name = "857d960c-4b91-49cc-98fd-fa543906c52d")]
public class CreateTask : ICommand
{
    public CreateTask() { }

    public CreateTask(TaskId id, UserId userId, string name, DateTimeOffset timestamp)
    {
        if (id is null) throw new ArgumentNullException(nameof(id));
        if (userId is null) throw new ArgumentNullException(nameof(userId));
        if (name is null) throw new ArgumentNullException(nameof(name));
        if (timestamp == default) throw new ArgumentNullException(nameof(timestamp));

        Id = id;
        UserId = userId;
        Name = name;
        Timestamp = timestamp;
    }

    [DataMember(Order = 1)]
    public TaskId Id { get; private set; }

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

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

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

    public override string ToString()
    {
        return $"Create a task with id '{Id}' and name '{Name}' for user [{UserId}].";
    }
}
[ApiController]
[Route("[controller]/[action]")]
public class TaskController : ControllerBase
{
    private readonly IPublisher<ICommand> _publisher;

    public TaskController(IPublisher<ICommand> publisher)
    {
        _publisher = publisher;
    }

    [HttpPost]
    public IActionResult CreateTask(CreateTaskRequest request)
    {
        string id = Guid.NewGuid().ToString();
        string Userid = Guid.NewGuid().ToString();
        TaskId taskId = new TaskId(id);
        UserId userId = new UserId(Userid);
        var expireDate = DateTimeOffset.UtcNow;
        expireDate.AddDays(request.DaysActive);

        CreateTask command = new CreateTask(taskId, userId, request.Name, expireDate);

        if (_publisher.Publish(command) == false)
        {
            return Problem($"Unable to publish command. {command.Id}: {command.Name}");
        };
        return Ok(id);
    }
}
public class ConcertState : AggregateRootState<Concert, ConcertId>
{
    public ConcertState()
    {
        Performers = new List<Performer>();
    }

    public override ConcertId Id { get; set; }

    public string Name { get; private set; }

    public Venue Venue { get; private set; }

    public DateTimeOffset StartTime { get; private set; }

    public TimeSpan Duration { get; private set; }

    public List<Performer> Performers { get; private set; }
    
    public void When(ConcertAnnounced @event)
    {
        // change the state here ...
    }
    
    public void When(PerformerRegistered @event)
    {
        // change the state here ...
    }
}
[DataContract(Name = "e96d90d0-4943-43f4-8a84-cd90b1217d06")]
public class ConcertId : AggregateRootId
{
    const string RootName = "concert";

    public ConcertId(AggregateUrn urn) : base(RootName, urn) { }
    public ConcertId(string idBase, string tenant) : base(idBase, RootName, tenant) { }
    protected ConcertId() { }
}
[DataContract(Name = "e96d90d0-4943-43f4-8a84-cd90b1217d06")]
public class ConcertId : AggregateRootId<ConcertId>
{
    const string RootName = "concert";

    ConcertId() { }
    public ConcertId(string id, string tenant) : base(id, RootName, tenant) { }

    protected override ConcertId Construct(string id, string tenant)
    {
        return new ConcertId(id, tenant);
    }
}

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.

circle-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).

hashtag
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 statefield 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) { ... }.

hashtag
Entity id

All entity ids must implement the IEntityId interface. Since Cronus uses for ids that will require implementing the as well. If you don't want to do that, you can use the provided helper base class EntityId<TAggregateRootId>.

public class Wallet : Entity<UserAggregate, WalletState>
{
    public Wallet(UserAggregate root, WalletId entityId, string name, decimal amount) : base(root, entityId)
    {
        state.EntityId = entityId;
        state.Name = name;
        state.Amount = amount;
    }

    public void AddMoney(decimal value, UserId userId)
    {

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

Signals

https://github.com/Elders/Cronus/issues/262arrow-up-right

Messages

URNsarrow-up-right
URN specificationarrow-up-right
public class WalletState : EntityState<WalletId>
{
    public override WalletId EntityId { get; set; }

    public string Name { get; set; }

    public decimal Amount { get; set; }
}
[DataContract(Name = "1d23c591-219f-491e-bfb1-a775fe2751b6")]
public class WalletId : EntityId<UserId>
{
    protected override ReadOnlySpan<char> EntityName => "wallet";

    WalletId() { }

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

Events

An event is something significant that has happened in the domain. It encapsulates all relevant data of the action that happened.

circle-check

You can/should/must...

  • an event must be immutable

  • an event must represent a domain event that already happened with a name in the past tense

  • an event can be dispatched only by one aggregate

To create an event with Cronus, just use the IEvent markup interface.

circle-info

Cronus uses the ToString() method for logging, so you can override it to generate user-readable logs. Otherwise, the name of the event class will be used for log messages.

Sagas

Sometimes called a Process Manager

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.

hashtag
Key Characteristics of Sagas

IDs

TODO: describe all different types of ids Cronus provides, their purpose and hierarchy. Explain how and why to define custom ids (simple and composite) for aggregates, entities and projections. Explain URNs and the different parsing methods.

Projections

A projection is a representation of an object using a different perspective. In the context of CQRS, projections are queryable models on the "read" side that never manipulate the original data (events in event-sourced systems) in any way. Projections should be designed in a way that is useful and convenient for the reader (API, UI, etc.).

Cronus supports non-event-sourced and event-sourced projections with snapshots.

hashtag
Defining a projection

To create a projection, create a class for it that inherits

https://github.com/Elders/Cronus/issues/273arrow-up-right
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.

  • hashtag
    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.

    hashtag
    Communication Guide Table

    Triggered by
    Description

    Event

    Domain events represent business changes that have already happened.

    hashtag
    Best Practices

    • 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

    ProjectionDefinition<TState, TId>
    . The id can be any type that implements the
    IBlobId
    interface. All ids provided by Cronus implement this interface but it is common to create your own for specific business cases. The
    ProjectionDefinition<TState, TId>
    base class provides a
    Subscribe()
    the method that is used to create a projection id from an event. This will define an event-sourced projection with a state that will be used to persist snapshots.

    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.

    Create a class for the projection state. The state of the projection gets serialized and deserialized when persisting or restoring a snapshot. That's why it must have a parameterless constructor, a data contract and data members.

    circle-info

    There is no guarantee the events will be handled in the order of publishing nor that every event will be handled at most once. That's why you should design projections in a way that solves those problems. Always assign all possible properties from the handled event to the state and make sure the projection is idempotent.

    circle-info

    If the projection state contains a collection, make sure it doesn't get populated with duplicates. This can be achieved by using a HashSet<T> and ValueObject.

    You can define a non-event-sourced projection by decorating it with the IProjection interface. This is useful when you want to persist the state in an external system (e.g. ElasticSearch, relational database).

    By default, all projections' states are being persisted as snapshots. If you want to disable this feature for a specific projection, use the IAmNotSnapshotable interface.

    hashtag
    Querying a projection

    To query a projection, you need to inject an instance of IProjectionReader in your code and invoke the Get() or GetAsync() method. The returned object will be of type ReadResult or Task<ReadResult> containing the projection and a few properties indicating if the loading was successful.

    circle-info

    Use separate models for the API responses from the projection states to ensure you won't introduce breaking changes if the projection gets modified.

    hashtag
    Projection versioning

    TODO

    hashtag
    Best Practices

    circle-check

    You can/should/must...

    • a projection must be idempotent

    • a projection must not issue new commands or events

    circle-exclamation

    You should not...

    • a projection should not query other projections. All the data of a projection must be collected from the Events' data

    Serializationchevron-right
    [DataContract(Name = "728fc4e7-628b-4962-bd68-97c98aa05694")]
    public class TaskCreated : IEvent
    {
        TaskCreated() { }
    
        public TaskCreated(TaskId id, UserId userId, string name, DateTimeOffset timestamp)
        {
            Id = id;
            UserId = userId;
            Name = name;
            CreatedAt = DateTimeOffset.UtcNow;
            Timestamp = timestamp;
        }
    
        [DataMember(Order = 1)]
        public TaskId Id { get; private set; }
    
        [DataMember(Order = 2)]
        public UserId UserId { get; private set; }
    
        [DataMember(Order = 3)]
        public string Name { get; private set; }
    
        [DataMember(Order = 4)]
        public DateTimeOffset CreatedAt { get; private set; }
    
        [DataMember(Order = 5)]
        public DateTimeOffset Timestamp { get; private set; }
    
        public override string ToString()
        {
            return $"Task with id '{Id}' and name '{Name}' for user [{UserId}] at {CreatedAt} has been created.";
        }
    }
    [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;
        }
    
    }
    
    [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; }
    }
    [DataContract(Name = "c94513d1-e5ee-4aae-8c0f-6e85b63a4e03")]
    public class TaskProjection : ProjectionDefinition<TaskProjectionData, TaskId>,
        IEventHandler<TaskCreated>
    {
        public TaskProjection()
        {
            Subscribe<TaskCreated>(x => new TaskId(x.Id.NID));
        }
    
        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));
        }
    }
    [DataContract(Name = "c135893e-b9e3-453a-b0e0-53545094ec5d")]
    public class TaskProjectionData
    {
        public TaskProjectionData()
        {
            Tasks = new List<Data>();
        }
    
        [DataMember(Order = 1)]
        public List<Data> Tasks { 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; }
        }
    }
    // TODO: give a relevant example
    [DataContract(Name = "af157a4d-7608-4c9d-8e42-63bd483a8ad4")]
    public class ExampleEfProjection : IProjection,
            IEventHandler<ExampleCreated>
    {
    		public DbContext Context { get; set; }
    
    		public void Handle(ExampleCreated @event)
        {
    				var exampleDto = new ExampleDto(@event.Id, @event.Name);
            Context.Examples.Add(exampleDto);
            Context.SaveChanges();
        }
    }
    // TODO: give a relevant example
    [DataContract(Name = "bae8bd10-9903-4960-95c4-b4fa4688a860")]
    public class ExampleByIdProjection : ProjectionDefinition<ExampleByIdProjectionState, ExampleId>,
        IEventHandler<ExampleCreated>,
        IAmNotSnapshotable
    {
    		// ...
    }
    public class GetExampleController : ControllerBase
    {
        private IProjectionReader projectionReader;
        
        public GetExampleController(IProjectionReader projectionReader)
        {
            this.projectionReader = projectionReader;
        }
    
        public async Task<IActionResult> GetExample(GetExampleRequest request)
        {
    				var id = ExampleId.New(request.Tenant, request.Id);
            var result = await projectionReader.GetAsync<ExampleByIdProjection>(id);
            if (result.IsSuccess)
                return Ok(new GetExampleResponse(result.Data.State));
            else
                return BadRequest(result.Error);
        }
    
    		public class GetExampleResponse
    		{
    				// ...
    		}
    }

    Gateways

    https://github.com/Elders/Cronus/issues/260arrow-up-right

    Compared to a 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 it's 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 is 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.

    hashtag
    Communication Guide Table

    hashtag
    Best Practices

    circle-check

    You can/should/must...

    • a gateway can send new commands

    Application Services

    This is a handler where commands are received and delivered to the addressed aggregate. Such a handler is called an application service. 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 orchestrate how commands will be fulfilled. Its the application service's responsibility to invoke the appropriate aggregate methods and pass 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.

    Aggregatechevron-right

    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.

    hashtag
    Best Practices

    circle-check

    You can/should/must...

    • an application service can load an aggregate root from the event store

    circle-exclamation

    You should not...

    • an application service should not update more than one aggregate root in a single command/handler

    Triggered by

    Description

    Event

    Domain events represent business changes which have already happened

    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.

  • 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

  • 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(...));
        }
    
        ...
    }

    Handlers

    Public Events

    https://github.com/Elders/Cronus/issues/277arrow-up-right

    Ports

    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.

    hashtag
    Key Characteristics of Ports

    • 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.

    • Statelessness: Ports do not maintain any persistent state. Their sole responsibility is to handle the routing of events to appropriate command handlers.

    hashtag
    When to Use Ports

    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.

    hashtag
    Communication Guide Table

    Triggered by
    Description

    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

    Event

    Domain events represent business changes that have already happened.

    [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;
        }
    }

    Triggers

    https://github.com/Elders/Cronus/issues/261arrow-up-right