For making a transaction, user has to follow these steps:
- create a class-type, which inherits
SagaOperationBase<TRollbackData>. This class stands for an operation in a transaction. - implement
CommitAsync()andRollbackAsync()methods - using
SagaExecutorBuildercreate an instance ofSagaExecutor - register all of operations that developer wants to commit in a single transaction using
SagaExecutor.RegisterOperation()method - execute transaction using
SagaExecutor.ExecuteTransactionAsync()method
From now on everything is done by transaction executor and user doesn't need to worry about anything. Here are possible outcomes:
- if all operations in a transaction succeed, all operations are marked as
Comittedand transaction is in aFinishedCorrectlystatus. There is nothing we can do about transaction anymore. - if first operation has failed, we don't even need to rollback anything. So transaction is created, but no operations are bound to that transaction. Again we don't do anything (just placing it in
Failedstate in database) - if first operation has succeeded, but then any operation has failed, we mark every operation with
NeedsToRollbackstatus. Then we start rollback-ing all of them till the one that has failed.
This library is stateful, so it saves statuses of committed and rollback-ed operations in a database. There are two tables that contain all of the information about transaction operations.
distributed_transaction table has unique id, a text identifier for a transaction_type (i.e. it could be CreateLogisticOrderAndHandoverTransactionType) and a status
CREATE TABLE distributed_transaction
(
id BIGSERIAL NOT NULL CONSTRAINT distributed_transaction_id_pk PRIMARY KEY,
transaction_type TEXT NOT NULL,
status TEXT NOT NULL
);
distributed_transaction_operations table stands for an operation. It has a transaction_id as an foreign key to distributed_transaction table.
Also operation saves rollback_data_type, rollback_data and executor_type. It is very important to understand meaning of these fields.
CREATE TABLE distributed_transaction_operation
(
id BIGSERIAL NOT NULL PRIMARY KEY,
transaction_id BIGINT NOT NULL,
operation_type TEXT NOT NULL,
rollback_priority INT,
execution_stage INT,
rollback_data_type TEXT NOT NULL,
executor_type TEXT NOT NULL,
rollback_data TEXT NOT NULL,
status TEXT NOT NULL
);
rollback_datais a serialized string value of data needed forRollbackAsync()method.rollback_data_typeis aSystem.Typevalue saved in a string representation, so that we can use reflection and recreate data programmatically.executor_typeis a user-defined type of operation. We need to save that type to recreate class using reflection.
When transaction succeeds, there is not much we need to do.
But when transaction is in rollback stage, we are retrieving rollback_data and rollback_data_type from database, creating an instance using Activator.CreateInstance and executing RollbackAsync.
There is one trouble left - we need to somehow inject services, httpClients and other objects to operation instance, so that we can i.e. send a delete request to another microservice for rollback-ing the creation of some items.
So there is a special interface for injecting any services.
public interface ITransactionContext
{
T GetRequiredService<T>();
}There is a special implementation ServiceTransactionContext, that delegates an implementation to IServiceProvider of your ASP.NET service.
So that user can use any service registered by the mechanism Dependency Injection