public async Task Save(ISagaData sagaData, Dictionary<string, string> sagaAuditMetadata) { using (var connection = await _connectionHelper.GetConnection()) { using (var command = connection.CreateCommand()) { command.CommandText = $@" INSERT INTO ""{_tableName}"" (""id"", ""revision"", ""data"", ""metadata"") VALUES (@id, @revision, @data, @metadata); "; command.Parameters.Add("id", NpgsqlDbType.Uuid).Value = sagaData.Id; command.Parameters.Add("revision", NpgsqlDbType.Integer).Value = sagaData.Revision; command.Parameters.Add("data", NpgsqlDbType.Bytea).Value = _objectSerializer.Serialize(sagaData); command.Parameters.Add("metadata", NpgsqlDbType.Jsonb).Value = _dictionarySerializer.SerializeToString(sagaAuditMetadata); await command.ExecuteNonQueryAsync(); } connection.Complete(); } }
/// <summary> /// Updates the saga data /// </summary> public async Task Update(ISagaData sagaData, IEnumerable<ISagaCorrelationProperty> correlationProperties) { var id = GetId(sagaData); lock (_lock) { if (!_data.ContainsKey(id)) { throw new ConcurrencyException("Saga data with ID {0} no longer exists and cannot be updated", id); } VerifyCorrelationPropertyUniqueness(sagaData, correlationProperties); var existingCopy = _data[id]; if (existingCopy.Revision != sagaData.Revision) { throw new ConcurrencyException("Attempted to update saga data with ID {0} with revision {1}, but the existing data was updated to revision {2}", id, sagaData.Revision, existingCopy.Revision); } var clone = Clone(sagaData); clone.Revision++; _data[id] = clone; sagaData.Revision++; } }
public async Task Insert(ISagaData sagaData, IEnumerable<ISagaCorrelationProperty> correlationProperties) { if (sagaData.Id == Guid.Empty) { throw new InvalidOperationException($"Saga data {sagaData.GetType()} has an uninitialized Id property!"); } if (sagaData.Revision != 0) { throw new InvalidOperationException($"Attempted to insert saga data with ID {sagaData.Id} and revision {sagaData.Revision}, but revision must be 0 on first insert!"); } using (var session = _documentStore.OpenAsyncSession()) { session.Advanced.UseOptimisticConcurrency = true; var sagaDataDocumentId = SagaDataDocument.GetIdFromGuid(sagaData.Id); var existingSagaDataDocument = await session.LoadAsync<SagaDataDocument>(sagaDataDocumentId); if (existingSagaDataDocument != null) { throw new ConcurrencyException("Cannot insert document with an id that already exists"); } var sagaDataDocument = new SagaDataDocument(sagaData); await session.StoreAsync(sagaDataDocument, sagaDataDocumentId); var correlationPropertyDocumentIds = await SaveCorrelationProperties(session, sagaData, correlationProperties, sagaDataDocumentId); sagaDataDocument.SagaCorrelationPropertyDocumentIds = correlationPropertyDocumentIds; await session.SaveChangesAsync(); } }
/// <summary> /// Saves a snapshot of the saga data along with the given metadata /// </summary> public async Task Save(ISagaData sagaData, Dictionary<string, string> sagaAuditMetadata) { using (var connection = await _connectionProvider.GetConnection()) { using (var command = connection.CreateCommand()) { command.CommandText = string.Format(@" INSERT INTO [{0}] ( [id], [revision], [data], [metadata] ) VALUES ( @id, @revision, @data, @metadata ) ", _tableName); command.Parameters.Add("id", SqlDbType.UniqueIdentifier).Value = sagaData.Id; command.Parameters.Add("revision", SqlDbType.Int).Value = sagaData.Revision; command.Parameters.Add("data", SqlDbType.NVarChar).Value = JsonConvert.SerializeObject(sagaData, DataSettings); command.Parameters.Add("metadata", SqlDbType.NVarChar).Value = JsonConvert.SerializeObject(sagaAuditMetadata, MetadataSettings); await command.ExecuteNonQueryAsync(); } await connection.Complete(); } }
public void Insert(ISagaData sagaData, IEnumerable<ISagaCorrelationProperty> correlationProperties) { var indexItems = ReadIndexItems(); indexItems.RemoveAll(i => i.SagaId == sagaData.Id); indexItems.AddRange(GetPropertiesToIndex(sagaData, correlationProperties)); var path = SagaFilePath(sagaData.Id); File.WriteAllText(path, JsonConvert.SerializeObject(sagaData, _serializerSettings)); WriteSagaIndexItems(indexItems); }
public async Task Insert(ISagaData sagaData, IEnumerable<ISagaCorrelationProperty> correlationProperties) { if (sagaData.Id == Guid.Empty) { throw new InvalidOperationException(string.Format("Attempted to insert saga data {0} without an ID", sagaData.GetType())); } var collection = GetCollection(sagaData.GetType()); await collection.InsertOneAsync(sagaData.ToBsonDocument()).ConfigureAwait(false); }
public async Task Save(ISagaData sagaData, Dictionary<string, string> sagaAuditMetadata) { var document = new BsonDocument { {"_id", new {Id = sagaData.Id, Revision = sagaData.Revision}.ToBsonDocument()}, {"Metadata", sagaAuditMetadata.ToBsonDocument()}, {"Data", sagaData.ToBsonDocument()} }; await _snapshots.InsertOneAsync(document); }
Dictionary<string, string> GetMetadata(ISagaData sagaData, object handler, Message message) { return new Dictionary<string, string> { {SagaAuditingMetadataKeys.HandleQueue, _transport.Address}, {SagaAuditingMetadataKeys.SagaDataType, sagaData.GetType().GetSimpleAssemblyQualifiedName()}, {SagaAuditingMetadataKeys.SagaHandlerType, handler.GetType().GetSimpleAssemblyQualifiedName()}, {SagaAuditingMetadataKeys.MessageType, message.GetMessageType()}, {SagaAuditingMetadataKeys.MessageId, message.GetMessageId()}, {SagaAuditingMetadataKeys.MachineName, Environment.MachineName}, }; }
public async Task Save(ISagaData sagaData, Dictionary<string, string> sagaAuditMetadata) { var logData = new { Data = sagaData, Metadata = sagaAuditMetadata }; var jsonText = JsonConvert.SerializeObject(logData, Formatting.None); _log.Info(jsonText); }
internal void AddInstance(ISagaData sagaData) { lock (_lock) { var instance = Clone(sagaData); if (instance.Id == Guid.Empty) { instance.Id = Guid.NewGuid(); } _data[instance.Id] = instance; } }
public async Task Delete(ISagaData sagaData) { var collection = GetCollection(sagaData.GetType()); var result = await collection.DeleteManyAsync(new BsonDocument("_id", sagaData.Id)).ConfigureAwait(false); if (result.DeletedCount != 1) { throw new ConcurrencyException("Saga data {0} with ID {1} in collection {2} could not be deleted", sagaData.GetType(), sagaData.Id, collection.CollectionNamespace); } }
/// <summary> /// Archives the given saga data under its current ID and revision /// </summary> public async Task Save(ISagaData sagaData, Dictionary<string, string> sagaAuditMetadata) { var dataRef = $"{sagaData.Id:N}/{sagaData.Revision:0000000000}/data.json"; var metaDataRef = $"{sagaData.Id:N}/{sagaData.Revision:0000000000}/metadata.json"; var dataBlob = _container.GetBlockBlobReference(dataRef); var metaDataBlob = _container.GetBlockBlobReference(metaDataRef); dataBlob.Properties.ContentType = "application/json"; metaDataBlob.Properties.ContentType = "application/json"; await dataBlob.UploadTextAsync(JsonConvert.SerializeObject(sagaData, DataSettings), TextEncoding, DefaultAccessCondition, DefaultRequestOptions, DefaultOperationContext); await metaDataBlob.UploadTextAsync(JsonConvert.SerializeObject(sagaAuditMetadata, MetadataSettings), TextEncoding, DefaultAccessCondition, DefaultRequestOptions, DefaultOperationContext); await dataBlob.SetPropertiesAsync(); await metaDataBlob.SetPropertiesAsync(); }
public void Delete(ISagaData sagaData) { var collection = database.GetCollection(collectionName); if (Transaction.Current != null) { var hack = new AmbientTxHack(() => collection.Remove(Query.EQ("_id", sagaData.Id))); Transaction.Current.EnlistVolatile(hack, EnlistmentOptions.None); } else { collection.Remove(Query.EQ("_id", sagaData.Id)); } }
public void Delete(ISagaData sagaData) { using (var connection = new SqlConnection(connectionString)) { connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = @"delete from sagas where id = @id; delete from saga_index where saga_id = @id"; command.Parameters.AddWithValue("id", sagaData.Id); command.ExecuteNonQuery(); } } }
public void Delete(ISagaData sagaData) { var collection = database.GetCollection(collectionName); var query = Query.And(Query.EQ("_id", sagaData.Id), Query.EQ("_rev", sagaData.Revision)); var safeModeResult = collection.Remove(query, SafeMode.True); EnsureResultIsGood(safeModeResult, "delete saga data of type {0} with _id {1} and _rev {2}", sagaData.GetType(), sagaData.Id, sagaData.Revision); }
public async Task Delete(ISagaData sagaData) { var collection = GetCollection(sagaData.GetType()); var result = collection.Remove(Query.EQ("_id", sagaData.Id)); try { CheckResult(result, 1); } catch (Exception exception) { throw new ConcurrencyException(exception, "Saga data {0} with ID {1} in collection {2} could not be deleted", sagaData.GetType(), sagaData.Id, collection.Name); } }
public async Task Insert(ISagaData sagaData, IEnumerable<ISagaCorrelationProperty> correlationProperties) { if (sagaData.Id == Guid.Empty) { throw new InvalidOperationException($"Attempted to insert saga data {sagaData.GetType()} without an ID"); } if (sagaData.Revision != 0) { throw new InvalidOperationException($"Attempted to insert saga data with ID {sagaData.Id} and revision {sagaData.Revision}, but revision must be 0 on first insert!"); } var collection = GetCollection(sagaData.GetType()); await collection.InsertOneAsync(sagaData.ToBsonDocument()).ConfigureAwait(false); }
public async Task Update(ISagaData sagaData, IEnumerable<ISagaCorrelationProperty> correlationProperties) { var collection = GetCollection(sagaData.GetType()); var criteria = Builders<BsonDocument>.Filter.And(Builders<BsonDocument>.Filter.Eq("_id", sagaData.Id), Builders<BsonDocument>.Filter.Eq("Revision", sagaData.Revision)); sagaData.Revision++; var result = await collection.ReplaceOneAsync(criteria, sagaData.ToBsonDocument(sagaData.GetType())).ConfigureAwait(false); if (!result.IsModifiedCountAvailable || result.ModifiedCount != 1) { throw new ConcurrencyException("Saga data {0} with ID {1} in collection {2} could not be updated!", sagaData.GetType(), sagaData.Id, collection.CollectionNamespace); } }
public async Task Update(ISagaData sagaData, IEnumerable<ISagaCorrelationProperty> correlationProperties) { using (var session = _documentStore.OpenAsyncSession()) { var documentId = SagaDataDocument.GetIdFromGuid(sagaData.Id); var existingSagaData = await session.LoadAsync<SagaDataDocument>(documentId); sagaData.Revision++; existingSagaData.SagaData = sagaData; await DeleteCorrelationPropertyDataForSaga(existingSagaData, session); //add the new saga correlation documents var correlationPropertyDocumentIds = await SaveCorrelationProperties(session, sagaData, correlationProperties, existingSagaData.Id); existingSagaData.SagaCorrelationPropertyDocumentIds = correlationPropertyDocumentIds; await session.SaveChangesAsync(); } }
/// <summary> /// Saves a snapshot of the saga data along with the given metadata /// </summary> public async Task Save(ISagaData sagaData, Dictionary<string, string> sagaAuditMetadata) { var jsonText = JsonConvert.SerializeObject(new Snapshot { Data = sagaData, Metadata = sagaAuditMetadata }, Formatting.Indented); var snapshotFilePath = Path.Combine(_snapshotDirectory, string.Format("{0:N}-{1}.json", sagaData.Id, sagaData.Revision)); using (var file = File.OpenWrite(snapshotFilePath)) { using (var writer = new StreamWriter(file, Encoding.UTF8)) { await writer.WriteAsync(jsonText); } } }
public async Task Insert(ISagaData sagaData, IEnumerable<ISagaCorrelationProperty> correlationProperties) { if (sagaData.Id == Guid.Empty) { throw new InvalidOperationException($"Saga data {sagaData.GetType()} has an uninitialized Id property!"); } using (var session = _documentStore.OpenAsyncSession()) { var sagaDataDocumentId = SagaDataDocument.GetIdFromGuid(sagaData.Id); var sagaDataDocument = new SagaDataDocument(sagaData); await session.StoreAsync(sagaDataDocument, sagaDataDocumentId); var correlationPropertyDocumentIds = await SaveCorrelationProperties(session, sagaData, correlationProperties, sagaDataDocumentId); sagaDataDocument.SagaCorrelationPropertyDocumentIds = correlationPropertyDocumentIds; await session.SaveChangesAsync(); } }
/// <summary> /// Inserts the given saga data instance into the index file /// </summary> public async Task Insert(ISagaData sagaData, IEnumerable<ISagaCorrelationProperty> correlationProperties) { using (new FilesystemExclusiveLock(_lockFile, _log)) { var index = new FilesystemSagaIndex(_basePath); var id = GetId(sagaData); if (sagaData.Revision != 0) { throw new InvalidOperationException($"Attempted to insert saga data with ID {id} and revision {sagaData.Revision}, but revision must be 0 on first insert!"); } var existingSaga = index.FindById(id); if (existingSaga != null) { throw new ConcurrencyException("Saga data with ID {0} already exists!", id); } index.Insert(sagaData, correlationProperties); } }
/// <summary> /// Updates the given saga data instance in the index file /// </summary> public async Task Update(ISagaData sagaData, IEnumerable<ISagaCorrelationProperty> correlationProperties) { using (new FilesystemExclusiveLock(_lockFile, _log)) { var index = new FilesystemSagaIndex(_basePath); var id = GetId(sagaData); var existingCopy = index.FindById(id); if (existingCopy == null) { throw new ConcurrencyException("Saga data with ID {0} does not exist!", id); } if (existingCopy.Revision != sagaData.Revision) { throw new ConcurrencyException("Attempted to update saga data with ID {0} with revision {1}, but the existing data was updated to revision {2}", id, sagaData.Revision, existingCopy.Revision); } sagaData.Revision++; index.Insert(sagaData, correlationProperties); } }
/// <summary> /// Saves the given saga data, throwing an exception if the instance already exists /// </summary> public async Task Insert(ISagaData sagaData, IEnumerable<ISagaCorrelationProperty> correlationProperties) { var id = GetId(sagaData); lock (_lock) { if (_data.ContainsKey(id)) { throw new ConcurrencyException("Saga data with ID {0} already exists!", id); } if (sagaData.Revision != 0) { throw new InvalidOperationException(string.Format("Attempted to insert saga data with ID {0} and revision {1}, but revision must be 0 on first insert!", id, sagaData.Revision)); } _data[id] = Clone(sagaData); } }
public async Task Insert(ISagaData sagaData, IEnumerable<ISagaCorrelationProperty> correlationProperties) { if (sagaData.Id == Guid.Empty) { throw new InvalidOperationException(string.Format("Attempted to insert saga data {0} without an ID", sagaData.GetType())); } var collection = GetCollection(sagaData.GetType()); var result = collection.Insert(sagaData); try { CheckResult(result,0); } catch (Exception exception) { throw new ConcurrencyException(exception, "Saga data {0} with ID {1} in collection {2} could not be inserted!", sagaData.GetType(), sagaData.Id, collection.Name); } }
/// <summary> /// Saves a snapshot of the saga data along with the given metadata /// </summary> public async Task Save(ISagaData sagaData, Dictionary <string, string> sagaAuditMetadata) { using (var connection = await _connectionProvider.GetConnection()) { using (var command = connection.CreateCommand()) { command.CommandText = $@" INSERT INTO {_tableName.QualifiedName} ( [id], [revision], [data], [metadata] ) VALUES ( @id, @revision, @data, @metadata ) "; var dataString = DataSerializer.SerializeToString(sagaData); var metadataString = MetadataSerializer.SerializeToString(sagaAuditMetadata); command.Parameters.Add("id", SqlDbType.UniqueIdentifier).Value = sagaData.Id; command.Parameters.Add("revision", SqlDbType.Int).Value = sagaData.Revision; command.Parameters.Add("data", SqlDbType.NVarChar, MathUtil.GetNextPowerOfTwo(dataString.Length)).Value = dataString; command.Parameters.Add("metadata", SqlDbType.NVarChar, MathUtil.GetNextPowerOfTwo(metadataString.Length)).Value = metadataString; Console.WriteLine($"OK WE'RE SAVING SAGA SNAPSHOT {sagaData.Id} rev. {sagaData.Revision} NOW"); await command.ExecuteNonQueryAsync().ConfigureAwait(false); } await connection.Complete(); } }
// ReSharper restore UnusedMember.Local void PerformSaveActions(Saga saga, ISagaData sagaData) { if (saga.Complete) { if (!saga.IsNew) { storeSagaData.Delete(sagaData); } return; } var sagaDataPropertyPathsToIndex = GetSagaDataPropertyPathsToIndex(saga).Distinct().ToArray(); if (!saga.IsNew) { storeSagaData.Update(sagaData, sagaDataPropertyPathsToIndex); } else { storeSagaData.Insert(sagaData, sagaDataPropertyPathsToIndex); } }
public async Task Update(ISagaData sagaData, IEnumerable<ISagaCorrelationProperty> correlationProperties) { var collection = GetCollection(sagaData.GetType()); var criteria = Query.And( Query.EQ("_id", sagaData.Id), Query.EQ("Revision", sagaData.Revision)); sagaData.Revision++; var result = collection.Update(criteria, MongoDB.Driver.Builders.Update.Replace(sagaData)); try { CheckResult(result, 1); } catch (Exception exception) { throw new ConcurrencyException(exception, "Saga data {0} with ID {1} in collection {2} could not be updated!", sagaData.GetType(), sagaData.Id, collection.Name); } }
public void Save(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex) { var collection = database.GetCollection(collectionName); if (!indexCreated) { foreach (var propertyToIndex in sagaDataPropertyPathsToIndex) { collection.EnsureIndex(IndexKeys.Ascending(propertyToIndex), IndexOptions.SetBackground(false)); } indexCreated = true; } var criteria = Query.And(Query.EQ("_id", sagaData.Id), Query.EQ("_rev", sagaData.Revision)); sagaData.Revision++; var update = Update.Replace(sagaData); SafeModeResult safeModeResult; try { safeModeResult = collection.Update(criteria, update, UpdateFlags.Upsert, SafeMode.True); } catch (MongoSafeModeException) { // in case of race conditions, we get a duplicate key error because the upsert // cannot proceed to insert a document with the same _id as an existing document // ... therefore, we map the MongoSafeModeException to our own OptimisticLockingException throw new OptimisticLockingException(sagaData); } EnsureResultIsGood(safeModeResult, "save saga data of type {0} with _id {1} and _rev {2}", sagaData.GetType(), sagaData.Id, sagaData.Revision); }
public void Delete(ISagaData sagaData) { var connection = getConnection(); try { using (var command = connection.CreateCommand()) { const string updateSagaSql = @"DELETE FROM ""{0}"" WHERE ""id"" = @id AND ""revision"" = @current_revision;"; command.CommandText = string.Format(updateSagaSql, sagaTableName); command.Parameters.AddWithValue("id", sagaData.Id); command.Parameters.AddWithValue("current_revision", sagaData.Revision); var rows = command.ExecuteNonQuery(); if (rows == 0) { throw new OptimisticLockingException(sagaData); } } using (var command = connection.CreateCommand()) { const string deleteSagaIndexSql = @"DELETE FROM ""{0}"" WHERE ""saga_id"" = @id"; command.CommandText = string.Format(deleteSagaIndexSql, sagaIndexTableName); command.Parameters.AddWithValue("id", sagaData.Id); command.ExecuteNonQuery(); } commitAction(connection); } finally { releaseConnection(connection); } }
void VerifyCorrelationPropertyUniqueness(ISagaData sagaData, IEnumerable <ISagaCorrelationProperty> correlationProperties) { foreach (var property in correlationProperties) { var valueFromSagaData = Reflect.Value(sagaData, property.PropertyName); foreach (var existingSagaData in _data.Values) { if (existingSagaData.Id == sagaData.Id) { continue; } var valueFromExistingInstance = Reflect.Value(existingSagaData, property.PropertyName); if (Equals(valueFromSagaData, valueFromExistingInstance)) { throw new ConcurrencyException("Correlation property '{0}' has value '{1}' in existing saga data with ID {2}", property.PropertyName, valueFromExistingInstance, existingSagaData.Id); } } } }
/// <summary> /// Saves the given saga data, throwing an exception if the instance already exists /// </summary> public async Task Insert(ISagaData sagaData, IEnumerable <ISagaCorrelationProperty> correlationProperties) { var id = GetId(sagaData); lock (_lock) { if (_data.ContainsKey(id)) { throw new ConcurrencyException($"Saga data with ID {id} already exists!"); } VerifyCorrelationPropertyUniqueness(sagaData, correlationProperties); if (sagaData.Revision != 0) { throw new InvalidOperationException($"Attempted to insert saga data with ID {id} and revision {sagaData.Revision}, but revision must be 0 on first insert!"); } var clone = Clone(sagaData); _data[id] = clone; Created?.Invoke(clone); } }
static async Task ClearSagaIndex(ISagaData sagaData, CloudTable table) { var partitionKey = sagaData.GetType().Name; var op = TableOperation.Retrieve <DynamicTableEntity>(partitionKey, $"{sagaData.Id:N}_{sagaData.Revision:0000000000}"); var operationContext = new OperationContext(); var tableRequestOptions = new TableRequestOptions { RetryPolicy = new ExponentialRetry() }; var res = await table.ExecuteAsync(op, tableRequestOptions, operationContext); if (res != null && res.Result != null) { var index = (DynamicTableEntity)res.Result; var entries = GetIndicies(index, partitionKey); foreach (var e in entries) { await table.ExecuteAsync(TableOperation.Delete(e), tableRequestOptions, operationContext); } await table.ExecuteAsync(TableOperation.Delete(index), tableRequestOptions, operationContext); } }
/// <summary> /// Saves the given saga data, throwing an exception if the instance already exists /// </summary> public async Task Insert(ISagaData sagaData, IEnumerable<ISagaCorrelationProperty> correlationProperties) { var id = GetId(sagaData); lock (_lock) { if (_data.ContainsKey(id)) { throw new ConcurrencyException("Saga data with ID {0} already exists!", id); } VerifyCorrelationPropertyUniqueness(sagaData, correlationProperties); if (sagaData.Revision != 0) { throw new InvalidOperationException($"Attempted to insert saga data with ID {id} and revision {sagaData.Revision}, but revision must be 0 on first insert!"); } var clone = Clone(sagaData); _data[id] = clone; Created?.Invoke(clone); } }
private async Task <IEnumerable <string> > SaveCorrelationProperties(IAsyncDocumentSession session, ISagaData sagaData, IEnumerable <ISagaCorrelationProperty> correlationProperties, string sagaDataDocumentId) { var documentIds = new List <string>(); foreach (var correlationProperty in correlationProperties) { var propertyName = correlationProperty.PropertyName; var value = sagaData.GetType().GetProperty(propertyName).GetValue(sagaData).ToString(); var documentId = SagaCorrelationPropertyDocument.GetIdForCorrelationProperty(correlationProperty.SagaDataType, propertyName, value); var existingSagaCorrelationPropertyDocument = await session.LoadAsync <SagaCorrelationPropertyDocument>(documentId); if (existingSagaCorrelationPropertyDocument != null) { if (existingSagaCorrelationPropertyDocument.SagaDataDocumentId != sagaDataDocumentId) { throw new ConcurrencyException( $"Could not save correlation properties. The following correlation property already exists with the same value for another saga: {propertyName} - {value}"); } } else { var sagaCorrelationPropertyDocument = new SagaCorrelationPropertyDocument(correlationProperty.SagaDataType, propertyName, value, sagaDataDocumentId); await session.StoreAsync(sagaCorrelationPropertyDocument, documentId); } documentIds.Add(documentId); } return(documentIds); }
/// <summary> /// Archives the given saga data in MySql under its current ID and revision /// </summary> public async Task Save(ISagaData sagaData, Dictionary <string, string> sagaAuditMetadata) { using (var connection = await _connectionHelper.GetConnection()) { using (var command = connection.CreateCommand()) { command.CommandText = $@" INSERT INTO `{_tableName}` (`id`, `revision`, `data`, `metadata`) VALUES (@id, @revision, @data, @metadata); "; command.Parameters.Add(command.CreateParameter("id", DbType.Guid, sagaData.Id)); command.Parameters.Add(command.CreateParameter("revision", DbType.Int32, sagaData.Revision)); command.Parameters.Add(command.CreateParameter("data", DbType.Binary, _objectSerializer.Serialize(sagaData))); command.Parameters.Add(command.CreateParameter("metadata", DbType.String, _dictionarySerializer.SerializeToString(sagaAuditMetadata))); await command.ExecuteNonQueryAsync(); } connection.Complete(); } }
/// <summary> /// Saves the <paramref name="sagaData"/> snapshot and the accompanying <paramref name="sagaAuditMetadata"/> /// </summary> public async Task Save(ISagaData sagaData, Dictionary <string, string> sagaAuditMetadata) { using (var connection = _connectionHelper.GetConnection()) { using (var command = connection.CreateCommand()) { command.CommandText = $@" INSERT INTO {_tableName} (id, revision, data, metadata) VALUES (:id, :revision, :data, :metadata) "; command.Parameters.Add("id", OracleDbType.Raw).Value = sagaData.Id; command.Parameters.Add("revision", OracleDbType.Int64).Value = sagaData.Revision; command.Parameters.Add("data", OracleDbType.Blob).Value = _objectSerializer.Serialize(sagaData); command.Parameters.Add("metadata", OracleDbType.Clob).Value = _dictionarySerializer.SerializeToString(sagaAuditMetadata); await command.ExecuteNonQueryAsync(); } connection.Complete(); } }
public TSagaDataToFind Find <TSagaDataToFind>(string sagaDataPropertyPath, object fieldFromMessage) where TSagaDataToFind : class, ISagaData { var result = innerPersister.Find <TSagaDataToFind>(sagaDataPropertyPath, fieldFromMessage); if (result != null) { MostRecentlyLoadedSagaData = result; if (Correlated != null) { Correlated(result); } } else { MostRecentlyLoadedSagaData = null; if (CouldNotCorrelate != null) { CouldNotCorrelate(); } } return(result); }
public void Save(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex) { var collection = database.GetCollection(collectionName); if (!indexCreated) { foreach (var propertyToIndex in sagaDataPropertyPathsToIndex) { collection.EnsureIndex(IndexKeys.Ascending(propertyToIndex), IndexOptions.SetBackground(false)); } indexCreated = true; } // if an ambient TX is present, enlist the insert to be performed at commit time if (Transaction.Current != null) { var hack = new AmbientTxHack(() => collection.Save(sagaData, SafeMode.True)); Transaction.Current.EnlistVolatile(hack, EnlistmentOptions.None); } else { collection.Save(sagaData, SafeMode.True); } }
/// <summary> /// Inserts the saga data /// </summary> public async Task Insert(ISagaData sagaData, IEnumerable <ISagaCorrelationProperty> correlationProperties) { //TODO: Implement concurrency if (sagaData.Id == Guid.Empty) { throw new InvalidOperationException($"Saga data {sagaData.GetType()} has an uninitialized Id property!"); } if (sagaData.Revision != 0) { throw new InvalidOperationException($"Attempted to insert saga data with ID {sagaData.Id} and revision {sagaData.Revision}, but revision must be 0 on first insert!"); } var dataBlob = GetSagaDataBlob(sagaData.Id); //if (await dataBlob.ExistsAsync() && dataBlob.Metadata["revision"] != revisionToUpdate.ToString()) //{ // throw new ConcurrencyException("Update of saga with ID {0} did not succeed because someone else beat us to it", sagaData.Id); //} dataBlob.Properties.ContentType = "application/json"; dataBlob.Metadata[RevisionKey] = sagaData.Revision.ToString(); var jsonText = JsonConvert.SerializeObject(sagaData, Settings); await dataBlob.UploadTextAsync(jsonText, TextEncoding, DefaultAccessCondition, DefaultBlobRequestOptions, DefaultOperationContext); await dataBlob.SetPropertiesAsync(DefaultAccessCondition, new BlobRequestOptions { RetryPolicy = new ExponentialRetry() }, new OperationContext()); await dataBlob.SetMetadataAsync(AccessCondition.GenerateEmptyCondition(), new BlobRequestOptions { RetryPolicy = new ExponentialRetry() }, new OperationContext()); await InsertSagaCorrelationProperties(sagaData, correlationProperties); }
private void DeclareIndexUsingReturningClause(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary <string, object> propertiesToIndex) { var dialect = scope.Dialect; var connection = scope.Connection; var existingKeys = Enumerable.Empty <string>(); var sagaTypeName = GetSagaTypeName(sagaData.GetType()); var parameters = propertiesToIndex .Select((p, i) => new { PropertyName = p.Key, PropertyValue = p.Value, PropertyNameParameter = string.Format("n{0}", i), PropertyValueParameter = string.Format("v{0}", i), PropertyValuesParameter = string.Format("vs{0}", i) }) .ToList(); var tuples = parameters .Select(p => string.Format("({0}, {1}, {2}, {3})", dialect.EscapeParameter(SAGAINDEX_ID_COLUMN), dialect.EscapeParameter(p.PropertyNameParameter), dialect.EscapeParameter(p.PropertyValueParameter), dialect.EscapeParameter(p.PropertyValuesParameter) )); using (var command = connection.CreateCommand()) { command.CommandText = string.Format( "INSERT INTO {0} ({1}, {2}, {3}, {4}) VALUES {5} " + "ON CONFLICT ({1}, {2}) DO UPDATE SET {3} = excluded.{3}, {4} = excluded.{4} " + "RETURNING {2};", dialect.QuoteForTableName(sagaIndexTableName), //< 0 dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN), //< 1 dialect.QuoteForColumnName(SAGAINDEX_KEY_COLUMN), //< 2 dialect.QuoteForColumnName(SAGAINDEX_VALUE_COLUMN), //< 3 dialect.QuoteForColumnName(SAGAINDEX_VALUES_COLUMN), //< 4 string.Join(", ", tuples) //< 5 ); foreach (var parameter in parameters) { var value = GetIndexValue(parameter.PropertyValue); var values = value == null ? null : ArraysEnabledFor(dialect) ? (object)GetIndexValues(parameter.PropertyValue)?.ToArray() : GetConcatenatedIndexValues(GetIndexValues(parameter.PropertyValue)); var valuesDbType = ArraysEnabledFor(dialect) ? DbType.Object : DbType.String; command.AddParameter(dialect.EscapeParameter(parameter.PropertyNameParameter), DbType.String, parameter.PropertyName); command.AddParameter(dialect.EscapeParameter(parameter.PropertyValueParameter), DbType.String, value); command.AddParameter(dialect.EscapeParameter(parameter.PropertyValuesParameter), valuesDbType, values); } command.AddParameter(dialect.EscapeParameter(SAGAINDEX_ID_COLUMN), DbType.Guid, sagaData.Id); try { using (var reader = command.ExecuteReader()) { existingKeys = reader.AsEnumerable <string>(SAGAINDEX_KEY_COLUMN).ToArray(); } } catch (DbException exception) { throw new OptimisticLockingException(sagaData, exception); } } var idx = 0; using (var command = connection.CreateCommand()) { command.CommandText = string.Format( "DELETE FROM {0} " + "WHERE {1} = {2} AND {3} NOT IN ({4});", dialect.QuoteForTableName(sagaIndexTableName), //< 0 dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN), //< 1 dialect.EscapeParameter(SAGAINDEX_ID_COLUMN), //< 2 dialect.QuoteForColumnName(SAGAINDEX_KEY_COLUMN), //< 3 string.Join(", ", existingKeys.Select(k => dialect.EscapeParameter($"k{idx++}"))) ); for (int i = 0; i < existingKeys.Count(); i++) { command.AddParameter(dialect.EscapeParameter($"k{i}"), DbType.StringFixedLength, existingKeys.ElementAt(i).Trim()); } command.AddParameter(dialect.EscapeParameter(SAGAINDEX_ID_COLUMN), DbType.Guid, sagaData.Id); try { command.ExecuteNonQuery(); } catch (DbException exception) { throw new OptimisticLockingException(sagaData, exception); } } }
/// <summary> /// Serializes the given ISagaData object into a string /// </summary> /// <param name="obj"></param> /// <returns></returns> public string SerializeToString(ISagaData obj) { return(base.SerializeToString(obj)); }
private void DeclareIndexUsingTableExpressions(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary <string, object> propertiesToIndex) { var dialect = scope.Dialect; var connection = scope.Connection; var sagaTypeName = GetSagaTypeName(sagaData.GetType()); var parameters = propertiesToIndex .Select((p, i) => new { PropertyName = p.Key, PropertyValue = p.Value, PropertyNameParameter = string.Format("n{0}", i), PropertyValueParameter = string.Format("v{0}", i), PropertyValuesParameter = string.Format("vs{0}", i) }) .ToList(); var tuples = parameters .Select(p => string.Format("({0}, {1}, {2}, {3})", dialect.EscapeParameter(SAGAINDEX_ID_COLUMN), dialect.EscapeParameter(p.PropertyNameParameter), dialect.EscapeParameter(p.PropertyValueParameter), dialect.EscapeParameter(p.PropertyValuesParameter) )); using (var command = connection.CreateCommand()) { command.CommandText = string.Format( "WITH existing AS (" + "INSERT INTO {0} ({1}, {2}, {3}, {4}) VALUES {6} " + "ON CONFLICT ({1}, {2}) DO UPDATE SET {3} = excluded.{3}, {4} = excluded.{4} " + "RETURNING {2}) " + "DELETE FROM {0} " + "WHERE {1} = {5} AND {2} NOT IN (SELECT {2} FROM existing);", dialect.QuoteForTableName(sagaIndexTableName), //< 0 dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN), //< 1 dialect.QuoteForColumnName(SAGAINDEX_KEY_COLUMN), //< 2 dialect.QuoteForColumnName(SAGAINDEX_VALUE_COLUMN), //< 3 dialect.QuoteForColumnName(SAGAINDEX_VALUES_COLUMN), //< 4 dialect.EscapeParameter(SAGAINDEX_ID_COLUMN), //< 5 string.Join(", ", tuples) //< 6 ); foreach (var parameter in parameters) { var value = GetIndexValue(parameter.PropertyValue); command.AddParameter(dialect.EscapeParameter(parameter.PropertyNameParameter), DbType.String, parameter.PropertyName); command.AddParameter(dialect.EscapeParameter(parameter.PropertyValueParameter), DbType.String, value); var values = ArraysEnabledFor(dialect) ? (object)GetIndexValues(parameter.PropertyValue)?.ToArray() : GetConcatenatedIndexValues(GetIndexValues(parameter.PropertyValue)); var dbtype = ArraysEnabledFor(dialect) ? DbType.Object : DbType.String; command.AddParameter(dialect.EscapeParameter(parameter.PropertyValuesParameter), dbtype, values); } command.AddParameter(dialect.EscapeParameter(SAGAINDEX_ID_COLUMN), DbType.Guid, sagaData.Id); try { command.ExecuteNonQuery(); } catch (DbException exception) { throw new OptimisticLockingException(sagaData, exception); } } }
/// <summary> /// Stores the specified data. /// </summary> /// <param name="data">The <see cref="ISagaData">data</see> to store.</param> /// <param name="correlationProperty">The property used to correlate the stored data.</param> /// <param name="cancellationToken">The <see cref="CancellationToken">token</see> that can be used to cancel the operation.</param> /// <returns>A <see cref="Task">task</see> representing the asynchronous operation.</returns> public virtual Task Store(ISagaData data, CorrelationProperty correlationProperty, CancellationToken cancellationToken) { Arg.NotNull(data, nameof(data)); storage.AddOrUpdate(data.Id, data, (key, current) => data); return(CompletedTask); }
ISagaData Clone(ISagaData sagaData) { var serializedObject = JsonConvert.SerializeObject(sagaData, _serializerSettings); return(JsonConvert.DeserializeObject <ISagaData>(serializedObject, _serializerSettings)); }
public Task Store(ISagaData data, CorrelationProperty correlationProperty, CancellationToken cancellationToken) => throw new NotSupportedException(SR.NoConfiguredSagaStorage);
public async Task Update(ISagaData sagaData, IEnumerable <ISagaCorrelationProperty> correlationProperties) { await _innerSagaStorage.Update(sagaData, correlationProperties); _persistentSagaData[sagaData.Id] = sagaData; }
public async Task Delete(ISagaData sagaData) { await Task.Delay(_delayMilliseconds); await _sagaStorage.Delete(sagaData); }
public SagaDataDocument(ISagaData sagaData) { SagaData = sagaData; }
/// <summary> /// Adds to the invoker a piece of saga data that has been determined to be relevant for the invocation /// </summary> public abstract void SetSagaData(ISagaData sagaData);
public async Task Update(ISagaData sagaData, IEnumerable <ISagaCorrelationProperty> correlationProperties) { await Task.Delay(_delayMilliseconds); await _sagaStorage.Update(sagaData, correlationProperties); }
public void Update(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex) { innerPersister.Update(sagaData, sagaDataPropertyPathsToIndex); }
void ISagaInstance.AttachNew(ISagaData data) => AttachNew((TData)data);
void ISagaInstance.AttachExisting(ISagaData data) => AttachExisting((TData)data);
private void DeclareIndexUnoptimized(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary <string, object> propertiesToIndex) { var connection = scope.Connection; var dialect = scope.Dialect; var sagaTypeName = GetSagaTypeName(sagaData.GetType()); var idxTbl = dialect.QuoteForTableName(sagaIndexTableName); var idCol = dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN); var keyCol = dialect.QuoteForColumnName(SAGAINDEX_KEY_COLUMN); var valueCol = dialect.QuoteForColumnName(SAGAINDEX_VALUE_COLUMN); var valuesCol = dialect.QuoteForColumnName(SAGAINDEX_VALUE_COLUMN); var idParam = dialect.EscapeParameter(SAGAINDEX_ID_COLUMN); var existingKeys = Enumerable.Empty <string>(); // Let's fetch existing keys.. using (var command = connection.CreateCommand()) { command.CommandText = string.Format( "SELECT {1} FROM {0} WHERE {2} = {3};", dialect.QuoteForTableName(sagaIndexTableName), //< 0 dialect.QuoteForColumnName(SAGAINDEX_KEY_COLUMN), //< 1 dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN), //< 2 dialect.EscapeParameter(SAGAINDEX_ID_COLUMN) //< 3 ); command.AddParameter(dialect.EscapeParameter(SAGAINDEX_ID_COLUMN), DbType.Guid, sagaData.Id); try { using (var reader = command.ExecuteReader()) { existingKeys = reader.AsEnumerable <string>(SAGAINDEX_KEY_COLUMN).ToArray(); } } catch (DbException exception) { throw new OptimisticLockingException(sagaData, exception); } } // For each exisring key, update it's value.. foreach (var key in existingKeys.Where(k => propertiesToIndex.Any(p => p.Key == k))) { using (var command = connection.CreateCommand()) { command.CommandText = string.Format( "UPDATE {0} SET {1} = {2}, {3} = {4} " + "WHERE {5} = {6} AND {7} = {8};", dialect.QuoteForTableName(sagaIndexTableName), //< 0 dialect.QuoteForColumnName(SAGAINDEX_VALUE_COLUMN), //< 1 dialect.EscapeParameter(SAGAINDEX_VALUE_COLUMN), //< 2 dialect.QuoteForColumnName(SAGAINDEX_VALUES_COLUMN), //< 3 dialect.EscapeParameter(SAGAINDEX_VALUES_COLUMN), //< 4 dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN), //< 5 dialect.EscapeParameter(SAGAINDEX_ID_COLUMN), //< 6 dialect.QuoteForColumnName(SAGAINDEX_KEY_COLUMN), //< 7 dialect.EscapeParameter(SAGAINDEX_KEY_COLUMN) //< 8 ); var value = GetIndexValue(propertiesToIndex[key]); var values = ArraysEnabledFor(dialect) ? (object)GetIndexValues(propertiesToIndex[key])?.ToArray() : GetConcatenatedIndexValues(GetIndexValues(propertiesToIndex[key])); var valuesDbType = ArraysEnabledFor(dialect) ? DbType.Object : DbType.String; command.AddParameter(dialect.EscapeParameter(SAGAINDEX_ID_COLUMN), DbType.Guid, sagaData.Id); command.AddParameter(dialect.EscapeParameter(SAGAINDEX_KEY_COLUMN), DbType.String, key); command.AddParameter(dialect.EscapeParameter(SAGAINDEX_VALUE_COLUMN), DbType.String, value); command.AddParameter(dialect.EscapeParameter(SAGAINDEX_VALUES_COLUMN), valuesDbType, values); try { command.ExecuteNonQuery(); } catch (DbException exception) { throw new OptimisticLockingException(sagaData, exception); } } } var removedKeys = existingKeys.Where(x => !propertiesToIndex.ContainsKey(x)).ToArray(); if (removedKeys.Length > 0) { // Remove no longer needed keys.. using (var command = connection.CreateCommand()) { command.CommandText = string.Format( "DELETE FROM {0} WHERE {1} = {2} AND {3} IN ({4});", dialect.QuoteForTableName(sagaIndexTableName), //< 0 dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN), //< 1 dialect.EscapeParameter(SAGAINDEX_ID_COLUMN), //< 2 dialect.QuoteForColumnName(SAGAINDEX_KEY_COLUMN), //< 3 string.Join(", ", existingKeys.Select((x, i) => dialect.EscapeParameter($"k{i}"))) ); command.AddParameter(dialect.EscapeParameter(SAGAINDEX_ID_COLUMN), DbType.Guid, sagaData.Id); for (int i = 0; i < existingKeys.Count(); i++) { command.AddParameter(dialect.EscapeParameter($"k{i}"), DbType.StringFixedLength, existingKeys.ElementAt(i).Trim()); } try { command.ExecuteNonQuery(); } catch (DbException exception) { throw new OptimisticLockingException(sagaData, exception); } } } var parameters = propertiesToIndex .Where(x => !existingKeys.Contains(x.Key)) .Select((p, i) => new { PropertyName = p.Key, PropertyValue = p.Value, PropertyNameParameter = string.Format("n{0}", i), PropertyValueParameter = string.Format("v{0}", i), PropertyValuesParameter = string.Format("vs{0}", i) }) .ToList(); if (parameters.Count > 0) { // Insert new keys.. using (var command = connection.CreateCommand()) { var tuples = parameters.Select(p => string.Format("({0}, {1}, {2}, {3})", idParam, dialect.EscapeParameter(p.PropertyNameParameter), dialect.EscapeParameter(p.PropertyValueParameter), dialect.EscapeParameter(p.PropertyValuesParameter) )); command.CommandText = string.Format( "INSERT INTO {0} ({1}, {2}, {3}, {4}) VALUES {5};", dialect.QuoteForTableName(sagaIndexTableName), //< 0 dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN), //< 1 dialect.QuoteForColumnName(SAGAINDEX_KEY_COLUMN), //< 2 dialect.QuoteForColumnName(SAGAINDEX_VALUE_COLUMN), //< 3 dialect.QuoteForColumnName(SAGAINDEX_VALUES_COLUMN), //< 4 string.Join(", ", tuples) //< 5 ); foreach (var parameter in parameters) { var value = GetIndexValue(parameter.PropertyValue); var values = ArraysEnabledFor(dialect) ? (object)GetIndexValues(parameter.PropertyValue)?.ToArray() : GetConcatenatedIndexValues(GetIndexValues(parameter.PropertyValue)); var valuesDbType = ArraysEnabledFor(dialect) ? DbType.Object : DbType.String; command.AddParameter(dialect.EscapeParameter(parameter.PropertyNameParameter), DbType.String, parameter.PropertyName); command.AddParameter(dialect.EscapeParameter(parameter.PropertyValueParameter), DbType.String, value); command.AddParameter(dialect.EscapeParameter(parameter.PropertyValuesParameter), valuesDbType, values); } command.AddParameter(dialect.EscapeParameter(SAGAINDEX_ID_COLUMN), DbType.Guid, sagaData.Id); try { command.ExecuteNonQuery(); } catch (DbException exception) { throw new OptimisticLockingException(sagaData, exception); } } } }
/// <summary> /// Stores the specified saga data. /// </summary> /// <param name="data">The data to store.</param> /// <param name="correlationProperty">The property used to correlate the stored data.</param> /// <param name="cancellationToken">The <see cref="CancellationToken">token</see> that can be used to cancel the operation.</param> /// <returns>A <see cref="Task">task</see> repesenting the asynchronous operation.</returns> public Task Store(ISagaData data, CorrelationProperty correlationProperty, CancellationToken cancellationToken) { Arg.NotNull(data, nameof(data)); Arg.NotNull(correlationProperty, nameof(correlationProperty)); return(Configuration.Store(data, correlationProperty, cancellationToken)); }
public RelevantSagaInfo(ISagaData sagaData, IEnumerable<CorrelationProperty> correlationProperties, Saga saga) { SagaData = sagaData; // only keep necessary correlation properties, i.e. CorrelationProperties = correlationProperties .GroupBy(p => p.PropertyName) .Select(g => g.First()) .ToList(); Saga = saga; }
void CreateIndex(IEnumerable <KeyValuePair <string, string> > propertiesToIndex, ConnectionHolder connection, ISagaData sagaData) { var sagaTypeName = GetSagaTypeName(sagaData.GetType()); var parameters = propertiesToIndex .Select((p, i) => new { PropertyName = p.Key, PropertyValue = p.Value ?? "", PropertyNameParameter = string.Format("@n{0}", i), PropertyValueParameter = string.Format("@v{0}", i) }) .ToList(); // lastly, generate new index using (var command = connection.CreateCommand()) { // generate batch insert with SQL for each entry in the index var inserts = parameters .Select(a => string.Format( @" insert into [{0}] ([saga_type], [key], [value], [saga_id]) values (@saga_type, {1}, {2}, @saga_id)", sagaIndexTableName, a.PropertyNameParameter, a.PropertyValueParameter)) .ToList(); var sql = string.Join(";" + Environment.NewLine, inserts); command.CommandText = sql; foreach (var parameter in parameters) { command.Parameters.Add(parameter.PropertyNameParameter, SqlDbType.NVarChar).Value = parameter.PropertyName; command.Parameters.Add(parameter.PropertyValueParameter, SqlDbType.NVarChar).Value = parameter.PropertyValue; } command.Parameters.Add("saga_type", SqlDbType.NVarChar).Value = sagaTypeName; command.Parameters.Add("saga_id", SqlDbType.UniqueIdentifier).Value = sagaData.Id; try { command.ExecuteNonQuery(); } catch (SqlException sqlException) { if (sqlException.Number == SqlServerMagic.PrimaryKeyViolationNumber) { throw new OptimisticLockingException(sagaData, sqlException); } throw; } } }
public Task Complete(ISagaData data, CancellationToken cancellationToken) => throw new NotSupportedException(SR.NoConfiguredSagaStorage);
internal abstract Task InvokeConflictResolution(ISagaData otherSagaData);