From 4872dc071a07cb2a2582f88136e28606b15ce563 Mon Sep 17 00:00:00 2001 From: Benjamin Mayer Date: Tue, 9 Jul 2024 07:22:34 +0200 Subject: [PATCH 1/5] Started documentation in README --- Deployment/pos-debezium.yml | 2 +- README.md | 46 ++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Deployment/pos-debezium.yml b/Deployment/pos-debezium.yml index c3520d6..a909761 100644 --- a/Deployment/pos-debezium.yml +++ b/Deployment/pos-debezium.yml @@ -25,7 +25,7 @@ spec: - name: debezium-configmap configMap: name: debezium-configmap - restartPolicy: Always + restartPolicy: Always --- apiVersion: v1 kind: Service diff --git a/README.md b/README.md index 2cb856c..3d84b2c 100644 --- a/README.md +++ b/README.md @@ -40,4 +40,48 @@ minikube service pos-service You can also use `kubectl port-forward` to forward a port from your local machine to a port on a pod. For example: ```bash kubectl port-forward deployment/pos-service 8080:8080 -``` \ No newline at end of file +``` + +## Used patterns in microservice + +### Saga +The Saga pattern is used to maintain data consistency in a microservice architecture. It is a sequence of local transactions where each transaction updates the data and publishes a message or event to trigger the next transaction in the sequence. If a transaction fails, the saga executes a series of compensating transactions that undo the changes that were made by the preceding transactions. + +In the PlayOfferService the Saga Pattern is used in conjunction with the CourtService, to automatically create a Reservation if a PlayOffer is joined by a second Member. It includes the following Steps: +1. After `JoinPlayOfferCommand` is received, the PlayOfferService publishes a `PlayOfferJoinedEvent` +2. The CourtService listens to the `PlayOfferJoinedEvent` and tries to create a Reservation for the PlayOffer at the specified time in the PlayOfferJoinedEvent +3. The CourtService publishes one of three possible events, each containing the `EventId` of the `PlayOfferJoinedEvent` in the `CorrelationId`: + - `ReservationCreatedEvent` if the Reservation was successfully created + - `ReservationRejectedEvent` if the Reservation could not be created (e.g. no court available) + - `ReservationLimitExceededEvent` if the Reservation could not be created due to a limit of Reservations per Member +4. The PlayOfferService listens to the events published by the CourtService and reacts depending on the event: + - If a `ReservationCreatedEvent` is received it then triggers a `PlayOfferReservationAddedEvent` in the PlayOfferService to add the Reservation to the respective PlayOffer + - If a `ReservationRejectedEvent` or `ReservationLimitExceededEvent` is received it then triggers a `PlayOfferOpponentRemovedEvent` to revert the changes of the `PlayOfferJoinedEvent` + + +The **compensation logic** for the Saga is implemented in the [ReservationEventHandler](./Application/Handlers/Events/ReservationEventHandler.cs) File in the functions in line _81_ and _102_. + +### CQRS +The CQRS pattern is used to separate the read and write operations of a system. It allows for the creation of different models for reading and writing data, which can be optimized for their respective use cases. The CQRS pattern is used in the PlayOfferService to separate the read and write operations for PlayOffers. + +The write operations are implemented using commands, which are located in the [Commands](./Application/Commands) folder. The read operations are implemented using queries, which are located in the [Queries](./Application/Queries) folder. + +These commands and queries are then handled by their respective handlers, which are located in the root of the [Handlers](./Application/Handlers) folder. Each handler is responsible for executing the logic for a specific command or query. + +#### Projection +The CQRS pattern is also used to implement projections in the PlayOfferService. Projections are used to transform the data from the write model to the read model. In the PlayOfferService, projections are implemented using the **Mediator Pattern** which is implemented in the [Events](./Application/Handlers/Events) folder. + +Each Aggregate has a dedicated `RedisStreamReader` which subscribes to the Redis stream and listens to and parses the events for a specific aggregate, these can be found in the root of the [Application](./Application) folder. +Afterward each Event is handled by the respective `EventHandler` for the aggregates, which can be found in the [Events](./Application/Handlers/Events) folder. These `EventHandlers` then update the read model accordingly. + +### Event Sourcing +TODO: Beschreiben wo events implementiert werden, besoderheiten WriteSide als EventStore + apply methoden in Aggregates + +### Optimistic Locking +TODO: Beschreibung wo genau implementiert in command handler, wie es funktioniert + +### Domain Driven Design +TODO: Design von Entitäten, enthalten business logic + +### Transaction Log Trailing +TODO: implementation debezium, für was verantwortlich --> projection von write zu read seite \ No newline at end of file From 9d1569fb07989d775ba04f65132558e88a5e2d01 Mon Sep 17 00:00:00 2001 From: CengoleFHV Date: Tue, 9 Jul 2024 23:12:04 +0200 Subject: [PATCH 2/5] i hope this is righter --- README.md | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3d84b2c..68c4e27 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,24 @@ ## Deploying Kubernetes Manifests Locally with Minikube ### Prerequisites + - [Install Minikube](https://minikube.sigs.k8s.io/docs/start/) - [Install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) ### Step-by-Step Guide #### 1. Start Minikube + Start Minikube with sufficient resources. + ```bash minikube start --memory=4096 --cpus=2 ``` #### 2. Apply kubernetes Manifests + Use kubectl to apply each of these YAML files. This will create the necessary Kubernetes resources. + ```bash kubectl apply -f redis.yml kubectl apply -f pos_postgres_write.yml @@ -25,19 +30,24 @@ kubectl apply -f pos_service.yml ``` #### 3. Check the status of pods/services + Check the status of your pods and services to ensure they are running correctly. + ```bash kubectl get pods kubectl get services ``` #### 4. Access the service + Minikube provides a way to access services running inside the cluster using minikube service. + ```bash minikube service pos-service ``` You can also use `kubectl port-forward` to forward a port from your local machine to a port on a pod. For example: + ```bash kubectl port-forward deployment/pos-service 8080:8080 ``` @@ -45,9 +55,11 @@ kubectl port-forward deployment/pos-service 8080:8080 ## Used patterns in microservice ### Saga + The Saga pattern is used to maintain data consistency in a microservice architecture. It is a sequence of local transactions where each transaction updates the data and publishes a message or event to trigger the next transaction in the sequence. If a transaction fails, the saga executes a series of compensating transactions that undo the changes that were made by the preceding transactions. In the PlayOfferService the Saga Pattern is used in conjunction with the CourtService, to automatically create a Reservation if a PlayOffer is joined by a second Member. It includes the following Steps: + 1. After `JoinPlayOfferCommand` is received, the PlayOfferService publishes a `PlayOfferJoinedEvent` 2. The CourtService listens to the `PlayOfferJoinedEvent` and tries to create a Reservation for the PlayOffer at the specified time in the PlayOfferJoinedEvent 3. The CourtService publishes one of three possible events, each containing the `EventId` of the `PlayOfferJoinedEvent` in the `CorrelationId`: @@ -58,10 +70,10 @@ In the PlayOfferService the Saga Pattern is used in conjunction with the CourtSe - If a `ReservationCreatedEvent` is received it then triggers a `PlayOfferReservationAddedEvent` in the PlayOfferService to add the Reservation to the respective PlayOffer - If a `ReservationRejectedEvent` or `ReservationLimitExceededEvent` is received it then triggers a `PlayOfferOpponentRemovedEvent` to revert the changes of the `PlayOfferJoinedEvent` - The **compensation logic** for the Saga is implemented in the [ReservationEventHandler](./Application/Handlers/Events/ReservationEventHandler.cs) File in the functions in line _81_ and _102_. ### CQRS + The CQRS pattern is used to separate the read and write operations of a system. It allows for the creation of different models for reading and writing data, which can be optimized for their respective use cases. The CQRS pattern is used in the PlayOfferService to separate the read and write operations for PlayOffers. The write operations are implemented using commands, which are located in the [Commands](./Application/Commands) folder. The read operations are implemented using queries, which are located in the [Queries](./Application/Queries) folder. @@ -69,19 +81,82 @@ The write operations are implemented using commands, which are located in the [C These commands and queries are then handled by their respective handlers, which are located in the root of the [Handlers](./Application/Handlers) folder. Each handler is responsible for executing the logic for a specific command or query. #### Projection + The CQRS pattern is also used to implement projections in the PlayOfferService. Projections are used to transform the data from the write model to the read model. In the PlayOfferService, projections are implemented using the **Mediator Pattern** which is implemented in the [Events](./Application/Handlers/Events) folder. Each Aggregate has a dedicated `RedisStreamReader` which subscribes to the Redis stream and listens to and parses the events for a specific aggregate, these can be found in the root of the [Application](./Application) folder. Afterward each Event is handled by the respective `EventHandler` for the aggregates, which can be found in the [Events](./Application/Handlers/Events) folder. These `EventHandlers` then update the read model accordingly. ### Event Sourcing + TODO: Beschreiben wo events implementiert werden, besoderheiten WriteSide als EventStore + apply methoden in Aggregates +Event Sourcing is a pattern that involves storing the state of an application as a sequence of events. These events represent changes to the state of the application and can be used to reconstruct the state of the application at any point in time. + +First a Request is received and the Controller creates a Command, which is then handled by the appropriate CommandHandler. The CommandHandler then creates an Event, which is stored in the Event Store. + +This way the PostgreSQL DB acts as an Event Store, which stores the events for each Aggregate. The events are then used to reconstruct the state of the application by applying the events to the Aggregates. + +The events are stored in the `events` table in the database, which contains the following columns: + +- `event_id`: The unique identifier for the event +- `entity_id`: The unique identifier for the entity that the event belongs to +- `event_type`: The type of the event +- `entity_type`: The type of the entity that the event belongs to +- `event_data`: The data associated with the event +- `timestamp`: The timestamp when the event occurred +- `correlation_id`: The correlation id of the event + +The events are then applied to the Aggregates by calling the `Apply` method on the Aggregate, which updates the state of the Aggregate based on the event. + +The Implementation of the Apply method can be found in the Aggregates classes in the [Models](./Domain/Models) folder. + +The Command Handlers `CancelPlayOfferHandler`, `CreatePlayOfferHandler` and `JoinPlayOfferHandler` are implemented in the [Commands](./Application/Handlers) folder and the Event Handlers are implemented in the [Events](./Application/Handlers/Events) + ### Optimistic Locking -TODO: Beschreibung wo genau implementiert in command handler, wie es funktioniert + +Optimistic Locking is a concurrency control mechanism that is used to prevent conflicts when multiple requests try to change or create data at the same time. + +In the PlayOfferService, Optimistic Locking is implemented using the `EFCore` and its transaction mechanism. When a request is received, the current amount of events is read and incremented by one. + +When the request is processed, the amount of events is read again and compared to the initial amount. If the amount of events has changed unexpectedly during the transaction, a concurrency exception is thrown and the transaction rolled back. + +otherwise the transaction is committed and the changes are saved to the database. + +The Optimistic Locking is implemented in the each CommandHandler in the [Commands](./Application/Handlers) folder. + +- [`CancelPlayOfferHandler`](./Application/Handlers/CancelPlayOfferHandler.cs) + - _Line 26:27_ + - _Line 67:75_ +- [`JoinPlayOfferHandler`](./Application/Handlers/JoinPlayOfferHandler.cs) + - _Line 29:30_ + - _Line 88:96_ +- [`CreatePlayOfferHandler`](./Application/Handlers/CreatePlayOfferHandler.cs) + - _Line 69:70_ + - _Line 75:83_ ### Domain Driven Design + TODO: Design von Entitäten, enthalten business logic +Domain-Driven Design (DDD) is an approach to software development that focuses on the core domain and domain logic of the application. It is used to model complex domains in software by creating a shared understanding of the domain between technical and non-technical stakeholders. + +In the PlayOfferService, DDD is used to model the core domain of the application, which includes the following entities: + +- `PlayOffer`: Represents a play offer that is created by a member and can be joined by other members +- `Member`: Represents a member of the platform who can create and join play offers +- `Reservation`: Represents a reservation for a play offer that is created by the court service +- `Court`: Represents a court that can be reserved for a play offer +- `Club`: Represents a club that can have multiple courts and members + ### Transaction Log Trailing -TODO: implementation debezium, für was verantwortlich --> projection von write zu read seite \ No newline at end of file + +TODO: implementation debezium, für was verantwortlich --> projection von write zu read seite + +Transaction Log Trailing is a technique used to capture changes to a database by reading the transaction log of the database. It is used to implement change data capture (CDC) in a microservice architecture. + +In the PlayOfferService, Transaction Log Trailing is implemented using Debezium, which is an open-source platform for change data capture. Debezium captures changes to the PostgreSQL database and publishes them as events to a Redis Stream. + +Debezium is a separate service that runs in the Kubernetes cluster and listens to the PostgreSQL database for changes. When a change occurs, Debezium captures the change and publishes it as an event to the Redis Stream. + +the Debezium configuration can be found in the [pos_debezium.yml](./debezium-conf/application.properties) file. From 634e438ce915ea8d2541059a2d4ce935ee142f58 Mon Sep 17 00:00:00 2001 From: CengoleFHV Date: Tue, 9 Jul 2024 23:12:37 +0200 Subject: [PATCH 3/5] removed todos --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 68c4e27..5481b52 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,6 @@ Afterward each Event is handled by the respective `EventHandler` for the aggrega ### Event Sourcing -TODO: Beschreiben wo events implementiert werden, besoderheiten WriteSide als EventStore + apply methoden in Aggregates - Event Sourcing is a pattern that involves storing the state of an application as a sequence of events. These events represent changes to the state of the application and can be used to reconstruct the state of the application at any point in time. First a Request is received and the Controller creates a Command, which is then handled by the appropriate CommandHandler. The CommandHandler then creates an Event, which is stored in the Event Store. @@ -137,8 +135,6 @@ The Optimistic Locking is implemented in the each CommandHandler in the [Command ### Domain Driven Design -TODO: Design von Entitäten, enthalten business logic - Domain-Driven Design (DDD) is an approach to software development that focuses on the core domain and domain logic of the application. It is used to model complex domains in software by creating a shared understanding of the domain between technical and non-technical stakeholders. In the PlayOfferService, DDD is used to model the core domain of the application, which includes the following entities: @@ -151,8 +147,6 @@ In the PlayOfferService, DDD is used to model the core domain of the application ### Transaction Log Trailing -TODO: implementation debezium, für was verantwortlich --> projection von write zu read seite - Transaction Log Trailing is a technique used to capture changes to a database by reading the transaction log of the database. It is used to implement change data capture (CDC) in a microservice architecture. In the PlayOfferService, Transaction Log Trailing is implemented using Debezium, which is an open-source platform for change data capture. Debezium captures changes to the PostgreSQL database and publishes them as events to a Redis Stream. From 75b14b039f5a4600d7da6ea430dd39a522c85d9d Mon Sep 17 00:00:00 2001 From: Benjamin Mayer Date: Wed, 10 Jul 2024 20:53:48 +0200 Subject: [PATCH 4/5] Expanded readme --- README.md | 129 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 91 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 5481b52..838b4d2 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ kubectl port-forward deployment/pos-service 8080:8080 ## Used patterns in microservice ### Saga - +TODO: Add file/line numbers The Saga pattern is used to maintain data consistency in a microservice architecture. It is a sequence of local transactions where each transaction updates the data and publishes a message or event to trigger the next transaction in the sequence. If a transaction fails, the saga executes a series of compensating transactions that undo the changes that were made by the preceding transactions. In the PlayOfferService the Saga Pattern is used in conjunction with the CourtService, to automatically create a Reservation if a PlayOffer is joined by a second Member. It includes the following Steps: @@ -70,10 +70,10 @@ In the PlayOfferService the Saga Pattern is used in conjunction with the CourtSe - If a `ReservationCreatedEvent` is received it then triggers a `PlayOfferReservationAddedEvent` in the PlayOfferService to add the Reservation to the respective PlayOffer - If a `ReservationRejectedEvent` or `ReservationLimitExceededEvent` is received it then triggers a `PlayOfferOpponentRemovedEvent` to revert the changes of the `PlayOfferJoinedEvent` -The **compensation logic** for the Saga is implemented in the [ReservationEventHandler](./Application/Handlers/Events/ReservationEventHandler.cs) File in the functions in line _81_ and _102_. +The **compensation logic** for the Saga is implemented in the [ReservationEventHandler](./Application/Handlers/Events/ReservationEventHandler.cs) File in the functions in lines _81 - 102_. ### CQRS - +TODO: Add file/line numbers The CQRS pattern is used to separate the read and write operations of a system. It allows for the creation of different models for reading and writing data, which can be optimized for their respective use cases. The CQRS pattern is used in the PlayOfferService to separate the read and write operations for PlayOffers. The write operations are implemented using commands, which are located in the [Commands](./Application/Commands) folder. The read operations are implemented using queries, which are located in the [Queries](./Application/Queries) folder. @@ -81,76 +81,129 @@ The write operations are implemented using commands, which are located in the [C These commands and queries are then handled by their respective handlers, which are located in the root of the [Handlers](./Application/Handlers) folder. Each handler is responsible for executing the logic for a specific command or query. #### Projection - +TODO: Add file/line numbers The CQRS pattern is also used to implement projections in the PlayOfferService. Projections are used to transform the data from the write model to the read model. In the PlayOfferService, projections are implemented using the **Mediator Pattern** which is implemented in the [Events](./Application/Handlers/Events) folder. Each Aggregate has a dedicated `RedisStreamReader` which subscribes to the Redis stream and listens to and parses the events for a specific aggregate, these can be found in the root of the [Application](./Application) folder. Afterward each Event is handled by the respective `EventHandler` for the aggregates, which can be found in the [Events](./Application/Handlers/Events) folder. These `EventHandlers` then update the read model accordingly. +### Queries +TODO: Add file/line numbers, describe how it is implemented + +### Commands +TODO: Add file/line numbers, describe how it is implemented + ### Event Sourcing +The write side of the CQRS implementation is using a event sourcing pattern. In the PlayOfferService, events are used to represent changes to the state of Entities. +When a command is received, it is validated and then converted into one or more events, which are then stored in the write side database. -Event Sourcing is a pattern that involves storing the state of an application as a sequence of events. These events represent changes to the state of the application and can be used to reconstruct the state of the application at any point in time. +The events are structured with a hierarchy of event classes: +- `Technical[...]Event`: Represents a group of events that are used for a specific entity, these are used to route the events to the correct `EventHandler` in the read model. Implements the `BaseEvent` class. + - [`TechnicalPlayOfferEvent.cs`](./Domain/Events/PlayOffer/TechnicalPlayOfferEvent.cs)(_Line 1:7_): Represents the events for the `PlayOffer` entity + - [`TechnicalMemberEvent.cs`](./Domain/Events/Member/TechnicalMemberEvent.cs)(_Line 1:7_): Represents the events for the `Member` entity + - [`TechnicalReservationEvent.cs`](./Domain/Events/Reservation/TechnicalReservationEvent.cs)(_Line 1:8_): Represents the events for the `Reservation` entity + - [`TechnicalCourtEvent.cs`](./Domain/Events/Court/TechnicalCourtEvent.cs)(_Line 1:7_): Represents the events for the `Court` entity + - [`TechnicalClubEvent.cs`](./Domain/Events/Club/TechnicalClubEvent.cs)(_Line 1:7_): Represents the events for the `Club` entity +- [`BaseEvent.cs`](./Domain/Events/BaseEvent.cs)(_Line 1:34_): Represents the whole event including the following metadata: +Each event class represents a specific type of event that can occur in the system. + - `event_id`: The unique identifier for the event + - `entity_id`: The unique identifier for the entity that the event belongs to + - `event_type`: The type of the event + - `entity_type`: The type of the entity that the event belongs to + - `timestamp`: The timestamp when the event occurred + - `correlation_id`: The correlation id of the event +- [`DomainEvent.cs`](./Domain/Events/DomainEvent.cs)(_Line 1:35_): Is used as the data type of the `eventData` property in the `BaseEvent` class. It is also used for json serialization and deserialization. -First a Request is received and the Controller creates a Command, which is then handled by the appropriate CommandHandler. The CommandHandler then creates an Event, which is stored in the Event Store. +The smallest unit of events can be found in the [Events](./Domain/Events) folder. Each event class represents a specific type of event that can occur in the system and implements the `DomainEvent` class. +- **PlayOfferEvents**: + - [`PlayOfferCreatedEvent.cs`](./Domain/Events/PlayOffer/PlayOfferCreatedEvent.cs)(_Line 1:26_): Represents the event when a PlayOffer is created + - [`PlayOfferJoinedEvent.cs`](./Domain/Events/PlayOffer/PlayOfferJoinedEvent.cs)(_Line 1:9_): Represents the event when a Opponent joins a PlayOffer + - [`PlayOfferCancelledEvent.cs`](./Domain/Events/PlayOffer/PlayOfferCancelledEvent.cs)(_Line 1:6_): Represents the event when a PlayOffer is canceled + - [`PlayOfferReservationAddedEvent.cs`](./Domain/Events/PlayOffer/PlayOfferReservationAddedEvent.cs)(_Line 1:6_): Represents the event when a Reservation was created by the court service and was now added to the PlayOffer + - [`PlayOfferOpponentRemovedEvent.cs`](./Domain/Events/PlayOffer/PlayOfferOpponentRemovedEvent.cs)(_Line 1:5_): Represents the event when no Reservation could be created by the court service and therefore the opponent was removed from the PlayOffer -This way the PostgreSQL DB acts as an Event Store, which stores the events for each Aggregate. The events are then used to reconstruct the state of the application by applying the events to the Aggregates. -The events are stored in the `events` table in the database, which contains the following columns: +- **MemberEvents**: + - [`MemberCreatedEvent.cs`](./Domain/Events/Member/MemberCreatedEvent.cs)(_Line 1:13_): Represents the event when a Member is created + - [`MemberDeletedEvent.cs`](./Domain/Events/Member/MemberDeletedEvent.cs)(_Line 1:5_): Represents the event when a Member is deleted + - [`MemberEmailChangedEvent.cs`](./Domain/Events/Member/MemberEmailChangedEvent.cs)(_Line 1:6_): Represents the event when the email of a Member is changed + - [`MemberFullNameChangedEvent.cs`](./Domain/Events/Member/MemberFullNameChangedEvent.cs)(_Line 1:8_): Represents the event when the name of a Member is changed + - [`MemberLockedEvent.cs`](./Domain/Events/Member/MemberLockedEvent.cs)(_Line 1:6_): Represents the event when a Member is locked + - [`MemberUnlockedEvent.cs`](./Domain/Events/Member/MemberUnlockedEvent.cs)(_Line 1:6_): Represents the event when a Member is unlocked -- `event_id`: The unique identifier for the event -- `entity_id`: The unique identifier for the entity that the event belongs to -- `event_type`: The type of the event -- `entity_type`: The type of the entity that the event belongs to -- `event_data`: The data associated with the event -- `timestamp`: The timestamp when the event occurred -- `correlation_id`: The correlation id of the event -The events are then applied to the Aggregates by calling the `Apply` method on the Aggregate, which updates the state of the Aggregate based on the event. +- **ReservationEvents**: + - [`ReservationCreatedEvent.cs`](./Domain/Events/Reservation/ReservationCreatedEvent.cs)(_Line 1:21_): Represents the event when a Reservation is created + - [`ReservationCancelledEvent.cs`](./Domain/Events/Reservation/ReservationCancelledEvent.cs)(_Line 1:5_): Represents the event when a Reservation is canceled + - [`ReservationLimitExceededEvent.cs`](./Domain/Events/Reservation/ReservationLimitExceededEvent.cs)(_Line 1:21_): Represents the event when the limit of Reservations per Member is exceeded + - [`ReservationRejectedEvent.cs`](./Domain/Events/Reservation/ReservationRejectedEvent.cs)(_Line 1:6_): Represents the event when a Reservation could not be created -The Implementation of the Apply method can be found in the Aggregates classes in the [Models](./Domain/Models) folder. -The Command Handlers `CancelPlayOfferHandler`, `CreatePlayOfferHandler` and `JoinPlayOfferHandler` are implemented in the [Commands](./Application/Handlers) folder and the Event Handlers are implemented in the [Events](./Application/Handlers/Events) +- **CourtEvents**: + - [`CourtCreatedEvent.cs`](./Domain/Events/Court/CourtCreatedEvent.cs)(_Line 1:14_): Represents the event when a Court is created + - [`CourtUpdatedEvent.cs`](./Domain/Events/Court/CourtUpdatedEvent.cs)(_Line 1:12_): Represents the event when a Court is changed -### Optimistic Locking -Optimistic Locking is a concurrency control mechanism that is used to prevent conflicts when multiple requests try to change or create data at the same time. +- **ClubEvents**: + - [`ClubCreatedEvent.cs`](./Domain/Events/Club/ClubCreatedEvent.cs)(_Line 1:13_): Represents the event when a Club is created + - [`ClubDeletedEvent.cs`](./Domain/Events/Club/ClubDeletedEvent.cs)(_Line 1:16_): Represents the event when a Club is deleted + - [`ClubNameChangedEvent.cs`](./Domain/Events/Club/ClubNameChangedEvent.cs)(_Line 1:6_): Represents the event when the name of a Club is changed + - [`ClubLockedEvent.cs`](./Domain/Events/Club/ClubLockedEvent.cs)(_Line 1:6_): Represents the event when a Club is locked + - [`ClubUnlockedEvent.cs`](./Domain/Events/Club/ClubUnlockedEvent.cs)(_Line 1:6_): Represents the event when a Club is unlocked + + +The events are applied to the entities in the `apply` methods, the implementation location can be found under [Domain Driven Design](#domain-driven-design). + +#### Idempotent Events +In the PlayOfferService, the idempotency of all events is guaranteed! +All events which were read from the redis stream and were processed by the `EventHandlers` are saved into the `AppliedEvents` table in the read side database. This allows us to check if a received event was already processed and therefore can be ignored. +Therefore the outcome of all events won't change if they are processed multiple times. + +### Authentication and Authorization +TODO: Add file/line numbers, describe how it is implemented + + +### Optimistic Locking In the PlayOfferService, Optimistic Locking is implemented using the `EFCore` and its transaction mechanism. When a request is received, the current amount of events is read and incremented by one. When the request is processed, the amount of events is read again and compared to the initial amount. If the amount of events has changed unexpectedly during the transaction, a concurrency exception is thrown and the transaction rolled back. -otherwise the transaction is committed and the changes are saved to the database. +Otherwise the transaction is committed and the changes are saved to the database. The Optimistic Locking is implemented in the each CommandHandler in the [Commands](./Application/Handlers) folder. -- [`CancelPlayOfferHandler`](./Application/Handlers/CancelPlayOfferHandler.cs) +- [`CancelPlayOfferHandler.cs`](./Application/Handlers/CancelPlayOfferHandler.cs) - _Line 26:27_ - _Line 67:75_ -- [`JoinPlayOfferHandler`](./Application/Handlers/JoinPlayOfferHandler.cs) +- [`JoinPlayOfferHandler.cs`](./Application/Handlers/JoinPlayOfferHandler.cs) - _Line 29:30_ - _Line 88:96_ -- [`CreatePlayOfferHandler`](./Application/Handlers/CreatePlayOfferHandler.cs) +- [`CreatePlayOfferHandler.cs`](./Application/Handlers/CreatePlayOfferHandler.cs) - _Line 69:70_ - _Line 75:83_ ### Domain Driven Design - -Domain-Driven Design (DDD) is an approach to software development that focuses on the core domain and domain logic of the application. It is used to model complex domains in software by creating a shared understanding of the domain between technical and non-technical stakeholders. - In the PlayOfferService, DDD is used to model the core domain of the application, which includes the following entities: -- `PlayOffer`: Represents a play offer that is created by a member and can be joined by other members -- `Member`: Represents a member of the platform who can create and join play offers -- `Reservation`: Represents a reservation for a play offer that is created by the court service -- `Court`: Represents a court that can be reserved for a play offer -- `Club`: Represents a club that can have multiple courts and members - -### Transaction Log Trailing +- [`PlayOffer.cs`](./Domain/Models/PlayOffer.cs)(_Line 1:81_): Represents a play offer that is created by a member and can be joined by other members +- [`Member.cs`](./Domain/Models/Member.cs)(_Line 1:81_): Represents a member of the platform who can create and join play offers +- [`Reservation.cs`](./Domain/Models/Reservation.cs)(_Line 1:51_): Represents a reservation for a play offer that is created by the court service +- [`Court.cs`](./Domain/Models/Court.cs)(_Line 1:45_): Represents a court that can be reserved for a play offer +- [`Club.cs`](./Domain/Models/Club.cs)(_Line 1:66_): Represents a club that can have multiple courts and members -Transaction Log Trailing is a technique used to capture changes to a database by reading the transaction log of the database. It is used to implement change data capture (CDC) in a microservice architecture. +Since event sourcing was also used each entity implements a `apply` method which is used to apply the events to the entity. It is important to note that the `apply` method is not allowed to fail, as it is used to reconstruct the state of the entity and the correctness of the events is guaranteed by the `CommandHandlers +`. +The implementation for the `apply` methods can be found here: +- [`PlayOffer.cs`](./Domain/Models/PlayOffer.cs)(_Line 23_) +- [`Member.cs`](./Domain/Models/Member.cs)(_Line 17_) +- [`Reservation.cs`](./Domain/Models/Reservation.cs)(_Line 18_) +- [`Court.cs`](./Domain/Models/Court.cs)(_Line 14_) +- [`Club.cs`](./Domain/Models/Club.cs)(_Line 14_) -In the PlayOfferService, Transaction Log Trailing is implemented using Debezium, which is an open-source platform for change data capture. Debezium captures changes to the PostgreSQL database and publishes them as events to a Redis Stream. +However, we didn't implement a `process` method in each entity, since the processing of the events is done in the `CommandHandlers`. -Debezium is a separate service that runs in the Kubernetes cluster and listens to the PostgreSQL database for changes. When a change occurs, Debezium captures the change and publishes it as an event to the Redis Stream. +### Transaction Log Trailing +In the PlayOfferService, Transaction Log Trailing is implemented using Debezium, which is an open-source platform for change data capture. Debezium captures changes to the PostgreSQL database and publishes them to a Redis Stream. -the Debezium configuration can be found in the [pos_debezium.yml](./debezium-conf/application.properties) file. +The Debezium configuration can be found in the [pos_debezium.yml](./debezium-conf/application.properties)(_Line 1:21_) file. From f9cd43d15b474fff8862e9582c6d7e9af7be24a1 Mon Sep 17 00:00:00 2001 From: Benjamin Mayer Date: Thu, 11 Jul 2024 09:47:43 +0200 Subject: [PATCH 5/5] added openapi config, expanded readme --- README.md | 64 +++++-- openapi.json | 470 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 516 insertions(+), 18 deletions(-) create mode 100644 openapi.json diff --git a/README.md b/README.md index 838b4d2..1954a72 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,14 @@ You can also use `kubectl port-forward` to forward a port from your local machin kubectl port-forward deployment/pos-service 8080:8080 ``` +## API Documentation +We provided a OpenAPI documentation for the PlayOfferService. +It can be found in the [`openapi.json`](./openapi.json) file. + ## Used patterns in microservice +**All `.cs` files are linked to the respective file in the project** ### Saga -TODO: Add file/line numbers The Saga pattern is used to maintain data consistency in a microservice architecture. It is a sequence of local transactions where each transaction updates the data and publishes a message or event to trigger the next transaction in the sequence. If a transaction fails, the saga executes a series of compensating transactions that undo the changes that were made by the preceding transactions. In the PlayOfferService the Saga Pattern is used in conjunction with the CourtService, to automatically create a Reservation if a PlayOffer is joined by a second Member. It includes the following Steps: @@ -73,25 +77,44 @@ In the PlayOfferService the Saga Pattern is used in conjunction with the CourtSe The **compensation logic** for the Saga is implemented in the [ReservationEventHandler](./Application/Handlers/Events/ReservationEventHandler.cs) File in the functions in lines _81 - 102_. ### CQRS -TODO: Add file/line numbers -The CQRS pattern is used to separate the read and write operations of a system. It allows for the creation of different models for reading and writing data, which can be optimized for their respective use cases. The CQRS pattern is used in the PlayOfferService to separate the read and write operations for PlayOffers. - -The write operations are implemented using commands, which are located in the [Commands](./Application/Commands) folder. The read operations are implemented using queries, which are located in the [Queries](./Application/Queries) folder. +The CQRS pattern is used in the PlayOfferService to separate the read and write operations for PlayOffers. The write operations are implemented using commands, which are located in the [Commands](./Application/Commands) folder. The read operations are implemented using queries, which are located in the [Queries](./Application/Queries) folder. +Each query and command is then handled by their respective handlers, which are located in the root of the [Handlers](./Application/Handlers) folder. Each handler is responsible for executing the logic for a specific command or query. + +#### Queries +The following queries are implemented in the PlayOfferService with their respective handlers: +- [`GetPlayOffersByClubIdQuery.cs`](./Application/Queries/GetPlayOffersByClubIdQuery.cs)(_Line 1:8_): Returns all PlayOffers for a specific Club. The query is created and sent to the handler in the [`PlayOfferController.cs`](./Application/Controllers/PlayOfferController.cs)(_Line 40_). + - [`GetPlayOffersByClubIdHandler.cs`](./Application/Handlers/GetPlayOffersByClubIdHandler.cs)(_Line 1:49_): Handles the `GetPlayOffersByClubIdQuery` and returns the PlayOffers for the specified Club +- [`GetPlayOffersByParticipantIdQuery.cs`](./Application/Queries/GetPlayOffersByParticipantIdQuery.cs)(_Line 1:8_): Returns all PlayOffers for a specific participant (either as creator or opponent). The query is created and sent to the handler in the [`PlayOfferController.cs`](./Application/Controllers/PlayOfferController.cs)(_Line 64_). + - [`GetPlayOffersByParticipantIdHandler.cs`](./Application/Handlers/GetPlayOffersByParticipantIdHandler.cs)(_Line 1:49_): Handles the `GetPlayOffersByParticipantIdQuery` and returns the PlayOffers for the specified participant +- [`GetPlayOffersByCreatorNameQuery.cs`](./Application/Queries/GetPlayOffersByCreatorNameQuery.cs)(_Line 1:8_): Returns a specific PlayOffer by the name of it's creator. The query is created and sent to the handler in the [`PlayOfferController.cs`](./Application/Controllers/PlayOfferController.cs)(_Line 91_). + - [`GetPlayOffersByCreatorNameHandler.cs`](./Application/Handlers/GetPlayOffersByCreatorNameHandler.cs)(_Line 1:59_): Handles the `GetPlayOfferByCreatorNameQuery` and returns the PlayOffer with the specified Id + +#### Commands +The following commands are implemented in the PlayOfferService with their respective handlers: +- [`CancelPlayOfferCommand.cs`](./Application/Commands/CancelPlayOfferCommand.cs)(_Line 1:7_): Cancels a PlayOffer. The command is created and sent to the handler in the [`PlayOfferController.cs`](./Application/Controllers/PlayOfferController.cs)(_Line 158_). + - [`CancelPlayOfferHandler.cs`](./Application/Handlers/CancelPlayOfferHandler.cs)(_Line 1:79_): Handles the `CancelPlayOfferCommand` and cancels the PlayOffer +- [`CreatePlayOfferCommand.cs`](./Application/Commands/CreatePlayOfferCommand.cs)(_Line 1:7_): Creates a new PlayOffer. The command is created and sent to the handler in the [`PlayOfferController.cs`](./Application/Controllers/PlayOfferController.cs)(_Line 128_). + - [`CreatePlayOfferHandler.cs`](./Application/Handlers/CreatePlayOfferHandler.cs)(_Line 1:87_): Handles the `CreatePlayOfferCommand` and creates a new +- [`JoinPlayOfferCommand.cs`](./Application/Commands/JoinPlayOfferCommand.cs)(_Line 1:7_): Joins a PlayOffer. The command is created and sent to the handler in the [`PlayOfferController.cs`](./Application/Controllers/PlayOfferController.cs)(_Line 192_). + - [`JoinPlayOfferHandler.cs`](./Application/Handlers/JoinPlayOfferHandler.cs)(_Line 1:100_): Handles the `JoinPlayOfferCommand` and joins the PlayOffer -These commands and queries are then handled by their respective handlers, which are located in the root of the [Handlers](./Application/Handlers) folder. Each handler is responsible for executing the logic for a specific command or query. #### Projection -TODO: Add file/line numbers -The CQRS pattern is also used to implement projections in the PlayOfferService. Projections are used to transform the data from the write model to the read model. In the PlayOfferService, projections are implemented using the **Mediator Pattern** which is implemented in the [Events](./Application/Handlers/Events) folder. - -Each Aggregate has a dedicated `RedisStreamReader` which subscribes to the Redis stream and listens to and parses the events for a specific aggregate, these can be found in the root of the [Application](./Application) folder. -Afterward each Event is handled by the respective `EventHandler` for the aggregates, which can be found in the [Events](./Application/Handlers/Events) folder. These `EventHandlers` then update the read model accordingly. - -### Queries -TODO: Add file/line numbers, describe how it is implemented - -### Commands -TODO: Add file/line numbers, describe how it is implemented +In the PlayOfferService, projections are implemented using the **Mediator Pattern** which is implemented, in dedicated `EventHandlers` for each entity, in the [Events](./Application/Handlers/Events) folder. + +Each Entity has a dedicated `RedisStreamReader` which subscribes to the Redis stream and listens to, filters and parses the events for a specific entity: +- [`PlayOfferEventHandler.cs`](./Application/Handlers/Events/PlayOfferEventHandler.cs)(_Line 1:94_): Handles the events for the `PlayOffer` entity +- [`MemberEventHandler.cs`](./Application/Handlers/Events/MemberEventHandler.cs)(_Line 1:126_): Handles the events for the `Member` entity +- [`ReservationEventHandler.cs`](./Application/Handlers/Events/ReservationEventHandler.cs)(_Line 1:151_): Handles the events for the `Reservation` entity +- [`CourtEventHandler.cs`](./Application/Handlers/Events/CourtEventHandler.cs)(_Line 1:57_): Handles the events for the `Court` entity +- [`ClubEventHandler.cs`](./Application/Handlers/Events/ClubEventHandler.cs)(_Line 1:116_): Handles the events for the `Club` entity + +The `EventHandlers` receive their events from the `RedisStreamService` and then apply the events to the respective entity: +- [`RedisClubStreamService.cs`](./Application/RedisClubStreamService.cs)(_Line 1:82_): Read the events from the redis club stream and sends them to the `ClubEventHandler` +- [`RedisCourtStreamService.cs`](./Application/RedisCourtStreamService.cs)(_Line 1:83_): Read the events from the redis court stream and sends them to the `CourtEventHandler` +- [`RedisMemberStreamService.cs`](./Application/RedisMemberStreamService.cs)(_Line 1:86_): Read the events from the redis member stream and sends them to the `MemberEventHandler` +- [`RedisPlayOfferStreamService.cs`](./Application/RedisPlayOfferStreamService.cs)(_Line 1:68_): Read the events from the redis play offer stream and sends them to the `PlayOfferEventHandler` +- [`RedisReservationStreamService.cs`](./Application/RedisReservationStreamService.cs)(_Line 1:76_): Read the events from the redis reservation stream and sends them to the `ReservationEventHandler` ### Event Sourcing The write side of the CQRS implementation is using a event sourcing pattern. In the PlayOfferService, events are used to represent changes to the state of Entities. @@ -161,8 +184,13 @@ All events which were read from the redis stream and were processed by the `Even Therefore the outcome of all events won't change if they are processed multiple times. ### Authentication and Authorization -TODO: Add file/line numbers, describe how it is implemented +In the PlayOfferService, Authentication and Authorization are implemented using a JWT token, which is is provided by the club service. All requests to the PlayOfferService must include a valid JWT token in the Authorization header. + +All Queries can be executed by users with the `ADMIN` and `MEMBER` role. The commands can only be executed by users with the `MEMBER` roles. +A custom [`JwtClaimsMiddleware.cs`](./JwtClaimsMiddleware.cs)(_Line 1:43_) is used to extract the claims from the JWT token and add them to the `HttpContext` of the request. +These claims are then checked with the `Authorize` attribute in the [`PlayOfferController.cs`](./Application/Controllers/PlayOfferController.cs)(_Lines 31,55,80,115,147,181_) to ensure that the user has the necessary roles to execute the request. +Furthermore, most requests also extract the `memberId` and/or the `clubId` from the claims to ensure that the user can only access their own data, this can be seen in [`PlayOfferController.cs`](./Application/Controllers/PlayOfferController.cs)(_Lines 39,63,122:123,154,189_). ### Optimistic Locking In the PlayOfferService, Optimistic Locking is implemented using the `EFCore` and its transaction mechanism. When a request is received, the current amount of events is read and incremented by one. diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..f55097e --- /dev/null +++ b/openapi.json @@ -0,0 +1,470 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "PlayOfferService API", + "description": "An ASP.NET Core Web API for managing PlayOffers", + "version": "v1" + }, + "paths": { + "/api/playoffers/club": { + "get": { + "tags": [ + "PlayOffer" + ], + "summary": "Retrieve all Play Offers of the logged in users club", + "responses": { + "200": { + "description": "Returns a list of Play offers matching the query params", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayOfferDto" + } + } + } + } + }, + "204": { + "description": "No Play offer with matching properties was found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResult" + } + } + } + } + } + } + }, + "/api/playoffers/participant": { + "get": { + "tags": [ + "PlayOffer" + ], + "summary": "Retrieve all Play Offers of a logged in user", + "responses": { + "200": { + "description": "Returns a list of Play offers matching the query params", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayOffer" + } + } + } + } + }, + "204": { + "description": "No Play offer with matching properties was found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResult" + } + } + } + } + } + } + }, + "/api/playoffers/search": { + "get": { + "tags": [ + "PlayOffer" + ], + "summary": "Get all Play offers created by a member with a matching name", + "parameters": [ + { + "name": "creatorName", + "in": "query", + "description": "Name of the creator in the format '[FirstName] [LastName]', '[FirstName]' or '[LastName]'", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Returns a List of Play offers with creator matching the query params", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayOffer" + } + } + } + } + }, + "204": { + "description": "No Play offers with matching creator was found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResult" + } + } + } + } + } + } + }, + "/api/playoffers": { + "post": { + "tags": [ + "PlayOffer" + ], + "summary": "Create a new Play Offer for the logged in user", + "requestBody": { + "description": "The Play Offer to create", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePlayOfferDto" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlayOffer" + } + } + } + }, + "400": { + "description": "Invalid Play Offer structure", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResult" + } + } + } + }, + "200": { + "description": "Returns the id of the created Play Offer" + }, + "401": { + "description": "Only members can create Play Offers" + } + } + }, + "delete": { + "tags": [ + "PlayOffer" + ], + "summary": "Cancels a Play Offer with a matching id of the logged in user", + "parameters": [ + { + "name": "playOfferId", + "in": "query", + "description": "The id of the Play Offer to cancel", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The Play Offer with the matching id was cancelled", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResult" + } + } + } + }, + "400": { + "description": "No Play Offer with matching id found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResult" + } + } + } + }, + "401": { + "description": "Only creator can cancel Play Offers" + } + } + } + }, + "/api/playoffers/join": { + "post": { + "tags": [ + "PlayOffer" + ], + "summary": "Logged in user joins a Play Offer with a matching playOfferId", + "requestBody": { + "description": "The opponentId to add to the Play Offer with the matching playOfferId", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JoinPlayOfferDto" + } + } + } + }, + "responses": { + "200": { + "description": "The opponentId was added to the Play Offer with the matching playOfferId", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResult" + } + } + } + }, + "400": { + "description": "No playOffer with a matching playOfferId found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResult" + } + } + } + }, + "401": { + "description": "Only members can join Play Offers" + } + } + } + } + }, + "components": { + "schemas": { + "ActionResult": { + "type": "object", + "additionalProperties": false + }, + "ClubDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/Status" + } + }, + "additionalProperties": false + }, + "CourtDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "CreatePlayOfferDto": { + "type": "object", + "properties": { + "proposedStartTime": { + "type": "string", + "format": "date-time" + }, + "proposedEndTime": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false + }, + "JoinPlayOfferDto": { + "type": "object", + "properties": { + "playOfferId": { + "type": "string", + "format": "uuid" + }, + "acceptedStartTime": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false + }, + "MemberDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "email": { + "type": "string", + "nullable": true + }, + "firstName": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/Status" + } + }, + "additionalProperties": false + }, + "PlayOffer": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "clubId": { + "type": "string", + "format": "uuid" + }, + "creatorId": { + "type": "string", + "format": "uuid" + }, + "opponentId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "proposedStartTime": { + "type": "string", + "format": "date-time" + }, + "proposedEndTime": { + "type": "string", + "format": "date-time" + }, + "acceptedStartTime": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "reservationId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "isCancelled": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "PlayOfferDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "club": { + "$ref": "#/components/schemas/ClubDto" + }, + "creator": { + "$ref": "#/components/schemas/MemberDto" + }, + "opponent": { + "$ref": "#/components/schemas/MemberDto" + }, + "proposedStartTime": { + "type": "string", + "format": "date-time" + }, + "proposedEndTime": { + "type": "string", + "format": "date-time" + }, + "acceptedStartTime": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "reservation": { + "$ref": "#/components/schemas/ReservationDto" + }, + "isCancelled": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "ReservationDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "court": { + "$ref": "#/components/schemas/CourtDto" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "endTime": { + "type": "string", + "format": "date-time" + }, + "isCancelled": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Status": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "format": "int32" + } + }, + "securitySchemes": { + "Bearer": { + "type": "apiKey", + "description": "JWT Authorization header using the Bearer scheme.", + "name": "Authorization", + "in": "header" + } + } + }, + "security": [ + { + "Bearer": [ ] + } + ] +} \ No newline at end of file