/// <summary> /// Consumes the specified saga message - finds the existing saga that can consume given message type and with matching CorrelationId. /// </summary> /// <param name="sagaMessage">The saga message.</param> /// <returns> /// Result of the operation /// </returns> /// <exception cref="System.ArgumentException"></exception> public OperationResult Consume(ISagaMessage sagaMessage) { Guard.CheckSagaMessage(sagaMessage, nameof(sagaMessage)); var resolvedSaga = sagaFactory.ResolveSagaConsumedBy(sagaMessage); var sagaType = resolvedSaga.GetType(); var saga = NSagaReflection.InvokeGenericMethod(sagaRepository, "Find", sagaType, sagaMessage.CorrelationId); if (saga == null) { throw new ArgumentException($"Saga with this CorrelationId does not exist. Please initiate a saga with IInitiatingMessage. {sagaMessage.CorrelationId}"); } pipelineHook.BeforeConsuming(new PipelineContext(sagaMessage, (IAccessibleSaga)saga)); var errors = (OperationResult)NSagaReflection.InvokeMethod(saga, "Consume", sagaMessage); pipelineHook.AfterConsuming(new PipelineContext(sagaMessage, (IAccessibleSaga)saga, errors)); if (errors.IsSuccessful) { sagaRepository.Save((IAccessibleSaga)saga); pipelineHook.AfterSave(new PipelineContext(sagaMessage, (IAccessibleSaga)saga, errors)); } return(errors); }
/// <summary> /// Finds and returns saga instance with the given correlation ID. /// You will get exceptions if TSaga does not match the actual saga data with the provided exception. /// /// Actually creates an instance of saga from service locator, retrieves SagaData and Headers from the storage and populates the instance with these. /// </summary> /// <typeparam name="TSaga">Type of saga we are looking for</typeparam> /// <param name="correlationId">CorrelationId to identify the saga</param> /// <returns>An instance of the saga. Or Null if there is no saga with this ID.</returns> public TSaga Find <TSaga>(Guid correlationId) where TSaga : class, IAccessibleSaga { Guard.ArgumentIsNotNull(correlationId, nameof(correlationId)); using (var connection = connectionFactory.CreateOpenConnection()) using (var database = new Database(connection)) { var sql = Sql.Builder.Where("correlationId = @0", correlationId); var persistedData = database.SingleOrDefault <SagaData>(sql); if (persistedData == null) { return(null); } var sagaInstance = sagaFactory.ResolveSaga <TSaga>(); var sagaDataType = NSagaReflection.GetInterfaceGenericType <TSaga>(typeof(ISaga <>)); var sagaData = messageSerialiser.Deserialise(persistedData.BlobData, sagaDataType); var headersSql = Sql.Builder.Where("correlationId = @0", correlationId); var headersPersisted = database.Query <SagaHeaders>(headersSql); var headers = headersPersisted.ToDictionary(k => k.Key, v => v.Value); NSagaReflection.Set(sagaInstance, "CorrelationId", correlationId); NSagaReflection.Set(sagaInstance, "SagaData", sagaData); NSagaReflection.Set(sagaInstance, "Headers", headers); return(sagaInstance); } }
/// <summary> /// Finds and returns saga instance with the given correlation ID. /// Actually creates an instance of saga from Saga factory, retrieves SagaData and Headers from the storage and populates the instance with these. /// </summary> /// <typeparam name="TSaga">Type of saga we are looking for</typeparam> /// <param name="correlationId">CorrelationId to identify the saga</param> /// <returns> /// An instance of the saga. Or Null if there is no saga with this ID. /// </returns> public TSaga Find <TSaga>(Guid correlationId) where TSaga : class, IAccessibleSaga { string dataSerialised; if (!DataDictionary.TryGetValue(correlationId, out dataSerialised)) { return(null); } string headersSerialised; var headers = new Dictionary <String, String>(); if (HeadersDictionary.TryGetValue(correlationId, out headersSerialised)) { headers = messageSerialiser.Deserialise <Dictionary <String, String> >(headersSerialised); } var sagaDataType = NSagaReflection.GetInterfaceGenericType <TSaga>(typeof(ISaga <>)); var dataObject = messageSerialiser.Deserialise(dataSerialised, sagaDataType); var saga = sagaFactory.ResolveSaga <TSaga>(); NSagaReflection.Set(saga, "SagaData", dataObject); NSagaReflection.Set(saga, "CorrelationId", correlationId); NSagaReflection.Set(saga, "Headers", headers); return(saga); }
/// <summary> /// Initializes a new instance of the <see cref="PipelineContext"/> class. /// </summary> /// <param name="message">The message.</param> /// <param name="saga">The saga.</param> /// <param name="operationResult">The operation result. Null of operation have not finished</param> public PipelineContext(ISagaMessage message, IAccessibleSaga saga, OperationResult operationResult = null) { this.Message = message; this.AccessibleSaga = saga; SagaData = NSagaReflection.Get(saga, "SagaData"); OperationResult = operationResult; }
/// <summary> /// Deletes the saga instance from the storage /// </summary> /// <typeparam name="TSaga">Type of saga</typeparam> /// <param name="saga">Saga to be deleted</param> public void Complete <TSaga>(TSaga saga) where TSaga : class, IAccessibleSaga { Guard.ArgumentIsNotNull(saga, nameof(saga)); var correlationId = (Guid)NSagaReflection.Get(saga, "CorrelationId"); Complete(correlationId); }
internal static void RegisterSagas(this TinyIoCContainer container, IEnumerable <Assembly> assembliesToScan) { var sagaTypePairs = NSagaReflection.GetAllSagasInterfaces(assembliesToScan); foreach (var sagaTypePair in sagaTypePairs) { container.Register(sagaTypePair.Value, sagaTypePair.Key); } }
/// <summary> /// Returns a list of all the Saga classes in the provided assemblies. /// </summary> /// <param name="assemblies">The assemblies to scan</param> /// <returns>List of Types that implement <see cref="ISaga{TSagaData}"/></returns> public static IEnumerable <Type> GetAllSagaTypes(IEnumerable <Assembly> assemblies) { var allSagaTypes = assemblies.SelectMany(a => a.GetTypes()) .Where(t => NSagaReflection.TypeImplementsInterface(t, typeof(ISaga <>))) .Where(t => t.IsClass) .ToList(); return(allSagaTypes); }
/// <summary> /// Persists the instance of saga into the database storage. /// /// Actually stores SagaData and Headers. All other variables in saga are not persisted /// </summary> /// <typeparam name="TSaga">Type of saga</typeparam> /// <param name="saga">Saga instance</param> public void Save <TSaga>(TSaga saga) where TSaga : class, IAccessibleSaga { Guard.ArgumentIsNotNull(saga, nameof(saga)); var sagaData = NSagaReflection.Get(saga, "SagaData"); var sagaHeaders = (Dictionary <String, String>)NSagaReflection.Get(saga, "Headers"); var correlationId = (Guid)NSagaReflection.Get(saga, "CorrelationId"); var serialisedData = messageSerialiser.Serialise(sagaData); var dataModel = new SagaData() { CorrelationId = correlationId, BlobData = serialisedData, }; using (var connection = connectionFactory.CreateOpenConnection()) using (var database = new Database(connection)) using (var transaction = database.GetTransaction()) { try { int updatedRaws = database.Update(dataModel); if (updatedRaws == 0) { // no records were updated - this means no records already exist - need to insert new record database.Insert(dataModel); } // delete all existing headers database.Delete <SagaHeaders>("WHERE CorrelationId=@0", correlationId); // and insert updated ones foreach (var header in sagaHeaders) { var storedHeader = new SagaHeaders() { CorrelationId = correlationId, Key = header.Key, Value = header.Value, }; database.Insert(storedHeader); } transaction.Complete(); } catch (Exception) { transaction.Dispose(); throw; } } }
/// <summary> /// Persists the instance of saga into the database storage. /// Actually stores SagaData and Headers. All other variables in saga are not persisted /// </summary> /// <typeparam name="TSaga">Type of saga</typeparam> /// <param name="saga">Saga instance</param> public void Save <TSaga>(TSaga saga) where TSaga : class, IAccessibleSaga { var sagaData = NSagaReflection.Get(saga, "SagaData"); var correlationId = (Guid)NSagaReflection.Get(saga, "CorrelationId"); var headers = (Dictionary <String, String>)NSagaReflection.Get(saga, "Headers"); var serialisedData = messageSerialiser.Serialise(sagaData); var serialisedHeaders = messageSerialiser.Serialise(headers); DataDictionary[correlationId] = serialisedData; HeadersDictionary[correlationId] = serialisedHeaders; }
/// <summary> /// Persists the instance of saga into the database storage. /// /// Actually stores SagaData and Headers. All other variables in saga are not persisted /// </summary> /// <typeparam name="TSaga">Type of saga</typeparam> /// <param name="saga">Saga instance</param> public void Save <TSaga>(TSaga saga) where TSaga : class, IAccessibleSaga { Guard.ArgumentIsNotNull(saga, nameof(saga)); var sagaData = NSagaReflection.Get(saga, "SagaData"); var sagaHeaders = saga.Headers; var correlationId = saga.CorrelationId; var serialisedData = messageSerialiser.Serialise(sagaData); var dataModel = new SagaData() { CorrelationId = correlationId, BlobData = serialisedData, }; using (var transaction = database.BeginTransaction()) { try { int updatedRaws = database.Update(dataModel); if (updatedRaws == 0) { // no records were updated - this means no records already exist - need to insert new record database.Insert(dataModel); } // delete all existing headers database.DeleteById <SagaHeaders>(correlationId); // and insert updated ones foreach (var header in sagaHeaders) { var storedHeader = new SagaHeaders() { CorrelationId = correlationId, Key = header.Key, Value = header.Value, }; database.Insert(storedHeader); } transaction.CommitTransaction(); } catch (Exception ex) { transaction.RollbackTransaction(); throw; } } }
/// <summary> /// Consumes the specified initiating message and creates a new instance of the correlating saga. /// <para>Saga is not persisted if operation have failed</para> /// </summary> /// <param name="initiatingMessage">The initiating message.</param> /// <returns> /// Result of the operation /// </returns> /// <exception cref="System.ArgumentException"></exception> public OperationResult Consume(IInitiatingSagaMessage initiatingMessage) { Guard.CheckSagaMessage(initiatingMessage, nameof(initiatingMessage)); var resolvedSaga = sagaFactory.ResolveSagaInititatedBy(initiatingMessage); var sagaType = resolvedSaga.GetType(); // try to find sagas that already exist var existingSaga = NSagaReflection.InvokeGenericMethod(sagaRepository, "Find", sagaType, initiatingMessage.CorrelationId); if (existingSaga != null) { throw new ArgumentException($"Trying to initiate the same saga twice. {initiatingMessage.GetType().Name} is Initiating Message, but saga of type {sagaType.Name} with CorrelationId {initiatingMessage.CorrelationId} already exists"); } // now create an instance of saga and persist the data var saga = sagaFactory.ResolveSaga(sagaType); NSagaReflection.Set(saga, "CorrelationId", initiatingMessage.CorrelationId); // if SagaData is null - create an instance of the object and assign to saga var sagaData = NSagaReflection.Get(saga, "SagaData"); if (sagaData == null) { var sagaDataType = NSagaReflection.GetInterfaceGenericType(saga, typeof(ISaga <>)); var newSagaData = Activator.CreateInstance(sagaDataType); NSagaReflection.Set(saga, "SagaData", newSagaData); } var sagaHeaders = NSagaReflection.Get(saga, "Headers"); if (sagaHeaders == null) { NSagaReflection.Set(saga, "Headers", new Dictionary <String, String>()); } pipelineHook.BeforeInitialisation(new PipelineContext(initiatingMessage, (IAccessibleSaga)saga)); var errors = (OperationResult)NSagaReflection.InvokeMethod(saga, "Initiate", initiatingMessage); pipelineHook.AfterInitialisation(new PipelineContext(initiatingMessage, (IAccessibleSaga)saga, errors)); if (errors.IsSuccessful) { sagaRepository.Save(saga); pipelineHook.AfterSave(new PipelineContext(initiatingMessage, (IAccessibleSaga)saga, errors)); } return(errors); }
/// <summary> /// Asserts that your sagas and messages are written correctly: /// <list type="bullet"> /// <item><description>All classes impmenenting <see cref="ISaga{TSagaData}"/> should also have <see cref="InitiatedBy{TMsg}"/> applied.</description></item> /// <item><description>Any saga message can only be used by a single Saga. No message sharing between different sagas are allowed.</description></item> /// <item><description>All saga messages are used by sagas. No loose classes with <see cref="ISagaMessage"/> or <see cref="IInitiatingSagaMessage"/> without sagas consuming them are allowed.</description></item> /// </list> /// </summary> /// <exception cref="AggregateException">Aggregation of all the exceptions for each rule violation</exception> public void AssertConfigurationIsValid() { var allExceptions = new List <Exception>(); sagaTypes = NSagaReflection.GetAllSagaTypes(assemblies); allExceptions.AddRange(AllSagasHaveInitialiser()); allExceptions.AddRange(InitiatorMessagesAreNotShared()); allExceptions.AddRange(ConsumerMessagesAreNotShared()); if (allExceptions.Any()) { throw new AggregateException(allExceptions); } }
/// <summary> /// Returns a list of all the Saga classes in the provided assemblies. /// </summary> /// <param name="assemblies">The assemblies to scan</param> /// <returns>List of Types that implement <see cref="ISaga{TSagaData}"/></returns> public static IEnumerable <Type> GetAllSagaTypes(IEnumerable <Assembly> assemblies) { try { var types = assemblies.SelectMany(a => a.GetTypes()).ToList(); var allSagaTypes = types .Where(t => NSagaReflection.TypeImplementsInterface(t, typeof(ISaga <>))) .Where(t => t.IsClass) .ToList(); return(allSagaTypes); } catch (ReflectionTypeLoadException ex) { //Display or log the error based on your application. throw new Exception(BuildFusionException(ex)); } }
/// <summary> /// Finds and returns saga instance with the given correlation ID. /// You will get exceptions if TSaga does not match the actual saga data with the provided exception. /// /// Actually creates an instance of saga from service locator, retrieves SagaData and Headers from the storage and populates the instance with these. /// </summary> /// <typeparam name="TSaga">Type of saga we are looking for</typeparam> /// <param name="correlationId">CorrelationId to identify the saga</param> /// <returns>An instance of the saga. Or Null if there is no saga with this ID.</returns> public TSaga Find <TSaga>(Guid correlationId) where TSaga : class, IAccessibleSaga { Guard.ArgumentIsNotNull(correlationId, nameof(correlationId)); var persistedData = database.GetById <SagaData>(correlationId).FirstOrDefault(); if (persistedData == null) { return(null); } var sagaInstance = sagaFactory.ResolveSaga <TSaga>(); var sagaDataType = NSagaReflection.GetInterfaceGenericType <TSaga>(typeof(ISaga <>)); var sagaData = messageSerialiser.Deserialise(persistedData.BlobData, sagaDataType); var headersPersisted = database.GetById <SagaHeaders>(correlationId); var headers = headersPersisted.ToDictionary(k => k.Key, v => v.Value); sagaInstance.CorrelationId = correlationId; sagaInstance.Headers = headers; NSagaReflection.Set(sagaInstance, "SagaData", sagaData); return(sagaInstance); }
/// <summary> /// Deletes the saga instance from the storage /// </summary> /// <typeparam name="TSaga">Type of saga</typeparam> /// <param name="saga">Saga to be deleted</param> public void Complete <TSaga>(TSaga saga) where TSaga : class, IAccessibleSaga { var correlationId = (Guid)NSagaReflection.Get(saga, "CorrelationId"); Complete(correlationId); }