From 382873ee5a8faa562068bee40fdab99fa856fcb7 Mon Sep 17 00:00:00 2001
From: Pablo Ruiz
Date: Fri, 1 Jul 2022 23:44:07 +0200
Subject: [PATCH 1/8] Temporary measure to (forcibly) disable seqscans.
---
.gitignore | 1 +
Rebus.AdoNet/AdoNetSagaPersister.cs | 15 ++++++++++++---
2 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/.gitignore b/.gitignore
index d9bebaf..1153753 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
*.suo
*.user
*.sln.docstates
+.idea/
# Build results
diff --git a/Rebus.AdoNet/AdoNetSagaPersister.cs b/Rebus.AdoNet/AdoNetSagaPersister.cs
index 7da4d45..c52487d 100644
--- a/Rebus.AdoNet/AdoNetSagaPersister.cs
+++ b/Rebus.AdoNet/AdoNetSagaPersister.cs
@@ -753,6 +753,7 @@ private string GetSagaLockingClause(SqlDialect dialect)
public TSagaData Find(string sagaDataPropertyPath, object fieldFromMessage) where TSagaData : class, ISagaData
{
+ log.Debug("Finding saga of type {0} with {1} = {2}", typeof(TSagaData).Name, sagaDataPropertyPath, fieldFromMessage);
using (var scope = manager.GetScope(autocomplete: true))
{
var dialect = scope.Dialect;
@@ -808,7 +809,7 @@ private string GetSagaLockingClause(SqlDialect dialect)
var valuesPredicate = ArraysEnabledFor(dialect)
? dialect.FormatArrayAny($"i.{indexValuesCol}", indexValuesParm)
: $"(i.{indexValuesCol} LIKE ('%' || {indexValuesParm} || '%'))";
-
+
command.CommandText = $@"
SELECT s.{dataCol}
FROM {sagaTblName} s
@@ -826,7 +827,7 @@ private string GetSagaLockingClause(SqlDialect dialect)
END
)
{forUpdate};".Replace("\t", "");
-
+
var value = GetIndexValue(fieldFromMessage);
var values = value == null ? null : ArraysEnabledFor(dialect)
? (object)(new[] { value })
@@ -843,7 +844,9 @@ private string GetSagaLockingClause(SqlDialect dialect)
try
{
+ if (dialect is PostgreSqlDialect) connection.ExecuteCommand("SET LOCAL enable_seqscan = off;");
data = (string)command.ExecuteScalar();
+ if (dialect is PostgreSqlDialect) connection.ExecuteCommand("SET LOCAL enable_seqscan = on;");
}
catch (DbException ex)
{
@@ -857,8 +860,14 @@ private string GetSagaLockingClause(SqlDialect dialect)
throw;
}
+
+ if (data == null)
+ {
+ log.Debug("No saga found of type {0} with {1} = {2}", typeof(TSagaData).Name, sagaDataPropertyPath, fieldFromMessage);
+ return null;
+ }
- if (data == null) return null;
+ log.Debug("Found saga of type {0} with {1} = {2}", typeof(TSagaData).Name, sagaDataPropertyPath, fieldFromMessage);
try
{
From 226cd4ae7da58ed4d8a485534fbdfd608e9243c6 Mon Sep 17 00:00:00 2001
From: Pablo Ruiz
Date: Wed, 6 Jul 2022 03:38:29 +0200
Subject: [PATCH 2/8] Implemented support for Json-based saga indexing, using a
single table in order to speed up lookups by avoiding joins.
---
Rebus.AdoNet/AdoNetExceptionManager.cs | 22 -
Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs | 1040 ++++++++++++++++++
Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs | 1040 ++++++++++++++++++
Rebus.AdoNet/Dialects/PostgreSql92Dialect.cs | 5 +
4 files changed, 2085 insertions(+), 22 deletions(-)
delete mode 100644 Rebus.AdoNet/AdoNetExceptionManager.cs
create mode 100644 Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs
create mode 100644 Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs
create mode 100644 Rebus.AdoNet/Dialects/PostgreSql92Dialect.cs
diff --git a/Rebus.AdoNet/AdoNetExceptionManager.cs b/Rebus.AdoNet/AdoNetExceptionManager.cs
deleted file mode 100644
index 8bdd987..0000000
--- a/Rebus.AdoNet/AdoNetExceptionManager.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System;
-using System.Data;
-using System.Data.Common;
-using System.Linq;
-
-namespace Rebus.AdoNet
-{
- // TODO: Make this configurable/extensible..
- // TODO: Provide a delegate the user can customize as to convert
- // exceptions into DBConcurrencyException, which should be
- // what AdoNet's code should try to catch.
- public class AdoNetExceptionManager
- {
- public static bool IsDuplicatedKeyException(Exception ex)
- {
- // FIXME: This is too dummy.
- return (ex is DbException);
- }
-
-
- }
-}
diff --git a/Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs b/Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs
new file mode 100644
index 0000000..0845d52
--- /dev/null
+++ b/Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs
@@ -0,0 +1,1040 @@
+using System;
+using System.Data;
+using System.Linq;
+using System.Data.Common;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Text.RegularExpressions;
+using Newtonsoft.Json;
+
+using Rebus.Logging;
+using Rebus.Serialization;
+using Rebus.Serialization.Json;
+using Rebus.AdoNet.Schema;
+using Rebus.AdoNet.Dialects;
+
+namespace Rebus.AdoNet
+{
+ ///
+ /// Implements a saga persister for Rebus that stores sagas using an AdoNet provider.
+ ///
+ public class AdoNetSagaPersisterEx : IStoreSagaData, AdoNetSagaPersisterFluentConfigurer, ICanUpdateMultipleSagaDatasAtomically
+ {
+ private const int MaximumSagaDataTypeNameLength = 80;
+ private const string SAGA_ID_COLUMN = "id";
+ private const string SAGA_TYPE_COLUMN = "saga_type";
+ private const string SAGA_DATA_COLUMN = "data";
+ private const string SAGA_REVISION_COLUMN = "revision";
+ //private const string SAGAINDEX_ID_COLUMN = "saga_id";
+ //private const string SAGAINDEX_KEY_COLUMN = "key";
+ //private const string SAGAINDEX_VALUE_COLUMN = "value";
+ private const string SAGAINDEX_VALUES_COLUMN = "values";
+ private static ILog log;
+ private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings {
+ TypeNameHandling = TypeNameHandling.All, // TODO: Make it configurable by adding a SagaTypeResolver feature.
+ DateFormatHandling = DateFormatHandling.IsoDateFormat, // TODO: Make it configurable..
+ Binder = new CustomSerializationBinder()
+ };
+
+ private readonly AdoNetUnitOfWorkManager manager;
+ private readonly string sagaIndexTableName;
+ private readonly string sagaTableName;
+ private readonly string idPropertyName;
+ private bool useSagaLocking;
+ private bool useSqlArrays;
+ private bool useNoWaitSagaLocking;
+ private bool indexNullProperties = true;
+ private Func sagaNameCustomizer;
+
+ static AdoNetSagaPersisterEx()
+ {
+ RebusLoggerFactory.Changed += f => log = f.GetCurrentClassLogger();
+ }
+
+ ///
+ /// Constructs the persister with the ability to create connections to database using the specified connection string.
+ /// This also means that the persister will manage the connection by itself, closing it when it has stopped using it.
+ ///
+ public AdoNetSagaPersisterEx(AdoNetUnitOfWorkManager manager, string sagaTableName, string sagaIndexTableName)
+ {
+ this.manager = manager;
+ this.sagaTableName = sagaTableName;
+ this.sagaIndexTableName = sagaIndexTableName;
+ this.idPropertyName = Reflect.Path(x => x.Id);
+ }
+
+ #region AdoNetSagaPersisterFluentConfigurer
+
+ ///
+ /// Configures the persister to ignore null-valued correlation properties and not add them to the saga index.
+ ///
+ public AdoNetSagaPersisterFluentConfigurer DoNotIndexNullProperties()
+ {
+ indexNullProperties = false;
+ return this;
+ }
+
+ public AdoNetSagaPersisterFluentConfigurer UseLockingOnSagaUpdates(bool waitForLocks)
+ {
+ useSagaLocking = true;
+ useNoWaitSagaLocking = !waitForLocks;
+ return this;
+ }
+
+ ///
+ /// Creates the necessary saga storage tables if they haven't already been created. If a table already exists
+ /// with a name that matches one of the desired table names, no action is performed (i.e. it is assumed that
+ /// the tables already exist).
+ ///
+ public AdoNetSagaPersisterFluentConfigurer EnsureTablesAreCreated()
+ {
+ using (var uow = manager.Create(autonomous: true))
+ using (var scope = (uow as AdoNetUnitOfWork).GetScope())
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var tableNames = scope.GetTableNames();
+
+ // bail out if there's already a table in the database with one of the names
+ var sagaTableIsAlreadyCreated = tableNames.Contains(sagaTableName, StringComparer.InvariantCultureIgnoreCase);
+ var sagaIndexTableIsAlreadyCreated = tableNames.Contains(sagaIndexTableName, StringComparer.OrdinalIgnoreCase);
+
+ if (sagaTableIsAlreadyCreated && sagaIndexTableIsAlreadyCreated)
+ {
+ log.Debug("Tables '{0}' and '{1}' already exists.", sagaTableName, sagaIndexTableName);
+ return this;
+ }
+
+ if (sagaTableIsAlreadyCreated || sagaIndexTableIsAlreadyCreated)
+ {
+ // if saga index is created, then saga table is not created and vice versa
+ throw new ApplicationException(string.Format("Table '{0}' do not exist - you have to create it manually",
+ sagaIndexTableIsAlreadyCreated ? sagaTableName : sagaIndexTableName));
+ }
+
+ if (useSqlArrays && !dialect.SupportsArrayTypes)
+ {
+ throw new ApplicationException("Enabled UseSqlArraysForCorrelationIndexes but underlaying database does not support arrays?!");
+ }
+
+ log.Info("Tables '{0}' and '{1}' do not exist - they will be created now", sagaTableName, sagaIndexTableName);
+
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = scope.Dialect.FormatCreateTable(
+ new AdoNetTable()
+ {
+ Name = sagaTableName,
+ Columns = new []
+ {
+ new AdoNetColumn() { Name = SAGA_ID_COLUMN, DbType = DbType.Guid },
+ new AdoNetColumn() { Name = SAGA_TYPE_COLUMN, DbType = DbType.String, Length = MaximumSagaDataTypeNameLength },
+ new AdoNetColumn() { Name = SAGA_REVISION_COLUMN, DbType = DbType.Int32 },
+ new AdoNetColumn() { Name = SAGA_DATA_COLUMN, DbType = DbType.String, Length = 1073741823 }
+ },
+ PrimaryKey = new[] { SAGA_ID_COLUMN },
+ Indexes = new []
+ {
+ new AdoNetIndex() { Name = $"ix_{sagaTableName}_{SAGA_ID_COLUMN}_{SAGA_TYPE_COLUMN}", Columns = new [] { SAGA_ID_COLUMN, SAGA_TYPE_COLUMN } },
+ }
+ }
+ );
+
+ command.ExecuteNonQuery();
+ }
+
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = scope.Dialect.FormatCreateTable(
+ new AdoNetTable()
+ {
+ Name = sagaIndexTableName,
+ Columns = new []
+ {
+ new AdoNetColumn() { Name = SAGAINDEX_ID_COLUMN, DbType = DbType.Guid },
+ new AdoNetColumn() { Name = SAGAINDEX_KEY_COLUMN, DbType = DbType.String, Length = 200 },
+ new AdoNetColumn() { Name = SAGAINDEX_VALUE_COLUMN, DbType = DbType.String, Length = 1073741823, Nullable = true },
+ new AdoNetColumn() { Name = SAGAINDEX_VALUES_COLUMN, DbType = DbType.String, Length = 1073741823, Nullable = true, Array = useSqlArrays }
+ },
+ PrimaryKey = new[] { SAGAINDEX_ID_COLUMN, SAGAINDEX_KEY_COLUMN },
+ Indexes = new []
+ {
+ new AdoNetIndex()
+ {
+ Name = $"ix_{sagaIndexTableName}_{SAGAINDEX_KEY_COLUMN}_{SAGAINDEX_VALUE_COLUMN}",
+ Columns = new[] { SAGAINDEX_KEY_COLUMN, SAGAINDEX_VALUE_COLUMN },
+ //Kind = "ybgin"
+ },
+ new AdoNetIndex()
+ {
+ Name = $"ix_{sagaIndexTableName}_{SAGAINDEX_KEY_COLUMN}_{SAGAINDEX_VALUES_COLUMN}",
+ Columns = new[] { SAGAINDEX_KEY_COLUMN, SAGAINDEX_VALUES_COLUMN },
+ //Kind = "ybgin"
+ }
+ }
+ }
+ );
+
+ command.ExecuteNonQuery();
+ }
+
+ scope.Complete();
+ log.Info("Tables '{0}' and '{1}' created", sagaTableName, sagaIndexTableName);
+ }
+
+ /*using (var scope = manager.GetScope(autonomous: true))
+ {
+ var tableNames = scope.GetTableNames();
+ if (!tableNames.Contains(sagaTableName, StringComparer.InvariantCultureIgnoreCase))
+ throw new InvalidOperationException("Tables not present?!");
+
+ }*/
+
+ return this;
+ }
+
+ ///
+ /// Customizes the saga names by invoking this customizer.
+ ///
+ /// The customizer.
+ ///
+ public AdoNetSagaPersisterFluentConfigurer CustomizeSagaNamesAs(Func customizer)
+ {
+ this.sagaNameCustomizer = customizer;
+ return this;
+ }
+
+ ///
+ /// Enables locking of sagas as to avoid two or more workers to update them concurrently.
+ ///
+ /// The saga locking.
+ public AdoNetSagaPersisterFluentConfigurer EnableSagaLocking()
+ {
+ useSagaLocking = true;
+ return this;
+ }
+
+ ///
+ /// Uses the use of sql array types for storing indexes related to correlation properties.
+ ///
+ /// The sql arrays.
+ public AdoNetSagaPersisterFluentConfigurer UseSqlArraysForCorrelationIndexes()
+ {
+ useSqlArrays = true;
+ return this;
+ }
+
+ ///
+ /// Allows customizing opened connections by passing a delegate/lambda to invoke for each new connection.
+ ///
+ ///
+ ///
+ public AdoNetSagaPersisterFluentConfigurer CustomizeOpenedConnections(Action customizer)
+ {
+ manager.ConnectionFactory.ConnectionCustomizer = customizer;
+ return this;
+ }
+
+ ///
+ /// Customizes type2name & name2type mapping logic used during serialization/deserialization.
+ ///
+ /// Delegate to invoke when resolving a name-to-type during deserialization.
+ /// Delegate to invoke when resolving a type-to-name during serialization.
+ ///
+ public AdoNetSagaPersisterFluentConfigurer CustomizeSerializationTypeResolving(Func nameToTypeResolver, Func typeToNameResolver)
+ {
+ (Settings.Binder as CustomSerializationBinder).NameToTypeResolver = nameToTypeResolver;
+ (Settings.Binder as CustomSerializationBinder).TypeToNameResolver = typeToNameResolver;
+ return this;
+ }
+
+ #endregion
+
+ public void Insert(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
+ {
+ using (var scope = manager.GetScope())
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var tableNames = scope.GetTableNames();
+ var sagaTypeName = GetSagaTypeName(sagaData.GetType());
+
+ // next insert the saga
+ using (var command = connection.CreateCommand())
+ {
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.AddParameter(dialect.EscapeParameter(SAGA_TYPE_COLUMN), sagaTypeName);
+ command.AddParameter(dialect.EscapeParameter(SAGA_REVISION_COLUMN), ++sagaData.Revision);
+ command.AddParameter(dialect.EscapeParameter(SAGA_DATA_COLUMN), JsonConvert.SerializeObject(sagaData, Formatting.Indented, Settings));
+
+ command.CommandText = string.Format(
+ @"insert into {0} ({1}, {2}, {3}, {4}) values ({5}, {6}, {7}, {8});",
+ dialect.QuoteForTableName(sagaTableName),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN),
+ dialect.QuoteForColumnName(SAGA_TYPE_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN),
+ dialect.QuoteForColumnName(SAGA_DATA_COLUMN),
+ dialect.EscapeParameter(SAGA_ID_COLUMN),
+ dialect.EscapeParameter(SAGA_TYPE_COLUMN),
+ dialect.EscapeParameter(SAGA_REVISION_COLUMN),
+ dialect.EscapeParameter(SAGA_DATA_COLUMN)
+ );
+
+ try
+ {
+ command.ExecuteNonQuery();
+ }
+ catch (DbException exception)
+ {
+ throw new OptimisticLockingException(sagaData, exception);
+ }
+ }
+
+ var propertiesToIndex = GetPropertiesToIndex(sagaData, sagaDataPropertyPathsToIndex);
+
+ if (propertiesToIndex.Any())
+ {
+ DeclareIndex(sagaData, scope, propertiesToIndex);
+ }
+
+ scope.Complete();
+ }
+ }
+
+ public void Update(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
+ {
+ using (var scope = manager.GetScope())
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var tableNames = scope.GetTableNames();
+
+ // next, update or insert the saga
+ using (var command = connection.CreateCommand())
+ {
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.AddParameter(dialect.EscapeParameter("current_revision"), sagaData.Revision);
+ command.AddParameter(dialect.EscapeParameter("next_revision"), ++sagaData.Revision);
+ command.AddParameter(dialect.EscapeParameter(SAGA_DATA_COLUMN), JsonConvert.SerializeObject(sagaData, Formatting.Indented, Settings));
+
+ command.CommandText = string.Format(
+ @"UPDATE {0} SET {1} = {2}, {3} = {4} " +
+ @"WHERE {5} = {6} AND {7} = {8};",
+ dialect.QuoteForTableName(sagaTableName),
+ dialect.QuoteForColumnName(SAGA_DATA_COLUMN), dialect.EscapeParameter(SAGA_DATA_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("next_revision"),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("current_revision")
+ );
+ var rows = command.ExecuteNonQuery();
+ if (rows == 0)
+ {
+ throw new OptimisticLockingException(sagaData);
+ }
+ }
+
+ var propertiesToIndex = GetPropertiesToIndex(sagaData, sagaDataPropertyPathsToIndex);
+
+ if (propertiesToIndex.Any())
+ {
+ DeclareIndex(sagaData, scope, propertiesToIndex);
+ }
+
+ scope.Complete();
+ }
+ }
+
+ private void DeclareIndexUsingTableExpressions(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary 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);
+ }
+ }
+ }
+
+ private void DeclareIndexUsingReturningClause(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary propertiesToIndex)
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var existingKeys = Enumerable.Empty();
+
+ 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(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);
+ }
+ }
+ }
+
+ private void DeclareIndexUnoptimized(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary 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();
+
+ // 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(SAGAINDEX_KEY_COLUMN).ToArray();
+ }
+ }
+ catch (DbException exception)
+ {
+ // FIXME: Ensure exception is the right one..
+ 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);
+ }
+
+ }
+ }
+ }
+
+ private void DeclareIndex(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary propertiesToIndex)
+ {
+ var dialect = scope.Dialect;
+
+ if (dialect.SupportsOnConflictClause && dialect.SupportsReturningClause && dialect.SupportsTableExpressions)
+ {
+ DeclareIndexUsingTableExpressions(sagaData, scope, propertiesToIndex);
+ }
+ else if (dialect.SupportsOnConflictClause && dialect.SupportsReturningClause)
+ {
+ DeclareIndexUsingReturningClause(sagaData, scope, propertiesToIndex);
+ }
+ else
+ {
+ DeclareIndexUnoptimized(sagaData, scope, propertiesToIndex);
+ }
+ }
+
+ public void Delete(ISagaData sagaData)
+ {
+ using (var scope = manager.GetScope())
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = string.Format(
+ @"DELETE FROM {0} WHERE {1} = {2} AND {3} = {4};",
+ dialect.QuoteForTableName(sagaTableName),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("current_revision")
+ );
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.AddParameter(dialect.EscapeParameter("current_revision"), sagaData.Revision);
+
+ var rows = command.ExecuteNonQuery();
+
+ if (rows == 0)
+ {
+ throw new OptimisticLockingException(sagaData);
+ }
+ }
+
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = string.Format(
+ @"DELETE FROM {0} WHERE {1} = {2};",
+ dialect.QuoteForTableName(sagaIndexTableName),
+ dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN)
+ );
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.ExecuteNonQuery();
+ }
+
+ scope.Complete();
+ }
+ }
+
+ private string GetSagaLockingClause(SqlDialect dialect)
+ {
+ if (useSagaLocking)
+ {
+ return useNoWaitSagaLocking
+ ? $"{dialect.SelectForUpdateClause} {dialect.SelectForNoWaitClause}"
+ : dialect.SelectForUpdateClause;
+ }
+
+ return string.Empty;
+ }
+
+ public TSagaData Find(string sagaDataPropertyPath, object fieldFromMessage) where TSagaData : class, ISagaData
+ {
+ using (var scope = manager.GetScope(autocomplete: true))
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var sagaType = GetSagaTypeName(typeof(TSagaData));
+
+ if (useSagaLocking)
+ {
+ if (!dialect.SupportsSelectForUpdate)
+ throw new InvalidOperationException($"You can't use saga locking for a Dialect {dialect.GetType()} that does not supports Select For Update.");
+
+ if (useNoWaitSagaLocking && !dialect.SupportsSelectForWithNoWait)
+ throw new InvalidOperationException($"You can't use saga locking with no-wait for a Dialect {dialect.GetType()} that does not supports no-wait clause.");
+ }
+
+ using (var command = connection.CreateCommand())
+ {
+ if (sagaDataPropertyPath == idPropertyName)
+ {
+ var id = (fieldFromMessage is Guid) ? (Guid)fieldFromMessage : Guid.Parse(fieldFromMessage.ToString());
+ var idParam = dialect.EscapeParameter("id");
+ var sagaTypeParam = dialect.EscapeParameter(SAGA_TYPE_COLUMN);
+
+ command.CommandText = string.Format(
+ @"SELECT s.{0} FROM {1} s WHERE s.{2} = {3} AND s.{4} = {5} {6}",
+ dialect.QuoteForColumnName(SAGA_DATA_COLUMN),
+ dialect.QuoteForTableName(sagaTableName),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN),
+ idParam,
+ dialect.QuoteForColumnName(SAGA_TYPE_COLUMN),
+ sagaTypeParam,
+ GetSagaLockingClause(dialect)
+ );
+ command.AddParameter(sagaTypeParam, sagaType);
+ command.AddParameter(idParam, id);
+ }
+ else
+ {
+ var dataCol = dialect.QuoteForColumnName(SAGA_DATA_COLUMN);
+ var sagaTblName = dialect.QuoteForTableName(sagaTableName);
+ var sagaTypeCol = dialect.QuoteForColumnName(SAGA_TYPE_COLUMN);
+ var sagaTypeParam = dialect.EscapeParameter(SAGA_TYPE_COLUMN);
+ var indexTblName = dialect.QuoteForTableName(sagaIndexTableName);
+ var sagaIdCol = dialect.QuoteForColumnName(SAGA_ID_COLUMN);
+ var indexIdCol = dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN);
+ var indexKeyCol = dialect.QuoteForColumnName(SAGAINDEX_KEY_COLUMN);
+ var indexKeyParam = dialect.EscapeParameter(SAGAINDEX_KEY_COLUMN);
+ var indexValueCol = dialect.QuoteForColumnName(SAGAINDEX_VALUE_COLUMN);
+ var indexValueParm = dialect.EscapeParameter(SAGAINDEX_VALUE_COLUMN);
+ var indexValuesCol = dialect.QuoteForColumnName(SAGAINDEX_VALUES_COLUMN);
+ var indexValuesParm = dialect.EscapeParameter(SAGAINDEX_VALUES_COLUMN);
+ var forUpdate = GetSagaLockingClause(dialect);
+ var valuesPredicate = ArraysEnabledFor(dialect)
+ ? dialect.FormatArrayAny($"i.{indexValuesCol}", indexValuesParm)
+ : $"(i.{indexValuesCol} LIKE ('%' || {indexValuesParm} || '%'))";
+
+ command.CommandText = $@"
+ SELECT s.{dataCol}
+ FROM {sagaTblName} s
+ JOIN {indexTblName} i on s.{sagaIdCol} = i.{indexIdCol}
+ WHERE s.{sagaTypeCol} = {sagaTypeParam}
+ AND i.{indexKeyCol} = {indexKeyParam}
+ AND (
+ CASE WHEN {indexValueParm} IS NULL THEN i.{indexValueCol} IS NULL
+ ELSE
+ (
+ i.{indexValueCol} = {indexValueParm}
+ OR
+ (i.{indexValuesCol} is NOT NULL AND {valuesPredicate})
+ )
+ END
+ )
+ {forUpdate};".Replace("\t", "");
+
+ var value = GetIndexValue(fieldFromMessage);
+ var values = value == null ? null : ArraysEnabledFor(dialect)
+ ? (object)(new[] { value })
+ : GetConcatenatedIndexValues(new[] { value });
+ var valuesDbType = ArraysEnabledFor(dialect) ? DbType.Object : DbType.String;
+
+ command.AddParameter(indexKeyParam, sagaDataPropertyPath);
+ command.AddParameter(sagaTypeParam, sagaType);
+ command.AddParameter(indexValueParm, DbType.String, value);
+ command.AddParameter(indexValuesParm, valuesDbType, values);
+ }
+
+ string data = null;
+
+ try
+ {
+ log.Debug("Finding saga of type {0} with {1} = {2}", sagaType, sagaDataPropertyPath, fieldFromMessage);
+ //if (dialect is PostgreSqlDialect) connection.ExecuteCommand("SET LOCAL enable_seqscan = off;");
+ data = (string)command.ExecuteScalar();
+ //if (dialect is PostgreSqlDialect) connection.ExecuteCommand("SET LOCAL enable_seqscan = on;");
+ }
+ catch (DbException ex)
+ {
+ // When in no-wait saga-locking mode, inspect
+ // exception and rethrow ex as SagaLockedException.
+ if (useSagaLocking && useNoWaitSagaLocking)
+ {
+ if (dialect.IsSelectForNoWaitLockingException(ex))
+ throw new AdoNetSagaLockedException(ex);
+ }
+
+ throw;
+ }
+
+ if (data == null)
+ {
+ log.Debug("No saga found of type {0} with {1} = {2}", sagaType, sagaDataPropertyPath, fieldFromMessage);
+ return null;
+ }
+
+ log.Debug("Found saga of type {0} with {1} = {2}", sagaType, sagaDataPropertyPath, fieldFromMessage);
+
+ try
+ {
+ return JsonConvert.DeserializeObject(data, Settings);
+ }
+ catch { }
+
+ try
+ {
+ return (TSagaData)JsonConvert.DeserializeObject(data, Settings);
+ }
+ catch (Exception exception)
+ {
+ var message = string.Format("An error occurred while attempting to deserialize '{0}' into a {1}", data, typeof(TSagaData));
+
+ throw new ApplicationException(message, exception);
+ }
+ }
+ }
+ }
+
+ private bool ArraysEnabledFor(SqlDialect dialect)
+ {
+ return useSqlArrays && dialect.SupportsArrayTypes;
+ }
+
+ private bool ShouldIndexValue(object value)
+ {
+ if (indexNullProperties)
+ return true;
+
+ if (value == null) return false;
+ if (value is string) return true;
+ if ((value is IEnumerable) && !(value as IEnumerable).Cast().Any()) return false;
+
+ return true;
+ }
+
+ private IDictionary GetPropertiesToIndex(ISagaData sagaData, IEnumerable sagaDataPropertyPathsToIndex)
+ {
+ return sagaDataPropertyPathsToIndex
+ .Select(x => new { Key = x, Value = Reflect.Value(sagaData, x) })
+ .Where(ShouldIndexValue)
+ .ToDictionary(x => x.Key, x => x.Value);
+ }
+
+ private static string GetIndexValue(object value)
+ {
+ if (value is string)
+ {
+ return value as string;
+ }
+ else if (value == null || value is IEnumerable)
+ {
+ return null;
+ }
+
+ return Convert.ToString(value);
+ }
+
+ private static IEnumerable GetIndexValues(object value)
+ {
+ if (!(value is IEnumerable) || value is string)
+ {
+ return null;
+ }
+
+ return (value as IEnumerable).Cast().Select(x => Convert.ToString(x)).ToArray();
+ }
+
+ private static string GetConcatenatedIndexValues(IEnumerable values)
+ {
+ if (values == null || !values.Any())
+ {
+ return null;
+ }
+
+ var sb = new StringBuilder(values.Sum(x => x.Length + 1) + 1);
+ sb.Append('|');
+
+ foreach (var value in values)
+ {
+ sb.Append(value);
+ sb.Append('|');
+ }
+
+ return sb.ToString();
+ }
+
+ #region Default saga name
+
+ private static string GetClassName(Type type)
+ {
+ var classNameRegex = new Regex("^[a-zA-Z0-9_]*[\\w]", RegexOptions.IgnoreCase);
+ var match = classNameRegex.Match(type.Name);
+
+ if (!match.Success) throw new Exception($"Error trying extract name class from type: {type.Name}");
+
+ return match.Value;
+ }
+
+ private static IEnumerable GetGenericArguments(Type type)
+ {
+ return type.GetGenericArguments()
+ .Select(x => x.Name)
+ .ToList();
+ }
+
+ private static string GetDefaultSagaName(Type type)
+ {
+ var declaringType = type.DeclaringType;
+
+ if (type.IsNested && declaringType != null)
+ {
+ if (declaringType.IsGenericType)
+ {
+ var className = GetClassName(declaringType);
+ var genericArguments = GetGenericArguments(type).ToList();
+
+ return genericArguments.Any()
+ ? $"{className}<{string.Join(",", genericArguments)}>"
+ : $"{className}";
+ }
+
+ return declaringType.Name;
+ }
+
+ return type.Name;
+ }
+
+ #endregion
+
+ private string GetSagaTypeName(Type sagaDataType)
+ {
+ var sagaTypeName = sagaNameCustomizer != null ? sagaNameCustomizer(sagaDataType) : GetDefaultSagaName(sagaDataType);
+
+ if (sagaTypeName.Length > MaximumSagaDataTypeNameLength)
+ {
+ throw new InvalidOperationException(
+ string.Format(
+ @"Sorry, but the maximum length of the name of a saga data class is currently limited to {0} characters!
+
+This is due to a limitation in SQL Server, where compound indexes have a 900 byte upper size limit - and
+since the saga index needs to be able to efficiently query by saga type, key, and value at the same time,
+there's room for only 200 characters as the key, 200 characters as the value, and 80 characters as the
+saga type name.",
+ MaximumSagaDataTypeNameLength));
+ }
+
+ return sagaTypeName;
+ }
+ }
+}
diff --git a/Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs b/Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs
new file mode 100644
index 0000000..e6e5cb3
--- /dev/null
+++ b/Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs
@@ -0,0 +1,1040 @@
+using System;
+using System.Data;
+using System.Linq;
+using System.Data.Common;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Text.RegularExpressions;
+using Newtonsoft.Json;
+
+using Rebus.Logging;
+using Rebus.Serialization;
+using Rebus.Serialization.Json;
+using Rebus.AdoNet.Schema;
+using Rebus.AdoNet.Dialects;
+
+namespace Rebus.AdoNet
+{
+ ///
+ /// Implements a saga persister for Rebus that stores sagas using an AdoNet provider.
+ ///
+ public class AdoNetSagaPersister : IStoreSagaData, AdoNetSagaPersisterFluentConfigurer, ICanUpdateMultipleSagaDatasAtomically
+ {
+ private const int MaximumSagaDataTypeNameLength = 80;
+ private const string SAGA_ID_COLUMN = "id";
+ private const string SAGA_TYPE_COLUMN = "saga_type";
+ private const string SAGA_DATA_COLUMN = "data";
+ private const string SAGA_REVISION_COLUMN = "revision";
+ private const string SAGAINDEX_ID_COLUMN = "saga_id";
+ private const string SAGAINDEX_KEY_COLUMN = "key";
+ private const string SAGAINDEX_VALUE_COLUMN = "value";
+ private const string SAGAINDEX_VALUES_COLUMN = "values";
+ private static ILog log;
+ private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings {
+ TypeNameHandling = TypeNameHandling.All, // TODO: Make it configurable by adding a SagaTypeResolver feature.
+ DateFormatHandling = DateFormatHandling.IsoDateFormat, // TODO: Make it configurable..
+ Binder = new CustomSerializationBinder()
+ };
+
+ private readonly AdoNetUnitOfWorkManager manager;
+ private readonly string sagaIndexTableName;
+ private readonly string sagaTableName;
+ private readonly string idPropertyName;
+ private bool useSagaLocking;
+ private bool useSqlArrays;
+ private bool useNoWaitSagaLocking;
+ private bool indexNullProperties = true;
+ private Func sagaNameCustomizer;
+
+ static AdoNetSagaPersister()
+ {
+ RebusLoggerFactory.Changed += f => log = f.GetCurrentClassLogger();
+ }
+
+ ///
+ /// Constructs the persister with the ability to create connections to database using the specified connection string.
+ /// This also means that the persister will manage the connection by itself, closing it when it has stopped using it.
+ ///
+ public AdoNetSagaPersister(AdoNetUnitOfWorkManager manager, string sagaTableName, string sagaIndexTableName)
+ {
+ this.manager = manager;
+ this.sagaTableName = sagaTableName;
+ this.sagaIndexTableName = sagaIndexTableName;
+ this.idPropertyName = Reflect.Path(x => x.Id);
+ }
+
+ #region AdoNetSagaPersisterFluentConfigurer
+
+ ///
+ /// Configures the persister to ignore null-valued correlation properties and not add them to the saga index.
+ ///
+ public AdoNetSagaPersisterFluentConfigurer DoNotIndexNullProperties()
+ {
+ indexNullProperties = false;
+ return this;
+ }
+
+ public AdoNetSagaPersisterFluentConfigurer UseLockingOnSagaUpdates(bool waitForLocks)
+ {
+ useSagaLocking = true;
+ useNoWaitSagaLocking = !waitForLocks;
+ return this;
+ }
+
+ ///
+ /// Creates the necessary saga storage tables if they haven't already been created. If a table already exists
+ /// with a name that matches one of the desired table names, no action is performed (i.e. it is assumed that
+ /// the tables already exist).
+ ///
+ public AdoNetSagaPersisterFluentConfigurer EnsureTablesAreCreated()
+ {
+ using (var uow = manager.Create(autonomous: true))
+ using (var scope = (uow as AdoNetUnitOfWork).GetScope())
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var tableNames = scope.GetTableNames();
+
+ // bail out if there's already a table in the database with one of the names
+ var sagaTableIsAlreadyCreated = tableNames.Contains(sagaTableName, StringComparer.InvariantCultureIgnoreCase);
+ var sagaIndexTableIsAlreadyCreated = tableNames.Contains(sagaIndexTableName, StringComparer.OrdinalIgnoreCase);
+
+ if (sagaTableIsAlreadyCreated && sagaIndexTableIsAlreadyCreated)
+ {
+ log.Debug("Tables '{0}' and '{1}' already exists.", sagaTableName, sagaIndexTableName);
+ return this;
+ }
+
+ if (sagaTableIsAlreadyCreated || sagaIndexTableIsAlreadyCreated)
+ {
+ // if saga index is created, then saga table is not created and vice versa
+ throw new ApplicationException(string.Format("Table '{0}' do not exist - you have to create it manually",
+ sagaIndexTableIsAlreadyCreated ? sagaTableName : sagaIndexTableName));
+ }
+
+ if (useSqlArrays && !dialect.SupportsArrayTypes)
+ {
+ throw new ApplicationException("Enabled UseSqlArraysForCorrelationIndexes but underlaying database does not support arrays?!");
+ }
+
+ log.Info("Tables '{0}' and '{1}' do not exist - they will be created now", sagaTableName, sagaIndexTableName);
+
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = scope.Dialect.FormatCreateTable(
+ new AdoNetTable()
+ {
+ Name = sagaTableName,
+ Columns = new []
+ {
+ new AdoNetColumn() { Name = SAGA_ID_COLUMN, DbType = DbType.Guid },
+ new AdoNetColumn() { Name = SAGA_TYPE_COLUMN, DbType = DbType.String, Length = MaximumSagaDataTypeNameLength },
+ new AdoNetColumn() { Name = SAGA_REVISION_COLUMN, DbType = DbType.Int32 },
+ new AdoNetColumn() { Name = SAGA_DATA_COLUMN, DbType = DbType.String, Length = 1073741823 }
+ },
+ PrimaryKey = new[] { SAGA_ID_COLUMN },
+ Indexes = new []
+ {
+ new AdoNetIndex() { Name = $"ix_{sagaTableName}_{SAGA_ID_COLUMN}_{SAGA_TYPE_COLUMN}", Columns = new [] { SAGA_ID_COLUMN, SAGA_TYPE_COLUMN } },
+ }
+ }
+ );
+
+ command.ExecuteNonQuery();
+ }
+
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = scope.Dialect.FormatCreateTable(
+ new AdoNetTable()
+ {
+ Name = sagaIndexTableName,
+ Columns = new []
+ {
+ new AdoNetColumn() { Name = SAGAINDEX_ID_COLUMN, DbType = DbType.Guid },
+ new AdoNetColumn() { Name = SAGAINDEX_KEY_COLUMN, DbType = DbType.String, Length = 200 },
+ new AdoNetColumn() { Name = SAGAINDEX_VALUE_COLUMN, DbType = DbType.String, Length = 1073741823, Nullable = true },
+ new AdoNetColumn() { Name = SAGAINDEX_VALUES_COLUMN, DbType = DbType.String, Length = 1073741823, Nullable = true, Array = useSqlArrays }
+ },
+ PrimaryKey = new[] { SAGAINDEX_ID_COLUMN, SAGAINDEX_KEY_COLUMN },
+ Indexes = new []
+ {
+ new AdoNetIndex()
+ {
+ Name = $"ix_{sagaIndexTableName}_{SAGAINDEX_KEY_COLUMN}_{SAGAINDEX_VALUE_COLUMN}",
+ Columns = new[] { SAGAINDEX_KEY_COLUMN, SAGAINDEX_VALUE_COLUMN },
+ //Kind = "ybgin"
+ },
+ new AdoNetIndex()
+ {
+ Name = $"ix_{sagaIndexTableName}_{SAGAINDEX_KEY_COLUMN}_{SAGAINDEX_VALUES_COLUMN}",
+ Columns = new[] { SAGAINDEX_KEY_COLUMN, SAGAINDEX_VALUES_COLUMN },
+ //Kind = "ybgin"
+ }
+ }
+ }
+ );
+
+ command.ExecuteNonQuery();
+ }
+
+ scope.Complete();
+ log.Info("Tables '{0}' and '{1}' created", sagaTableName, sagaIndexTableName);
+ }
+
+ /*using (var scope = manager.GetScope(autonomous: true))
+ {
+ var tableNames = scope.GetTableNames();
+ if (!tableNames.Contains(sagaTableName, StringComparer.InvariantCultureIgnoreCase))
+ throw new InvalidOperationException("Tables not present?!");
+
+ }*/
+
+ return this;
+ }
+
+ ///
+ /// Customizes the saga names by invoking this customizer.
+ ///
+ /// The customizer.
+ ///
+ public AdoNetSagaPersisterFluentConfigurer CustomizeSagaNamesAs(Func customizer)
+ {
+ this.sagaNameCustomizer = customizer;
+ return this;
+ }
+
+ ///
+ /// Enables locking of sagas as to avoid two or more workers to update them concurrently.
+ ///
+ /// The saga locking.
+ public AdoNetSagaPersisterFluentConfigurer EnableSagaLocking()
+ {
+ useSagaLocking = true;
+ return this;
+ }
+
+ ///
+ /// Uses the use of sql array types for storing indexes related to correlation properties.
+ ///
+ /// The sql arrays.
+ public AdoNetSagaPersisterFluentConfigurer UseSqlArraysForCorrelationIndexes()
+ {
+ useSqlArrays = true;
+ return this;
+ }
+
+ ///
+ /// Allows customizing opened connections by passing a delegate/lambda to invoke for each new connection.
+ ///
+ ///
+ ///
+ public AdoNetSagaPersisterFluentConfigurer CustomizeOpenedConnections(Action customizer)
+ {
+ manager.ConnectionFactory.ConnectionCustomizer = customizer;
+ return this;
+ }
+
+ ///
+ /// Customizes type2name & name2type mapping logic used during serialization/deserialization.
+ ///
+ /// Delegate to invoke when resolving a name-to-type during deserialization.
+ /// Delegate to invoke when resolving a type-to-name during serialization.
+ ///
+ public AdoNetSagaPersisterFluentConfigurer CustomizeSerializationTypeResolving(Func nameToTypeResolver, Func typeToNameResolver)
+ {
+ (Settings.Binder as CustomSerializationBinder).NameToTypeResolver = nameToTypeResolver;
+ (Settings.Binder as CustomSerializationBinder).TypeToNameResolver = typeToNameResolver;
+ return this;
+ }
+
+ #endregion
+
+ public void Insert(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
+ {
+ using (var scope = manager.GetScope())
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var tableNames = scope.GetTableNames();
+ var sagaTypeName = GetSagaTypeName(sagaData.GetType());
+
+ // next insert the saga
+ using (var command = connection.CreateCommand())
+ {
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.AddParameter(dialect.EscapeParameter(SAGA_TYPE_COLUMN), sagaTypeName);
+ command.AddParameter(dialect.EscapeParameter(SAGA_REVISION_COLUMN), ++sagaData.Revision);
+ command.AddParameter(dialect.EscapeParameter(SAGA_DATA_COLUMN), JsonConvert.SerializeObject(sagaData, Formatting.Indented, Settings));
+
+ command.CommandText = string.Format(
+ @"insert into {0} ({1}, {2}, {3}, {4}) values ({5}, {6}, {7}, {8});",
+ dialect.QuoteForTableName(sagaTableName),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN),
+ dialect.QuoteForColumnName(SAGA_TYPE_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN),
+ dialect.QuoteForColumnName(SAGA_DATA_COLUMN),
+ dialect.EscapeParameter(SAGA_ID_COLUMN),
+ dialect.EscapeParameter(SAGA_TYPE_COLUMN),
+ dialect.EscapeParameter(SAGA_REVISION_COLUMN),
+ dialect.EscapeParameter(SAGA_DATA_COLUMN)
+ );
+
+ try
+ {
+ command.ExecuteNonQuery();
+ }
+ catch (DbException exception)
+ {
+ throw new OptimisticLockingException(sagaData, exception);
+ }
+ }
+
+ var propertiesToIndex = GetPropertiesToIndex(sagaData, sagaDataPropertyPathsToIndex);
+
+ if (propertiesToIndex.Any())
+ {
+ DeclareIndex(sagaData, scope, propertiesToIndex);
+ }
+
+ scope.Complete();
+ }
+ }
+
+ public void Update(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
+ {
+ using (var scope = manager.GetScope())
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var tableNames = scope.GetTableNames();
+
+ // next, update or insert the saga
+ using (var command = connection.CreateCommand())
+ {
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.AddParameter(dialect.EscapeParameter("current_revision"), sagaData.Revision);
+ command.AddParameter(dialect.EscapeParameter("next_revision"), ++sagaData.Revision);
+ command.AddParameter(dialect.EscapeParameter(SAGA_DATA_COLUMN), JsonConvert.SerializeObject(sagaData, Formatting.Indented, Settings));
+
+ command.CommandText = string.Format(
+ @"UPDATE {0} SET {1} = {2}, {3} = {4} " +
+ @"WHERE {5} = {6} AND {7} = {8};",
+ dialect.QuoteForTableName(sagaTableName),
+ dialect.QuoteForColumnName(SAGA_DATA_COLUMN), dialect.EscapeParameter(SAGA_DATA_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("next_revision"),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("current_revision")
+ );
+ var rows = command.ExecuteNonQuery();
+ if (rows == 0)
+ {
+ throw new OptimisticLockingException(sagaData);
+ }
+ }
+
+ var propertiesToIndex = GetPropertiesToIndex(sagaData, sagaDataPropertyPathsToIndex);
+
+ if (propertiesToIndex.Any())
+ {
+ DeclareIndex(sagaData, scope, propertiesToIndex);
+ }
+
+ scope.Complete();
+ }
+ }
+
+ private void DeclareIndexUsingTableExpressions(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary 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);
+ }
+ }
+ }
+
+ private void DeclareIndexUsingReturningClause(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary propertiesToIndex)
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var existingKeys = Enumerable.Empty();
+
+ 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(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);
+ }
+ }
+ }
+
+ private void DeclareIndexUnoptimized(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary 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();
+
+ // 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(SAGAINDEX_KEY_COLUMN).ToArray();
+ }
+ }
+ catch (DbException exception)
+ {
+ // FIXME: Ensure exception is the right one..
+ 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);
+ }
+
+ }
+ }
+ }
+
+ private void DeclareIndex(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary propertiesToIndex)
+ {
+ var dialect = scope.Dialect;
+
+ if (dialect.SupportsOnConflictClause && dialect.SupportsReturningClause && dialect.SupportsTableExpressions)
+ {
+ DeclareIndexUsingTableExpressions(sagaData, scope, propertiesToIndex);
+ }
+ else if (dialect.SupportsOnConflictClause && dialect.SupportsReturningClause)
+ {
+ DeclareIndexUsingReturningClause(sagaData, scope, propertiesToIndex);
+ }
+ else
+ {
+ DeclareIndexUnoptimized(sagaData, scope, propertiesToIndex);
+ }
+ }
+
+ public void Delete(ISagaData sagaData)
+ {
+ using (var scope = manager.GetScope())
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = string.Format(
+ @"DELETE FROM {0} WHERE {1} = {2} AND {3} = {4};",
+ dialect.QuoteForTableName(sagaTableName),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("current_revision")
+ );
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.AddParameter(dialect.EscapeParameter("current_revision"), sagaData.Revision);
+
+ var rows = command.ExecuteNonQuery();
+
+ if (rows == 0)
+ {
+ throw new OptimisticLockingException(sagaData);
+ }
+ }
+
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = string.Format(
+ @"DELETE FROM {0} WHERE {1} = {2};",
+ dialect.QuoteForTableName(sagaIndexTableName),
+ dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN)
+ );
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.ExecuteNonQuery();
+ }
+
+ scope.Complete();
+ }
+ }
+
+ private string GetSagaLockingClause(SqlDialect dialect)
+ {
+ if (useSagaLocking)
+ {
+ return useNoWaitSagaLocking
+ ? $"{dialect.SelectForUpdateClause} {dialect.SelectForNoWaitClause}"
+ : dialect.SelectForUpdateClause;
+ }
+
+ return string.Empty;
+ }
+
+ public TSagaData Find(string sagaDataPropertyPath, object fieldFromMessage) where TSagaData : class, ISagaData
+ {
+ using (var scope = manager.GetScope(autocomplete: true))
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var sagaType = GetSagaTypeName(typeof(TSagaData));
+
+ if (useSagaLocking)
+ {
+ if (!dialect.SupportsSelectForUpdate)
+ throw new InvalidOperationException($"You can't use saga locking for a Dialect {dialect.GetType()} that does not supports Select For Update.");
+
+ if (useNoWaitSagaLocking && !dialect.SupportsSelectForWithNoWait)
+ throw new InvalidOperationException($"You can't use saga locking with no-wait for a Dialect {dialect.GetType()} that does not supports no-wait clause.");
+ }
+
+ using (var command = connection.CreateCommand())
+ {
+ if (sagaDataPropertyPath == idPropertyName)
+ {
+ var id = (fieldFromMessage is Guid) ? (Guid)fieldFromMessage : Guid.Parse(fieldFromMessage.ToString());
+ var idParam = dialect.EscapeParameter("id");
+ var sagaTypeParam = dialect.EscapeParameter(SAGA_TYPE_COLUMN);
+
+ command.CommandText = string.Format(
+ @"SELECT s.{0} FROM {1} s WHERE s.{2} = {3} AND s.{4} = {5} {6}",
+ dialect.QuoteForColumnName(SAGA_DATA_COLUMN),
+ dialect.QuoteForTableName(sagaTableName),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN),
+ idParam,
+ dialect.QuoteForColumnName(SAGA_TYPE_COLUMN),
+ sagaTypeParam,
+ GetSagaLockingClause(dialect)
+ );
+ command.AddParameter(sagaTypeParam, sagaType);
+ command.AddParameter(idParam, id);
+ }
+ else
+ {
+ var dataCol = dialect.QuoteForColumnName(SAGA_DATA_COLUMN);
+ var sagaTblName = dialect.QuoteForTableName(sagaTableName);
+ var sagaTypeCol = dialect.QuoteForColumnName(SAGA_TYPE_COLUMN);
+ var sagaTypeParam = dialect.EscapeParameter(SAGA_TYPE_COLUMN);
+ var indexTblName = dialect.QuoteForTableName(sagaIndexTableName);
+ var sagaIdCol = dialect.QuoteForColumnName(SAGA_ID_COLUMN);
+ var indexIdCol = dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN);
+ var indexKeyCol = dialect.QuoteForColumnName(SAGAINDEX_KEY_COLUMN);
+ var indexKeyParam = dialect.EscapeParameter(SAGAINDEX_KEY_COLUMN);
+ var indexValueCol = dialect.QuoteForColumnName(SAGAINDEX_VALUE_COLUMN);
+ var indexValueParm = dialect.EscapeParameter(SAGAINDEX_VALUE_COLUMN);
+ var indexValuesCol = dialect.QuoteForColumnName(SAGAINDEX_VALUES_COLUMN);
+ var indexValuesParm = dialect.EscapeParameter(SAGAINDEX_VALUES_COLUMN);
+ var forUpdate = GetSagaLockingClause(dialect);
+ var valuesPredicate = ArraysEnabledFor(dialect)
+ ? dialect.FormatArrayAny($"i.{indexValuesCol}", indexValuesParm)
+ : $"(i.{indexValuesCol} LIKE ('%' || {indexValuesParm} || '%'))";
+
+ command.CommandText = $@"
+ SELECT s.{dataCol}
+ FROM {sagaTblName} s
+ JOIN {indexTblName} i on s.{sagaIdCol} = i.{indexIdCol}
+ WHERE s.{sagaTypeCol} = {sagaTypeParam}
+ AND i.{indexKeyCol} = {indexKeyParam}
+ AND (
+ CASE WHEN {indexValueParm} IS NULL THEN i.{indexValueCol} IS NULL
+ ELSE
+ (
+ i.{indexValueCol} = {indexValueParm}
+ OR
+ (i.{indexValuesCol} is NOT NULL AND {valuesPredicate})
+ )
+ END
+ )
+ {forUpdate};".Replace("\t", "");
+
+ var value = GetIndexValue(fieldFromMessage);
+ var values = value == null ? null : ArraysEnabledFor(dialect)
+ ? (object)(new[] { value })
+ : GetConcatenatedIndexValues(new[] { value });
+ var valuesDbType = ArraysEnabledFor(dialect) ? DbType.Object : DbType.String;
+
+ command.AddParameter(indexKeyParam, sagaDataPropertyPath);
+ command.AddParameter(sagaTypeParam, sagaType);
+ command.AddParameter(indexValueParm, DbType.String, value);
+ command.AddParameter(indexValuesParm, valuesDbType, values);
+ }
+
+ string data = null;
+
+ try
+ {
+ log.Debug("Finding saga of type {0} with {1} = {2}", sagaType, sagaDataPropertyPath, fieldFromMessage);
+ //if (dialect is PostgreSqlDialect) connection.ExecuteCommand("SET LOCAL enable_seqscan = off;");
+ data = (string)command.ExecuteScalar();
+ //if (dialect is PostgreSqlDialect) connection.ExecuteCommand("SET LOCAL enable_seqscan = on;");
+ }
+ catch (DbException ex)
+ {
+ // When in no-wait saga-locking mode, inspect
+ // exception and rethrow ex as SagaLockedException.
+ if (useSagaLocking && useNoWaitSagaLocking)
+ {
+ if (dialect.IsSelectForNoWaitLockingException(ex))
+ throw new AdoNetSagaLockedException(ex);
+ }
+
+ throw;
+ }
+
+ if (data == null)
+ {
+ log.Debug("No saga found of type {0} with {1} = {2}", sagaType, sagaDataPropertyPath, fieldFromMessage);
+ return null;
+ }
+
+ log.Debug("Found saga of type {0} with {1} = {2}", sagaType, sagaDataPropertyPath, fieldFromMessage);
+
+ try
+ {
+ return JsonConvert.DeserializeObject(data, Settings);
+ }
+ catch { }
+
+ try
+ {
+ return (TSagaData)JsonConvert.DeserializeObject(data, Settings);
+ }
+ catch (Exception exception)
+ {
+ var message = string.Format("An error occurred while attempting to deserialize '{0}' into a {1}", data, typeof(TSagaData));
+
+ throw new ApplicationException(message, exception);
+ }
+ }
+ }
+ }
+
+ private bool ArraysEnabledFor(SqlDialect dialect)
+ {
+ return useSqlArrays && dialect.SupportsArrayTypes;
+ }
+
+ private bool ShouldIndexValue(object value)
+ {
+ if (indexNullProperties)
+ return true;
+
+ if (value == null) return false;
+ if (value is string) return true;
+ if ((value is IEnumerable) && !(value as IEnumerable).Cast().Any()) return false;
+
+ return true;
+ }
+
+ private IDictionary GetPropertiesToIndex(ISagaData sagaData, IEnumerable sagaDataPropertyPathsToIndex)
+ {
+ return sagaDataPropertyPathsToIndex
+ .Select(x => new { Key = x, Value = Reflect.Value(sagaData, x) })
+ .Where(ShouldIndexValue)
+ .ToDictionary(x => x.Key, x => x.Value);
+ }
+
+ private static string GetIndexValue(object value)
+ {
+ if (value is string)
+ {
+ return value as string;
+ }
+ else if (value == null || value is IEnumerable)
+ {
+ return null;
+ }
+
+ return Convert.ToString(value);
+ }
+
+ private static IEnumerable GetIndexValues(object value)
+ {
+ if (!(value is IEnumerable) || value is string)
+ {
+ return null;
+ }
+
+ return (value as IEnumerable).Cast().Select(x => Convert.ToString(x)).ToArray();
+ }
+
+ private static string GetConcatenatedIndexValues(IEnumerable values)
+ {
+ if (values == null || !values.Any())
+ {
+ return null;
+ }
+
+ var sb = new StringBuilder(values.Sum(x => x.Length + 1) + 1);
+ sb.Append('|');
+
+ foreach (var value in values)
+ {
+ sb.Append(value);
+ sb.Append('|');
+ }
+
+ return sb.ToString();
+ }
+
+ #region Default saga name
+
+ private static string GetClassName(Type type)
+ {
+ var classNameRegex = new Regex("^[a-zA-Z0-9_]*[\\w]", RegexOptions.IgnoreCase);
+ var match = classNameRegex.Match(type.Name);
+
+ if (!match.Success) throw new Exception($"Error trying extract name class from type: {type.Name}");
+
+ return match.Value;
+ }
+
+ private static IEnumerable GetGenericArguments(Type type)
+ {
+ return type.GetGenericArguments()
+ .Select(x => x.Name)
+ .ToList();
+ }
+
+ private static string GetDefaultSagaName(Type type)
+ {
+ var declaringType = type.DeclaringType;
+
+ if (type.IsNested && declaringType != null)
+ {
+ if (declaringType.IsGenericType)
+ {
+ var className = GetClassName(declaringType);
+ var genericArguments = GetGenericArguments(type).ToList();
+
+ return genericArguments.Any()
+ ? $"{className}<{string.Join(",", genericArguments)}>"
+ : $"{className}";
+ }
+
+ return declaringType.Name;
+ }
+
+ return type.Name;
+ }
+
+ #endregion
+
+ private string GetSagaTypeName(Type sagaDataType)
+ {
+ var sagaTypeName = sagaNameCustomizer != null ? sagaNameCustomizer(sagaDataType) : GetDefaultSagaName(sagaDataType);
+
+ if (sagaTypeName.Length > MaximumSagaDataTypeNameLength)
+ {
+ throw new InvalidOperationException(
+ string.Format(
+ @"Sorry, but the maximum length of the name of a saga data class is currently limited to {0} characters!
+
+This is due to a limitation in SQL Server, where compound indexes have a 900 byte upper size limit - and
+since the saga index needs to be able to efficiently query by saga type, key, and value at the same time,
+there's room for only 200 characters as the key, 200 characters as the value, and 80 characters as the
+saga type name.",
+ MaximumSagaDataTypeNameLength));
+ }
+
+ return sagaTypeName;
+ }
+ }
+}
diff --git a/Rebus.AdoNet/Dialects/PostgreSql92Dialect.cs b/Rebus.AdoNet/Dialects/PostgreSql92Dialect.cs
new file mode 100644
index 0000000..9d89371
--- /dev/null
+++ b/Rebus.AdoNet/Dialects/PostgreSql92Dialect.cs
@@ -0,0 +1,5 @@
+namespace Rebus.AdoNet.Dialects {
+ public class PostgreSql92Dialect {
+
+ }
+}
\ No newline at end of file
From 68adaa3ab7d8b2ff9e29ca0291088353050c9802 Mon Sep 17 00:00:00 2001
From: Pablo Ruiz
Date: Wed, 6 Jul 2022 03:39:50 +0200
Subject: [PATCH 3/8] Implemented support for Json-based saga indexing, using a
single table in order to speed up lookups by avoiding joins.
---
README.md | 12 +
Rebus.AdoNet.Tests/Rebus.AdoNet.Tests.csproj | 37 +-
Rebus.AdoNet.Tests/SagaPersisterTests.cs | 218 ++--
Rebus.AdoNet.Tests/packages.config | 22 +-
Rebus.AdoNet/AdoNetExtensions.cs | 37 +-
Rebus.AdoNet/AdoNetSagaPersister.cs | 919 ++------------
Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs | 1070 ++++------------
Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs | 435 ++-----
Rebus.AdoNet/AdoNetSubscriptionStorage.cs | 2 +-
Rebus.AdoNet/AdoNetUnitOfWork.cs | 31 +-
Rebus.AdoNet/AdoNetUnitOfWorkManager.cs | 17 +-
Rebus.AdoNet/AdoNetUnitOfWorkScope.cs | 12 +-
Rebus.AdoNet/Dialects/PostgreSql10Dialect.cs | 2 +-
Rebus.AdoNet/Dialects/PostgreSql82Dialect.cs | 13 +-
Rebus.AdoNet/Dialects/PostgreSql91Dialect.cs | 19 +-
Rebus.AdoNet/Dialects/PostgreSql92Dialect.cs | 22 +-
Rebus.AdoNet/Dialects/PostgreSql94Dialect.cs | 8 +-
Rebus.AdoNet/Dialects/PostgreSqlDialect.cs | 261 ++--
Rebus.AdoNet/Dialects/SqlDialect.cs | 1138 +++++++++---------
Rebus.AdoNet/Dialects/YugabyteDbDialect.cs | 48 +-
Rebus.AdoNet/Rebus.AdoNet.csproj | 4 +-
Rebus.AdoNet/Schema/AdoNetColumn.cs | 43 +-
Rebus.AdoNet/Schema/AdoNetIndex.cs | 52 +-
23 files changed, 1541 insertions(+), 2881 deletions(-)
diff --git a/README.md b/README.md
index b98dc42..366fe69 100644
--- a/README.md
+++ b/README.md
@@ -6,3 +6,15 @@ In order to run unit-tests locally, a postgres instance is required.
It can be launched using docker, with same parameters as it is launched on appveyor.
* docker run -it --rm -e 'POSTGRES_PASSWORD=Password12!' -e 'POSTGRES_DB=test' -p 5432:5432 postgres:10
+
+# Running Unit Tests (against YugaByteDB)
+In order to run unit-tests locally using YugaByteDB, a 2.15+ version is required for full
+compatibility (as this version supports FOR UPDATE NOWAIT/SKIP LOCKED), and it should
+have been started as:
+
+docker run -it --rm --name yugabyte -p1070:7000 -p9000:9000 -p5432:5433 -p9042:9042 yugabytedb/yugabyte:2.15.0.0-b11 bin/yugabyted start --daemon=false --tserver_flags="yb_enable_read_committed_isolation=true"
+
+Also a test database has to be pre-created using:
+
+docker exec -it yugabyte bin/ysqlsh -C "CREATE DATABASE test;"
+
diff --git a/Rebus.AdoNet.Tests/Rebus.AdoNet.Tests.csproj b/Rebus.AdoNet.Tests/Rebus.AdoNet.Tests.csproj
index 7fa966b..0a6312b 100644
--- a/Rebus.AdoNet.Tests/Rebus.AdoNet.Tests.csproj
+++ b/Rebus.AdoNet.Tests/Rebus.AdoNet.Tests.csproj
@@ -1,5 +1,7 @@
+
+
Debug
@@ -40,37 +42,8 @@
..\packages\Common.Logging.Core.3.3.1\lib\net40\Common.Logging.Core.dll
True
-
- ..\packages\NUnit3TestAdapter.3.4.0\lib\Mono.Cecil.dll
- False
-
-
- ..\packages\NUnit3TestAdapter.3.4.0\lib\Mono.Cecil.Mdb.dll
- False
-
-
- ..\packages\NUnit3TestAdapter.3.4.0\lib\Mono.Cecil.Pdb.dll
- False
-
-
- ..\packages\NUnit3TestAdapter.3.4.0\lib\Mono.Cecil.Rocks.dll
- False
-
-
- ..\packages\NUnit3TestAdapter.3.4.0\lib\nunit.engine.dll
- False
-
-
- ..\packages\NUnit3TestAdapter.3.4.0\lib\nunit.engine.api.dll
- False
-
-
- ..\packages\NUnit.3.4.1\lib\net45\nunit.framework.dll
- True
-
-
- ..\packages\NUnit3TestAdapter.3.4.0\lib\NUnit3.TestAdapter.dll
- False
+
+ ..\packages\NUnit.3.13.3\lib\net45\nunit.framework.dll
..\packages\Rebus.0.84.0\lib\NET45\Rebus.dll
@@ -132,6 +105,8 @@
This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
"TEXT" (default)
- /// Names.Get(DbType,100) // --> "VARCHAR(100)" (100 is in [0:255])
- /// Names.Get(DbType,1000) // --> "LONGVARCHAR(1000)" (100 is in [256:65534])
- /// Names.Get(DbType,100000) // --> "TEXT" (default)
- ///
- /// On the other hand, simply putting
- ///
- /// Names.Put(DbType, "VARCHAR($l)" );
- ///
- /// would result in
- ///
- /// Names.Get(DbType) // --> "VARCHAR($l)" (will cause trouble)
- /// Names.Get(DbType,100) // --> "VARCHAR(100)"
- /// Names.Get(DbType,1000) // --> "VARCHAR(1000)"
- /// Names.Get(DbType,10000) // --> "VARCHAR(10000)"
- ///
- ///
- public class TypeNames
- {
- public const string LengthPlaceHolder = "$l";
- public const string PrecisionPlaceHolder = "$p";
- public const string ScalePlaceHolder = "$s";
-
- private readonly Dictionary> weighted = new Dictionary>();
- private readonly Dictionary defaults = new Dictionary();
-
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- private static string ReplaceOnce(string template, string placeholder, string replacement)
- {
- int loc = template.IndexOf(placeholder, StringComparison.Ordinal);
- if (loc < 0)
- {
- return template;
- }
- else
- {
- return new StringBuilder(template.Substring(0, loc))
- .Append(replacement)
- .Append(template.Substring(loc + placeholder.Length))
- .ToString();
- }
- }
-
- ///
- /// Replaces the specified type.
- ///
- /// The type.
- /// The size.
- /// The precision.
- /// The scale.
- ///
- private static string Replace(string type, uint size, uint precision, uint scale)
- {
- type = ReplaceOnce(type, LengthPlaceHolder, size.ToString());
- type = ReplaceOnce(type, ScalePlaceHolder, scale.ToString());
- return ReplaceOnce(type, PrecisionPlaceHolder, precision.ToString());
- }
-
- ///
- /// Get default type name for specified type
- ///
- /// the type key
- /// the default type name associated with the specified key
- public string Get(DbType typecode)
- {
- string result;
- if (!defaults.TryGetValue(typecode, out result))
- {
- throw new ArgumentException("Dialect does not support DbType." + typecode, "typecode");
- }
- return result;
- }
-
- ///
- /// Get the type name specified type and size
- ///
- /// the type key
- /// the SQL length
- /// the SQL scale
- /// the SQL precision
- ///
- /// The associated name with smallest capacity >= size if available and the
- /// default type name otherwise
- ///
- public string Get(DbType typecode, uint size, uint precision, uint scale)
- {
- SortedList map;
- weighted.TryGetValue(typecode, out map);
- if (map != null && map.Count > 0)
- {
- foreach (KeyValuePair entry in map)
- {
- if (size <= entry.Key)
- {
- return Replace(entry.Value, size, precision, scale);
- }
- }
- }
- //Could not find a specific type for the size, using the default
- return Replace(Get(typecode), size, precision, scale);
- }
-
- ///
- /// For types with a simple length, this method returns the definition
- /// for the longest registered type.
- ///
- ///
- ///
- public string GetLongest(DbType typecode)
- {
- SortedList map;
- weighted.TryGetValue(typecode, out map);
-
- if (map != null && map.Count > 0)
- return Replace(map.Values[map.Count - 1], map.Keys[map.Count - 1], 0, 0);
-
- return Get(typecode);
- }
-
- ///
- /// Set a type name for specified type key and capacity
- ///
- /// the type key
- /// the (maximum) type size/length
- /// The associated name
- public void Put(DbType typecode, uint capacity, string value)
- {
- SortedList map;
- if (!weighted.TryGetValue(typecode, out map))
- {
- // add new ordered map
- weighted[typecode] = map = new SortedList();
- }
- map[capacity] = value;
- }
-
- ///
- ///
- ///
- ///
- ///
- public void Put(DbType typecode, string value)
- {
- defaults[typecode] = value;
- }
- }
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Text;
+
+namespace Rebus.AdoNet.Dialects
+{
+ ///
+ /// This class maps a DbType to names.
+ ///
+ ///
+ /// Associations may be marked with a capacity. Calling the Get()
+ /// method with a type and actual size n will return the associated
+ /// name with smallest capacity >= n, if available and an unmarked
+ /// default type otherwise.
+ /// Eg, setting
+ ///
+ /// Names.Put(DbType, "TEXT" );
+ /// Names.Put(DbType, 255, "VARCHAR($l)" );
+ /// Names.Put(DbType, 65534, "LONGVARCHAR($l)" );
+ ///
+ /// will give you back the following:
+ ///
+ /// Names.Get(DbType) // --> "TEXT" (default)
+ /// Names.Get(DbType,100) // --> "VARCHAR(100)" (100 is in [0:255])
+ /// Names.Get(DbType,1000) // --> "LONGVARCHAR(1000)" (100 is in [256:65534])
+ /// Names.Get(DbType,100000) // --> "TEXT" (default)
+ ///
+ /// On the other hand, simply putting
+ ///
+ /// Names.Put(DbType, "VARCHAR($l)" );
+ ///
+ /// would result in
+ ///
+ /// Names.Get(DbType) // --> "VARCHAR($l)" (will cause trouble)
+ /// Names.Get(DbType,100) // --> "VARCHAR(100)"
+ /// Names.Get(DbType,1000) // --> "VARCHAR(1000)"
+ /// Names.Get(DbType,10000) // --> "VARCHAR(10000)"
+ ///
+ ///
+ public class TypeNames
+ {
+ public const string LengthPlaceHolder = "$l";
+ public const string PrecisionPlaceHolder = "$p";
+ public const string ScalePlaceHolder = "$s";
+
+ private readonly Dictionary> weighted = new Dictionary>();
+ private readonly Dictionary defaults = new Dictionary();
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static string ReplaceOnce(string template, string placeholder, string replacement)
+ {
+ int loc = template.IndexOf(placeholder, StringComparison.Ordinal);
+ if (loc < 0)
+ {
+ return template;
+ }
+ else
+ {
+ return new StringBuilder(template.Substring(0, loc))
+ .Append(replacement)
+ .Append(template.Substring(loc + placeholder.Length))
+ .ToString();
+ }
+ }
+
+ ///
+ /// Replaces the specified type.
+ ///
+ /// The type.
+ /// The size.
+ /// The precision.
+ /// The scale.
+ ///
+ private static string Replace(string type, uint size, uint precision, uint scale)
+ {
+ type = ReplaceOnce(type, LengthPlaceHolder, size.ToString());
+ type = ReplaceOnce(type, ScalePlaceHolder, scale.ToString());
+ return ReplaceOnce(type, PrecisionPlaceHolder, precision.ToString());
+ }
+
+ ///
+ /// Get default type name for specified type
+ ///
+ /// the type key
+ /// the default type name associated with the specified key
+ public string Get(DbType typecode)
+ {
+ string result;
+ if (!defaults.TryGetValue(typecode, out result))
+ {
+ throw new ArgumentException("Dialect does not support DbType." + typecode, "typecode");
+ }
+ return result;
+ }
+
+ ///
+ /// Get the type name specified type and size
+ ///
+ /// the type key
+ /// the SQL length
+ /// the SQL scale
+ /// the SQL precision
+ ///
+ /// The associated name with smallest capacity >= size if available and the
+ /// default type name otherwise
+ ///
+ public string Get(DbType typecode, uint size, uint precision, uint scale)
+ {
+ SortedList map;
+ weighted.TryGetValue(typecode, out map);
+ if (map != null && map.Count > 0)
+ {
+ foreach (KeyValuePair entry in map)
+ {
+ if (size <= entry.Key)
+ {
+ return Replace(entry.Value, size, precision, scale);
+ }
+ }
+ }
+ //Could not find a specific type for the size, using the default
+ return Replace(Get(typecode), size, precision, scale);
+ }
+
+ ///
+ /// For types with a simple length, this method returns the definition
+ /// for the longest registered type.
+ ///
+ ///
+ ///
+ public string GetLongest(DbType typecode)
+ {
+ SortedList map;
+ weighted.TryGetValue(typecode, out map);
+
+ if (map != null && map.Count > 0)
+ return Replace(map.Values[map.Count - 1], map.Keys[map.Count - 1], 0, 0);
+
+ return Get(typecode);
+ }
+
+ ///
+ /// Set a type name for specified type key and capacity
+ ///
+ /// the type key
+ /// the (maximum) type size/length
+ /// The associated name
+ public void Put(DbType typecode, uint capacity, string value)
+ {
+ SortedList map;
+ if (!weighted.TryGetValue(typecode, out map))
+ {
+ // add new ordered map
+ weighted[typecode] = map = new SortedList();
+ }
+ map[capacity] = value;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Put(DbType typecode, string value)
+ {
+ defaults[typecode] = value;
+ }
+ }
}
\ No newline at end of file
diff --git a/Rebus.AdoNet/Properties/AssemblyInfo.cs b/Rebus.AdoNet/Properties/AssemblyInfo.cs
index 139f099..7adf52b 100644
--- a/Rebus.AdoNet/Properties/AssemblyInfo.cs
+++ b/Rebus.AdoNet/Properties/AssemblyInfo.cs
@@ -1,39 +1,39 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Rebus.AdoNet")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Rebus.AdoNet")]
-[assembly: AssemblyCopyright("Copyright © Evidencias Certificadas S.L. (2015~2016)")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("03b56847-7469-4db3-b146-2d29ce61663e")]
-
-[assembly: InternalsVisibleTo("Rebus.AdoNet.Tests")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("0.0.0.0")]
-[assembly: AssemblyFileVersion("0.0.0.0")]
-[assembly: AssemblyInformationalVersion("VERSION_STRING")]
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Rebus.AdoNet")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Rebus.AdoNet")]
+[assembly: AssemblyCopyright("Copyright © Evidencias Certificadas S.L. (2015~2016)")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("03b56847-7469-4db3-b146-2d29ce61663e")]
+
+[assembly: InternalsVisibleTo("Rebus.AdoNet.Tests")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("0.0.0.0")]
+[assembly: AssemblyFileVersion("0.0.0.0")]
+[assembly: AssemblyInformationalVersion("VERSION_STRING")]
diff --git a/Rebus.AdoNet/Schema/AdoNetTable.cs b/Rebus.AdoNet/Schema/AdoNetTable.cs
index 1495f99..c903d49 100755
--- a/Rebus.AdoNet/Schema/AdoNetTable.cs
+++ b/Rebus.AdoNet/Schema/AdoNetTable.cs
@@ -1,19 +1,19 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Data;
-using System.Data.Common;
-using System.Text;
-
-namespace Rebus.AdoNet.Schema
-{
- public class AdoNetTable
- {
- public string Name { get; set; }
- public IEnumerable Columns { get; set; }
- public string[] PrimaryKey { get; set; }
- public IEnumerable Indexes { get; set; }
-
- public bool HasCompositePrimaryKey => PrimaryKey?.Count() > 1;
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Data;
+using System.Data.Common;
+using System.Text;
+
+namespace Rebus.AdoNet.Schema
+{
+ public class AdoNetTable
+ {
+ public string Name { get; set; }
+ public IEnumerable Columns { get; set; }
+ public string[] PrimaryKey { get; set; }
+ public IEnumerable Indexes { get; set; }
+
+ public bool HasCompositePrimaryKey => PrimaryKey?.Count() > 1;
+ }
+}
diff --git a/Rebus.AdoNet/netfx/System/Guard.cs b/Rebus.AdoNet/netfx/System/Guard.cs
index ff733b4..ab217bd 100755
--- a/Rebus.AdoNet/netfx/System/Guard.cs
+++ b/Rebus.AdoNet/netfx/System/Guard.cs
@@ -1,101 +1,101 @@
-#region BSD License
-/*
-Copyright (c) 2011, NETFx
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without modification,
-are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this list
- of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice, this
- list of conditions and the following disclaimer in the documentation and/or other
- materials provided with the distribution.
-
-* Neither the name of Clarius Consulting nor the names of its contributors may be
- used to endorse or promote products derived from this software without specific
- prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
-EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
-OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
-SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
-INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
-TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
-BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
-ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
-DAMAGE.
-*/
-#endregion
-
-using System;
-using System.Diagnostics;
-using System.Linq.Expressions;
-
-///
-/// Common guard class for argument validation.
-///
-///
-[DebuggerStepThrough]
-static class Guard
-{
- ///
- /// Ensures the given is not null.
- /// Throws otherwise.
- ///
- /// The is null.
- public static void NotNull(Expression> reference, T value)
- {
- if (value == null)
- throw new ArgumentNullException(GetParameterName(reference), "Parameter cannot be null.");
- }
-
- ///
- /// Ensures the given string is not null or empty.
- /// Throws in the first case, or
- /// in the latter.
- ///
- /// The is null or an empty string.
- public static void NotNullOrEmpty(Expression> reference, string value)
- {
- NotNull(reference, value);
- if (value.Length == 0)
- throw new ArgumentException("Parameter cannot be empty.", GetParameterName(reference));
- }
-
- ///
- /// Ensures the given string is valid according
- /// to the function. Throws
- /// otherwise.
- ///
- /// The is not valid according
- /// to the function.
- public static void IsValid(Expression> reference, T value, Func validate, string message)
- {
- if (!validate(value))
- throw new ArgumentException(message, GetParameterName(reference));
- }
-
- ///
- /// Ensures the given string is valid according
- /// to the function. Throws
- /// otherwise.
- ///
- /// The is not valid according
- /// to the function.
- public static void IsValid(Expression> reference, T value, Func validate, string format, params object[] args)
- {
- if (!validate(value))
- throw new ArgumentException(string.Format(format, args), GetParameterName(reference));
- }
-
- private static string GetParameterName(Expression reference)
- {
- var lambda = reference as LambdaExpression;
- var member = lambda.Body as MemberExpression;
-
- return member.Member.Name;
- }
+#region BSD License
+/*
+Copyright (c) 2011, NETFx
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this list
+ of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or other
+ materials provided with the distribution.
+
+* Neither the name of Clarius Consulting nor the names of its contributors may be
+ used to endorse or promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+*/
+#endregion
+
+using System;
+using System.Diagnostics;
+using System.Linq.Expressions;
+
+///
+/// Common guard class for argument validation.
+///
+///
+[DebuggerStepThrough]
+static class Guard
+{
+ ///
+ /// Ensures the given is not null.
+ /// Throws otherwise.
+ ///
+ /// The is null.
+ public static void NotNull(Expression> reference, T value)
+ {
+ if (value == null)
+ throw new ArgumentNullException(GetParameterName(reference), "Parameter cannot be null.");
+ }
+
+ ///
+ /// Ensures the given string is not null or empty.
+ /// Throws in the first case, or
+ /// in the latter.
+ ///
+ /// The is null or an empty string.
+ public static void NotNullOrEmpty(Expression> reference, string value)
+ {
+ NotNull(reference, value);
+ if (value.Length == 0)
+ throw new ArgumentException("Parameter cannot be empty.", GetParameterName(reference));
+ }
+
+ ///
+ /// Ensures the given string is valid according
+ /// to the function. Throws
+ /// otherwise.
+ ///
+ /// The is not valid according
+ /// to the function.
+ public static void IsValid(Expression> reference, T value, Func validate, string message)
+ {
+ if (!validate(value))
+ throw new ArgumentException(message, GetParameterName(reference));
+ }
+
+ ///
+ /// Ensures the given string is valid according
+ /// to the function. Throws
+ /// otherwise.
+ ///
+ /// The is not valid according
+ /// to the function.
+ public static void IsValid(Expression> reference, T value, Func validate, string format, params object[] args)
+ {
+ if (!validate(value))
+ throw new ArgumentException(string.Format(format, args), GetParameterName(reference));
+ }
+
+ private static string GetParameterName(Expression reference)
+ {
+ var lambda = reference as LambdaExpression;
+ var member = lambda.Body as MemberExpression;
+
+ return member.Member.Name;
+ }
}
\ No newline at end of file
diff --git a/Rebus.AdoNet/packages.config b/Rebus.AdoNet/packages.config
index e81bee4..d2e5194 100755
--- a/Rebus.AdoNet/packages.config
+++ b/Rebus.AdoNet/packages.config
@@ -1,8 +1,8 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
\ No newline at end of file
From 6f1674524794beeed4f27583fc5a05c7b8598ab4 Mon Sep 17 00:00:00 2001
From: Juanje
Date: Mon, 18 Jul 2022 13:58:40 +0200
Subject: [PATCH 6/8] Added some coverage for schema creation like indexes
created or dbtypes.. and added more coverage for main/strange properties
being in use as correlations.
Additionally, I've modified some stuff related to gitattributes behaviour and removed some whitespaces etc.
---
Rebus.AdoNet.Tests/DatabaseFixtureBase.cs | 55 ++++
Rebus.AdoNet.Tests/SagaPersisterTests.cs | 295 ++++++++++++++------
Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs | 25 +-
Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs | 23 +-
Rebus.AdoNet/Dialects/SqlDialect.cs | 33 ++-
Rebus.AdoNet/Dialects/TypeNames.cs | 12 +-
6 files changed, 325 insertions(+), 118 deletions(-)
diff --git a/Rebus.AdoNet.Tests/DatabaseFixtureBase.cs b/Rebus.AdoNet.Tests/DatabaseFixtureBase.cs
index 0771e42..a13c9b9 100644
--- a/Rebus.AdoNet.Tests/DatabaseFixtureBase.cs
+++ b/Rebus.AdoNet.Tests/DatabaseFixtureBase.cs
@@ -108,6 +108,61 @@ protected IEnumerable GetTableNames()
}
}
+ ///
+ /// Fetch the column names with their data types..
+ /// where the Tuple.Item1 is the column name and the Tuple.Item2 is the data type.
+ ///
+ protected IEnumerable> GetColumnSchemaFor(string tableName)
+ {
+ using (var connection = Factory.CreateConnection())
+ {
+ connection.ConnectionString = ConnectionString;
+ connection.Open();
+
+ // XXX: In order, to retrieve the schema information we can specify
+ // the catalog (0), schema (1), table name (2) and column name (3).
+ var restrictions = new string[4];
+ restrictions[2] = tableName;
+
+ var data = new List>();
+ var schemas = connection.GetSchema("Columns", restrictions);
+
+ foreach (DataRow row in schemas.Rows)
+ {
+ var name = row["COLUMN_NAME"] as string;
+ var type = row["DATA_TYPE"] as string;
+ data.Add(Tuple.Create(name, type));
+ }
+
+ return data.ToArray();
+ }
+ }
+
+ ///
+ /// Retrieve table's indexes for a specific table.
+ ///
+ protected IEnumerable GetIndexesFor(string tableName)
+ {
+ using (var connection = Factory.CreateConnection())
+ {
+ connection.ConnectionString = ConnectionString;
+ connection.Open();
+
+ // XXX: In order, to retrieve the schema information we can specify
+ // the catalog (0), schema (1), table name (2) and column name (3).
+ var restrictions = new string[4];
+ restrictions[2] = tableName;
+
+ var data = new List();
+ var schemas = connection.GetSchema("Indexes", restrictions);
+
+ foreach (DataRow row in schemas.Rows)
+ data.Add(row["INDEX_NAME"] as string);
+
+ return data.ToArray();
+ }
+ }
+
protected void DropTable(string tableName)
{
if (!GetTableNames().Contains(tableName, StringComparer.InvariantCultureIgnoreCase)) return;
diff --git a/Rebus.AdoNet.Tests/SagaPersisterTests.cs b/Rebus.AdoNet.Tests/SagaPersisterTests.cs
index 1aeb9b1..6e2d7a2 100644
--- a/Rebus.AdoNet.Tests/SagaPersisterTests.cs
+++ b/Rebus.AdoNet.Tests/SagaPersisterTests.cs
@@ -2,6 +2,7 @@
using System.IO;
using System.Data;
using System.Linq;
+using System.Collections;
using System.Collections.Generic;
using System.Reflection;
@@ -109,6 +110,60 @@ class IEnumerableSagaData : ISagaData
public int Revision { get; set; }
}
+ private class ComplexSaga : ISagaData
+ {
+ #region Inner Types
+
+ public enum Values
+ {
+ Unknown = 0,
+ Valid
+ }
+
+ [Flags]
+ public enum Flags
+ {
+ Unknown = 1 << 0,
+ One = 1 << 1,
+ Two = 1 << 2
+ }
+
+ #endregion
+
+ #region Properties
+
+ public Guid Id { get; set; }
+ public int Revision { get; set; }
+
+ public Tuple Tuple { get; set; }
+ public Guid Uuid { get; set; }
+ public char Char { get; set; }
+ public string Text { get; set; }
+ public bool Bool { get; set; }
+ public sbyte SByte { get; set; }
+ public byte Byte { get; set; }
+ public ushort UShort { get; set; }
+ public short Short { get; set; }
+ public uint UInt { get; set; }
+ public int Int { get; set; }
+ public ulong ULong { get; set; }
+ public long Long { get; set; }
+ public float Float { get; set; }
+ public double Double { get; set; }
+ public decimal Decimal { get; set; }
+ public DateTime Date { get; set; }
+ public TimeSpan Time { get; set; }
+ public Values Enum { get; set; }
+ public Flags EnumFlags { get; set; }
+ public object Object { get; set; }
+ public IEnumerable Strings { get; set; }
+ public IEnumerable Decimals { get; set; }
+ public IEnumerable Objects { get; set; }
+ public IDictionary Bag { get; set; }
+
+ #endregion
+ }
+
[Flags]
public enum Feature
{
@@ -122,12 +177,18 @@ public enum Feature
private static readonly ILog _Log = LogManager.GetLogger();
+ private static readonly IDictionary _correlations = typeof(ComplexSaga).GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => x.Name != nameof(ISagaData.Id) && x.Name != nameof(ISagaData.Revision))
+ .ToDictionary(x => x.Name, x => x);
+
private AdoNetConnectionFactory _factory;
private AdoNetUnitOfWorkManager _manager;
- private readonly Feature _features;
+ private readonly Feature _features;
private const string SagaTableName = "Sagas";
private const string SagaIndexTableName = "SagasIndex";
+ private AdoNetSagaPersister _persister;
+
public SagaPersisterTests(string provider, string connectionString, Feature features)
: base(provider, connectionString)
{
@@ -195,7 +256,7 @@ protected AdoNetSagaPersister CreatePersister(bool basic = false)
var result = _features.HasFlag(Feature.Json)
? new AdoNetSagaPersisterAdvanced(_manager, SagaTableName) as AdoNetSagaPersister
: new AdoNetSagaPersisterLegacy(_manager, SagaTableName, SagaIndexTableName);
-
+
if (!basic)
{
if (_features.HasFlag(Feature.Arrays)) result.UseSqlArraysForCorrelationIndexes();
@@ -203,6 +264,7 @@ protected AdoNetSagaPersister CreatePersister(bool basic = false)
}
return result;
}
+
protected void DropSagaTables()
{
try
@@ -221,7 +283,7 @@ protected void DropSagaTables()
{
}
}
-
+
[OneTimeSetUp]
public void OneTimeSetup()
{
@@ -236,17 +298,16 @@ public void OneTimeSetup()
ExecuteCommand("CREATE EXTENSION IF NOT EXISTS btree_gin;");
}
- var persister = CreatePersister();
- persister.EnsureTablesAreCreated();
+ _persister = CreatePersister();
+ _persister.EnsureTablesAreCreated();
}
-
[SetUp]
public new void SetUp()
{
if (!_features.HasFlag(Feature.Json)) ExecuteCommand($"DELETE FROM \"{SagaIndexTableName}\";");
ExecuteCommand($"DELETE FROM \"{SagaTableName}\";");
-
+
Disposables.TrackDisposable(EstablishMessageContext()); //< Initial (fake) message under each test will run.
}
@@ -255,11 +316,10 @@ public new void SetUp()
[Test]
public void InsertDoesPersistSagaData()
{
- var persister = CreatePersister();
var propertyName = Reflect.Path(d => d.PropertyThatCanBeNull);
var dataWithIndexedNullProperty = new SomePieceOfSagaData { SomeValueWeCanRecognize = "hello" };
- persister.Insert(dataWithIndexedNullProperty, new[] { propertyName });
+ _persister.Insert(dataWithIndexedNullProperty, new[] { propertyName });
var count = ExecuteScalar(string.Format("SELECT COUNT(*) FROM {0}", Dialect.QuoteForTableName(SagaTableName)));
@@ -269,7 +329,7 @@ public void InsertDoesPersistSagaData()
[Test]
public void WhenIgnoringNullProperties_DoesNotSaveNullPropertiesOnUpdate()
{
- var persister = CreatePersister();
+ var persister = _persister;
persister.DoNotIndexNullProperties();
@@ -299,7 +359,7 @@ public void WhenIgnoringNullProperties_DoesNotSaveNullPropertiesOnUpdate()
[Test]
public void WhenIgnoringNullProperties_DoesNotSaveNullPropertiesOnInsert()
{
- var persister = CreatePersister();
+ var persister = _persister;
persister.DoNotIndexNullProperties();
@@ -339,43 +399,129 @@ public void WhenIgnoringNullProperties_DoesNotSaveNullPropertiesOnInsert()
[Test]
public void CanCreateSagaTablesAutomatically()
{
- // arrange
- var persister = CreatePersister();
-
- // act
- persister.EnsureTablesAreCreated();
+ var tableNames = GetTableNames();
+ Assert.That(tableNames, Contains.Item(SagaTableName), "Missing main saga table?!");
- // assert
- var existingTables = GetTableNames();
- Assert.That(existingTables, Contains.Item(SagaTableName));
-
- if (persister is AdoNetSagaPersisterLegacy)
- Assert.That(existingTables, Contains.Item(SagaIndexTableName));
-
- // FIXME: Additional asserts depending on each case
- // - Ensure index columns are text/array/json..
- // - Ensure right indexes were created..
+ var schema = GetColumnSchemaFor(SagaTableName); //< (item1: COLUMN_NAME, item2: DATA_TYPE, [...?])
+ var indexes = GetIndexesFor(SagaTableName);
+ Assert.Multiple(() =>
+ {
+ Assert.That(Dialect.GetDbTypeFor(schema.First(x => x.Item1 == "id").Item2), Is.EqualTo(DbType.Guid), "#0.0");
+ Assert.That(Dialect.GetDbTypeFor(schema.First(x => x.Item1 == "revision").Item2), Is.EqualTo(DbType.Int32), "#0.1");
+ Assert.That(schema.First(x => x.Item1 == "saga_type").Item2, Is.EqualTo("varchar").Or.EqualTo("text"), "#0.2");
+ Assert.That(schema.First(x => x.Item1 == "data").Item2, Is.EqualTo("varchar").Or.EqualTo("text"), "#0.3");
+ Assert.That(indexes, Contains.Item($"ix_{SagaTableName}_id_saga_type"), "#0.4");
+ });
+
+ if (_persister is AdoNetSagaPersisterLegacy) //< NOTE: DATA_TYPE for PostgresSQL arrays are _text instead of text[].
+ {
+ Assert.Multiple(() =>
+ {
+ var sagaIndexSchema = GetColumnSchemaFor(SagaIndexTableName);
+ var sagaIndexIndexes = GetIndexesFor(SagaIndexTableName);
+ var isArray = _features.HasFlag(Feature.Arrays);
+ Assert.That(tableNames, Contains.Item(SagaIndexTableName), "Missing saga indexes table?!");
+ Assert.That(Dialect.GetDbTypeFor(sagaIndexSchema.First(x => x.Item1 == "saga_id").Item2), Is.EqualTo(DbType.Guid), "#1.0");
+ Assert.That(sagaIndexSchema.First(x => x.Item1 == "key").Item2, Is.EqualTo("varchar").Or.EqualTo("text"), "#1.1");
+ Assert.That(sagaIndexSchema.First(x => x.Item1 == "value").Item2, Is.EqualTo("varchar").Or.EqualTo("text"), "#1.2");
+ Assert.That(sagaIndexSchema.First(x => x.Item1 == "values").Item2, Is.EqualTo(isArray ? "varchar[]" : "varchar").Or.EqualTo(isArray ? "_text" : "text"), "#1.3");
+ Assert.That(sagaIndexIndexes, Contains.Item($"ix_{SagaIndexTableName}_key_value"), "#1.4");
+ Assert.That(sagaIndexIndexes, Contains.Item($"ix_{SagaIndexTableName}_key_values"), "#1.5");
+ });
+ }
+ else if (_persister is AdoNetSagaPersisterAdvanced)
+ {
+ Assert.Multiple(() =>
+ {
+ var correlationsType = schema.First(x => x.Item1 == "correlations").Item2;
+ Assert.That(tableNames, Does.Not.Contains(SagaIndexTableName), "Have we created saga indexes table?!");
+ Assert.That(Dialect.GetDbTypeFor(correlationsType), Is.EqualTo(DbType.Object), "#2.0");
+ Assert.That(correlationsType, Is.EqualTo("jsonb"), "#2.1");
+
+ if (Dialect.SupportsGinIndexes)
+ {
+ var ix = Dialect.SupportsMultiColumnGinIndexes ? $"ix_{SagaTableName}_saga_type_correlations" : $"ix_{SagaTableName}_correlations";
+ Assert.That(indexes, Contains.Item(ix), "#2.2");
+ }
+ });
+ }
+ else //< If someday we add another persister.. we should add coverage for it.
+ {
+ throw new NotSupportedException("Missing coverage for a new persister?!?!");
+ }
}
[Test]
public void DoesntDoAnythingIfTheTablesAreAlreadyThere()
{
- // arrange
- //DropSagaTables();
- var persister = CreatePersister();
- //ExecuteCommand(@"CREATE TABLE """ + SagaTableName + @""" (""id"" INT NOT NULL)");
- //ExecuteCommand(@"CREATE TABLE """ + SagaIndexTableName + @""" (""id"" INT NOT NULL)");
+ Assert.That(() =>
+ {
+ _persister.EnsureTablesAreCreated();
+ _persister.EnsureTablesAreCreated();
+ _persister.EnsureTablesAreCreated();
+ }, Throws.Nothing);
+ }
- // act
+ [Test]
+ public void SagaDataCanBeRecoveredWithDifferentKindOfValuesAsCorrelations()
+ {
+ var data = new ComplexSaga()
+ {
+ Id = Guid.NewGuid(),
+ Tuple = Tuple.Create("z", (object)129310.23D),
+ Uuid = Guid.NewGuid(),
+ Char = '\n',
+ Text = "this is a string w/ spanish characters like EÑE.",
+ Bool = true,
+ SByte = sbyte.MinValue,
+ Byte = byte.MaxValue,
+ UShort = ushort.MinValue,
+ Short = short.MaxValue,
+ UInt = uint.MinValue,
+ Int = int.MaxValue,
+ ULong = ulong.MinValue,
+ Long = long.MaxValue,
+ Float = float.NaN,
+ Double = double.PositiveInfinity,
+ Decimal = 10000.746525344148M,
+ Date = DateTime.UtcNow,
+ Time = TimeSpan.MaxValue,
+ Enum = ComplexSaga.Values.Valid,
+ EnumFlags = ComplexSaga.Flags.One | ComplexSaga.Flags.Two,
+ Object = new { one = 1, two = 2.7878798745M },
+ Strings = new[] { "x", "y" },
+ Decimals = new[] { 0.646584564M, 6.98984564544212M },
+ Objects = new object[] { 'x', 0x1, float.Epsilon },
+ Bag = new Dictionary() { { "a", 1 }, { "b", float.NaN } }
+ };
- // assert
- persister.EnsureTablesAreCreated();
- persister.EnsureTablesAreCreated();
- persister.EnsureTablesAreCreated();
+ _persister.Insert(data, _correlations.Keys.ToArray());
+
+ Assert.Multiple(() =>
+ {
+ foreach (var correlation in _correlations)
+ {
+ var value = correlation.Value.GetValue(data);
+ var recovered = _persister.Find(correlation.Key, value);
+ Assert.That(recovered, Is.Not.Null, "Can't recover saga using correlation: {0}?!", correlation.Key);
+
+ // XXX: If the property is a collection like an array.. we'll do an extra check (looking for the saga using an inner value).
+ // Using a KeyValuePair<,> as correlation is a corner case that we don't want to support.
+ if (value is IEnumerable enumerable && !(value is string) && !(value is IDictionary))
+ {
+ foreach (var property in enumerable)
+ {
+ recovered = _persister.Find(correlation.Key, property);
+ Assert.That(recovered, Is.Not.Null, "Can't recover saga using correlation: {0} - {1}?!", correlation.Key, property);
+ }
+ }
+
+ recovered = _persister.Find(correlation.Key, "None has this correlation");
+ Assert.That(recovered, Is.Null, "Something went wrong using correlation: {0}?!", correlation.Key);
+ }
+ });
}
- // FIXME: Add more tests with different kind of values (Dates, Decimal, Float, etc.), ensuring persisted entries can be found later on.
-
#endregion
#region Advanced Saga Persister tests
@@ -420,7 +566,7 @@ private IEnumerableSagaData EnumerableSagaData(int someNumber, IEnumerable(d => d.PropertyThatCanBeNull);
var dataWithIndexedNullProperty = new SomePieceOfSagaData { Id = Guid.NewGuid(), SomeValueWeCanRecognize = "hello" };
@@ -439,7 +585,7 @@ public void CanFindAndUpdateSagaDataByCorrelationPropertyWithNull()
[Test]
public void PersisterCanFindSagaByPropertiesWithDifferentDataTypes()
{
- var persister = CreatePersister();
+ var persister = _persister;
TestFindSagaByPropertyWithType(persister, "Hello worlds!!");
TestFindSagaByPropertyWithType(persister, 23);
TestFindSagaByPropertyWithType(persister, Guid.NewGuid());
@@ -448,7 +594,7 @@ public void PersisterCanFindSagaByPropertiesWithDifferentDataTypes()
[Test]
public void PersisterCanFindSagaById()
{
- var persister = CreatePersister();
+ var persister = _persister;
var savedSagaData = new MySagaData();
var savedSagaDataId = Guid.NewGuid();
savedSagaData.Id = savedSagaDataId;
@@ -462,7 +608,7 @@ public void PersisterCanFindSagaById()
[Test]
public void PersistsComplexSagaLikeExpected()
{
- var persister = CreatePersister();
+ var persister = _persister;
var sagaDataId = Guid.NewGuid();
var complexPieceOfSagaData =
@@ -496,7 +642,7 @@ public void CanDeleteSaga()
{
const string someStringValue = "whoolala";
- var persister = CreatePersister();
+ var persister = _persister;
var mySagaDataId = Guid.NewGuid();
var mySagaData = new SimpleSagaData
{
@@ -516,7 +662,7 @@ public void CanDeleteSaga()
[Test]
public void CanFindSagaByPropertyValues()
{
- var persister = CreatePersister();
+ var persister = _persister;
persister.Insert(SagaData(1, "some field 1"), new[] { "AnotherField" });
persister.Insert(SagaData(2, "some field 2"), new[] { "AnotherField" });
@@ -535,14 +681,14 @@ public void CanFindSagaByPropertyValues()
[Test]
public void CanFindSagaWithIEnumerableAsCorrelatorId()
{
- var persister = CreatePersister();
+ var persister = _persister;
persister.Insert(EnumerableSagaData(3, new string[] { "Field 1", "Field 2", "Field 3"}), new[] { "AnotherFields" });
var dataViaNonexistentValue = persister.Find("AnotherFields", "non-existent value");
var dataViaNonexistentField = persister.Find("SomeFieldThatDoesNotExist", "doesn't matter");
var mySagaData = persister.Find("AnotherFields", "Field 3");
-
+
Assert.That(dataViaNonexistentField, Is.Null);
Assert.That(dataViaNonexistentValue, Is.Null);
Assert.That(mySagaData, Is.Not.Null);
@@ -552,7 +698,7 @@ public void CanFindSagaWithIEnumerableAsCorrelatorId()
[Test]
public void SamePersisterCanSaveMultipleTypesOfSagaDatas()
{
- var persister = CreatePersister();
+ var persister = _persister;
var sagaId1 = Guid.NewGuid();
var sagaId2 = Guid.NewGuid();
persister.Insert(new SimpleSagaData { Id = sagaId1, SomeString = "Ol�" }, new[] { "Id" });
@@ -571,7 +717,7 @@ public void PersisterCanFindSagaDataWithNestedElements()
{
const string stringValue = "I expect to find something with this string!";
- var persister = CreatePersister();
+ var persister = _persister;
var path = Reflect.Path(d => d.ThisOneIsNested.SomeString);
persister.Insert(new SagaDataWithNestedElement
@@ -608,7 +754,7 @@ public void CanUpdateSaga()
// arrange
const string theValue = "this is just some value";
- var persister = CreatePersister();
+ var persister = _persister;
var firstSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = theValue };
var propertyPath = Reflect.Path(s => s.SomeCorrelationId);
@@ -626,7 +772,7 @@ public void CanUpdateSaga()
public void CannotInsertAnotherSagaWithDuplicateCorrelationId()
{
// arrange
- var persister = CreatePersister();
+ var persister = _persister;
var theValue = "this just happens to be the same in two sagas";
var firstSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = theValue };
var secondSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = theValue };
@@ -648,8 +794,8 @@ public void CannotInsertAnotherSagaWithDuplicateCorrelationId()
[Test]
public void CannotUpdateAnotherSagaWithDuplicateCorrelationId()
{
- // arrange
- var persister = CreatePersister();
+ // arrange
+ var persister = _persister;
var theValue = "this just happens to be the same in two sagas";
var firstSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = theValue };
var secondSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = "other value" };
@@ -675,7 +821,7 @@ public void CannotUpdateAnotherSagaWithDuplicateCorrelationId()
public void CanInsertAnotherSagaWithDuplicateCorrelationId()
{
// arrange
- var persister = CreatePersister();
+ var persister = _persister;
var theValue = "this just happens to be the same in two sagas";
var firstSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = theValue };
var secondSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = theValue };
@@ -698,8 +844,8 @@ public void CanInsertAnotherSagaWithDuplicateCorrelationId()
[Test]
public void CanUpdateAnotherSagaWithDuplicateCorrelationId()
{
- // arrange
- var persister = CreatePersister();
+ // arrange
+ var persister = _persister;
var theValue = "this just happens to be the same in two sagas";
var firstSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = theValue };
var secondSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = "other value" };
@@ -725,7 +871,7 @@ public void CanUpdateAnotherSagaWithDuplicateCorrelationId()
[Test]
public void EnsuresUniquenessAlsoOnCorrelationPropertyWithNull()
{
- var persister = CreatePersister();
+ var persister = _persister;
var propertyName = Reflect.Path(d => d.PropertyThatCanBeNull);
var dataWithIndexedNullProperty = new SomePieceOfSagaData { Id = Guid.NewGuid(), SomeValueWeCanRecognize = "hello" };
var anotherPieceOfDataWithIndexedNullProperty = new SomePieceOfSagaData { Id = Guid.NewGuid(), SomeValueWeCanRecognize = "hello" };
@@ -745,16 +891,16 @@ public void EnsuresUniquenessAlsoOnCorrelationPropertyWithNull()
[Test]
public void UsesOptimisticLockingAndDetectsRaceConditionsWhenUpdatingFindingBySomeProperty()
{
- var persister = CreatePersister();
+ var persister = _persister;
var indexBySomeString = new[] { "SomeString" };
var id = Guid.NewGuid();
var simpleSagaData = new SimpleSagaData { Id = id, SomeString = "hello world!" };
persister.Insert(simpleSagaData, indexBySomeString);
var sagaData1 = persister.Find("SomeString", "hello world!");
-
+
Assert.That(sagaData1, Is.Not.Null);
-
+
sagaData1.SomeString = "I changed this on one worker";
using (EnterAFakeMessageContext())
@@ -770,7 +916,7 @@ public void UsesOptimisticLockingAndDetectsRaceConditionsWhenUpdatingFindingBySo
[Test]
public void UsesOptimisticLockingAndDetectsRaceConditionsWhenUpdatingFindingById()
{
- var persister = CreatePersister();
+ var persister = _persister;
var indexBySomeString = new[] { "Id" };
var id = Guid.NewGuid();
var simpleSagaData = new SimpleSagaData { Id = id, SomeString = "hello world!" };
@@ -792,7 +938,7 @@ public void UsesOptimisticLockingAndDetectsRaceConditionsWhenUpdatingFindingById
[Test]
public void ConcurrentDeleteAndUpdateThrowsOnUpdate()
{
- var persister = CreatePersister();
+ var persister = _persister;
var indexBySomeString = new[] { "Id" };
var id = Guid.NewGuid();
var simpleSagaData = new SimpleSagaData { Id = id };
@@ -813,7 +959,7 @@ public void ConcurrentDeleteAndUpdateThrowsOnUpdate()
[Test]
public void ConcurrentDeleteAndUpdateThrowsOnDelete()
{
- var persister = CreatePersister();
+ var persister = _persister;
var indexBySomeString = new[] { "Id" };
var id = Guid.NewGuid();
var simpleSagaData = new SimpleSagaData { Id = id };
@@ -835,7 +981,7 @@ public void ConcurrentDeleteAndUpdateThrowsOnDelete()
public void InsertingTheSameSagaDataTwiceGeneratesAnError()
{
// arrange
- var persister = CreatePersister();
+ var persister = _persister;
var sagaDataPropertyPathsToIndex = new[] { Reflect.Path(d => d.Id) };
var sagaId = Guid.NewGuid();
@@ -856,7 +1002,7 @@ public void InsertingTheSameSagaDataTwiceGeneratesAnError()
[Test]
public void CanInsertTwoSagasUnderASingleUoW()
{
- var persister = CreatePersister();
+ var persister = _persister;
var sagaId1 = Guid.NewGuid();
var sagaId2 = Guid.NewGuid();
@@ -881,7 +1027,7 @@ public void CanInsertTwoSagasUnderASingleUoW()
[Test]
public void NoChangesAreMadeWhenUoWIsNotCommitted()
{
- var persister = CreatePersister();
+ var persister = _persister;
var sagaId1 = Guid.NewGuid();
var sagaId2 = Guid.NewGuid();
@@ -916,7 +1062,7 @@ public void FindSameSagaTwiceThrowsOnNoWait()
return;
}
- var persister = CreatePersister();
+ var persister = _persister;
var savedSagaData = new MySagaData();
var savedSagaDataId = Guid.NewGuid();
savedSagaData.Id = savedSagaDataId;
@@ -929,19 +1075,10 @@ public void FindSameSagaTwiceThrowsOnNoWait()
Assert.Throws(() =>
{
- //using (var thread = new CrossThreadRunner(() =>
- //{
- using (EnterAFakeMessageContext())
- //using (_manager.Create())
- //using (_manager.GetScope(autocomplete: true))
- {
- //_manager.GetScope().Connection.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ COMMITTED;");
- persister.Find("Id", savedSagaDataId);
- }
- //}))
- //{
- // thread.Run();
- //}
+ using (EnterAFakeMessageContext())
+ {
+ persister.Find("Id", savedSagaDataId);
+ }
});
}
}
diff --git a/Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs b/Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs
index b120bb4..17e54f0 100644
--- a/Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs
+++ b/Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs
@@ -19,7 +19,7 @@ namespace Rebus.AdoNet
{
///
/// Implements a saga persister for Rebus that stores sagas using an AdoNet provider.
- /// This is an advanced implementation using single-table scheme for saga & indexes.
+ /// This is an advanced implementation using single-table scheme for saga & indexes.
///
public class AdoNetSagaPersisterAdvanced : AdoNetSagaPersister, IStoreSagaData, AdoNetSagaPersisterFluentConfigurer, ICanUpdateMultipleSagaDatasAtomically
{
@@ -30,7 +30,8 @@ public class AdoNetSagaPersisterAdvanced : AdoNetSagaPersister, IStoreSagaData,
private const string SAGA_REVISION_COLUMN = "revision";
private const string SAGA_CORRELATIONS_COLUMN = "correlations";
private static ILog log;
- // FIXME? Maybe we should implement our own micro-serialization logic, so we can control actual conversions.
+
+ // TODO?: Maybe we should implement our own micro-serialization logic, so we can control actual conversions.
// I am thinking for example on issues with preccision on decimals, etc. (pruiz)
private static readonly JsonSerializerSettings IndexSerializerSettings = new JsonSerializerSettings {
Culture = CultureInfo.InvariantCulture,
@@ -40,7 +41,7 @@ public class AdoNetSagaPersisterAdvanced : AdoNetSagaPersister, IStoreSagaData,
new StringEnumConverter()
}
};
-
+
private readonly AdoNetUnitOfWorkManager manager;
private readonly string sagasTableName;
private readonly string idPropertyName;
@@ -86,13 +87,13 @@ public override AdoNetSagaPersisterFluentConfigurer EnsureTablesAreCreated()
log.Debug("Table '{0}' already exists.", sagasTableName);
return this;
}
-
+
if (UseSqlArrays /* && !dialect.SupportsArrayTypes*/)
{
throw new ApplicationException("Enabled UseSqlArraysForCorrelationIndexes AdoNetSagaPersister selecte does not support arrays?!");
//throw new ApplicationException("Enabled UseSqlArraysForCorrelationIndexes but underlaying database does not support arrays?!");
}
-
+
var indexes = new[] {
new AdoNetIndex() {
Name = $"ix_{sagasTableName}_{SAGA_ID_COLUMN}_{SAGA_TYPE_COLUMN}",
@@ -119,7 +120,7 @@ public override AdoNetSagaPersisterFluentConfigurer EnsureTablesAreCreated()
});
}
}
-
+
using (var command = connection.CreateCommand())
{
command.CommandText = scope.Dialect.FormatCreateTable(
@@ -237,7 +238,7 @@ public override void Update(ISagaData sagaData, string[] sagaDataPropertyPathsTo
scope.Complete();
}
}
-
+
public override void Delete(ISagaData sagaData)
{
using (var scope = manager.GetScope())
@@ -358,7 +359,11 @@ protected override string Fetch(string sagaDataPropertyPath, object f
s.{sagaCorrelationsCol} @> {dialect.Cast(sagaCorrelationsValuesParam, DbType.Object)}
)
{forUpdate};".Replace("\t", "");
-
+
+ var type = fieldFromMessage?.GetType();
+ if (type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
+ throw new NotSupportedException("Using a KeyValuePair<,> as correlation is not supported.");
+
var value = SerializeCorrelations(new Dictionary() { { sagaDataPropertyPath, fieldFromMessage } });
var values = SerializeCorrelations(new Dictionary() { { sagaDataPropertyPath, new[] { fieldFromMessage } } });
@@ -366,7 +371,7 @@ protected override string Fetch(string sagaDataPropertyPath, object f
command.AddParameter(sagaCorrelationsValueParam, DbType.String, value);
command.AddParameter(sagaCorrelationsValuesParam, DbType.String, values);
}
-
+
try
{
log.Debug("Finding saga of type {0} with {1} = {2}\n{3}", sagaType, sagaDataPropertyPath,
@@ -387,7 +392,7 @@ protected override string Fetch(string sagaDataPropertyPath, object f
}
}
}
-
+
#endregion
}
}
diff --git a/Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs b/Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs
index 1bb2c3d..7ec6def 100644
--- a/Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs
+++ b/Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs
@@ -36,7 +36,7 @@ public class AdoNetSagaPersisterLegacy : AdoNetSagaPersister, IStoreSagaData, Ad
private readonly string sagasIndexTableName;
private readonly string sagasTableName;
private readonly string idPropertyName;
-
+
static AdoNetSagaPersisterLegacy()
{
RebusLoggerFactory.Changed += f => log = f.GetCurrentClassLogger();
@@ -93,7 +93,7 @@ public override AdoNetSagaPersisterFluentConfigurer EnsureTablesAreCreated()
}
log.Info("Tables '{0}' and '{1}' do not exist - they will be created now", sagasTableName, sagasIndexTableName);
-
+
using (var command = connection.CreateCommand())
{
command.CommandText = scope.Dialect.FormatCreateTable(
@@ -147,7 +147,7 @@ public override AdoNetSagaPersisterFluentConfigurer EnsureTablesAreCreated()
}
}
);
-
+
command.ExecuteNonQuery();
}
@@ -157,7 +157,7 @@ public override AdoNetSagaPersisterFluentConfigurer EnsureTablesAreCreated()
return this;
}
-
+
#endregion
public override void Insert(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
@@ -350,7 +350,7 @@ private void DeclareIndexUsingReturningClause(ISagaData sagaData, AdoNetUnitOfWo
dialect.EscapeParameter(p.PropertyValueParameter),
dialect.EscapeParameter(p.PropertyValuesParameter)
));
-
+
using (var command = connection.CreateCommand())
{
command.CommandText = string.Format(
@@ -462,7 +462,6 @@ private void DeclareIndexUnoptimized(ISagaData sagaData, AdoNetUnitOfWorkScope s
}
catch (DbException exception)
{
- // FIXME: Ensure exception is the right one..
throw new OptimisticLockingException(sagaData, exception);
}
}
@@ -473,7 +472,7 @@ private void DeclareIndexUnoptimized(ISagaData sagaData, AdoNetUnitOfWorkScope s
using (var command = connection.CreateCommand())
{
command.CommandText = string.Format(
- "UPDATE {0} SET {1} = {2}, {3} = {4} " +
+ "UPDATE {0} SET {1} = {2}, {3} = {4} " +
"WHERE {5} = {6} AND {7} = {8};",
dialect.QuoteForTableName(sagasIndexTableName), //< 0
dialect.QuoteForColumnName(SAGAINDEX_VALUE_COLUMN), //< 1
@@ -675,7 +674,7 @@ private string GetSagaLockingClause(SqlDialect dialect)
return string.Empty;
}
-
+
private bool ArraysEnabledFor(SqlDialect dialect)
{
return UseSqlArrays && dialect.SupportsArrayTypes;
@@ -744,7 +743,7 @@ private static string GetConcatenatedIndexValues(IEnumerable values)
return sb.ToString();
}
- protected override string Fetch(string sagaDataPropertyPath, object fieldFromMessage)
+ protected override string Fetch(string sagaDataPropertyPath, object fieldFromMessage)
{
using (var scope = Manager.GetScope(autocomplete: true))
{
@@ -760,7 +759,7 @@ protected override string Fetch(string sagaDataPropertyPath, object f
if (UseNoWaitSagaLocking && !dialect.SupportsSelectForWithNoWait)
throw new InvalidOperationException($"You can't use saga locking with no-wait for a Dialect {dialect.GetType()} that does not supports no-wait clause.");
}
-
+
using (var command = connection.CreateCommand())
{
if (sagaDataPropertyPath == idPropertyName)
@@ -801,7 +800,7 @@ protected override string Fetch(string sagaDataPropertyPath, object f
var valuesPredicate = ArraysEnabledFor(dialect)
? dialect.FormatArrayAny($"i.{indexValuesCol}", indexValuesParm)
: $"(i.{indexValuesCol} LIKE ('%' || {indexValuesParm} || '%'))";
-
+
command.CommandText = $@"
SELECT s.{dataCol}
FROM {sagaTblName} s
@@ -819,7 +818,7 @@ protected override string Fetch(string sagaDataPropertyPath, object f
END
)
{forUpdate};".Replace("\t", "");
-
+
var value = GetIndexValue(fieldFromMessage);
var values = value == null ? DBNull.Value : ArraysEnabledFor(dialect)
? (object)(new[] { value })
diff --git a/Rebus.AdoNet/Dialects/SqlDialect.cs b/Rebus.AdoNet/Dialects/SqlDialect.cs
index a3bab0a..16dbca9 100644
--- a/Rebus.AdoNet/Dialects/SqlDialect.cs
+++ b/Rebus.AdoNet/Dialects/SqlDialect.cs
@@ -139,6 +139,11 @@ public virtual string GetLongestTypeName(DbType dbType)
return _typeNames.GetLongest(dbType);
}
+ ///
+ /// Reverse search for *testing purpose* in order to get (for each dialect) the related DBType..
+ ///
+ internal virtual DbType GetDbTypeFor(string name) => _typeNames.Defaults.First(x => x.Value.ToLowerInvariant() == name.ToLowerInvariant()).Key;
+
#endregion
#region Identifier quoting support
@@ -201,14 +206,14 @@ public virtual string Qualify(string catalog, string schema, string table)
///
/// This method assumes that the name is not already Quoted. So if the name passed
/// in is "name then it will return """name" . It escapes the first char
- /// - the " with "" and encloses the escaped string with OpenQuote and CloseQuote.
+ /// - the " with "" and encloses the escaped string with OpenQuote and CloseQuote.
///
///
protected virtual string Quote(string name)
{
string quotedName = name.Replace(OpenQuote.ToString(), new string(OpenQuote, 2));
- // in some dbs the Open and Close Quote are the same chars - if they are
+ // in some dbs the Open and Close Quote are the same chars - if they are
// then we don't have to escape the Close Quote char because we already
// got it.
if (OpenQuote != CloseQuote)
@@ -226,7 +231,7 @@ protected virtual string Quote(string name)
/// Unquoted string
///
///
- /// This method checks the string quoted to see if it is
+ /// This method checks the string quoted to see if it is
/// quoted. If the string quoted is already enclosed in the OpenQuote
/// and CloseQuote then those chars are removed.
///
@@ -239,7 +244,7 @@ protected virtual string Quote(string name)
/// The following quoted values return these results
/// "quoted" = quoted
/// "quote""d" = quote"d
- /// quote""d = quote"d
+ /// quote""d = quote"d
///
///
/// If this implementation is not sufficient for your Dialect then it needs to be overridden.
@@ -299,9 +304,9 @@ public virtual string[] UnQuote(string[] quoted)
/// A Quoted name in the format of OpenQuote + aliasName + CloseQuote
///
///
- /// If the aliasName is already enclosed in the OpenQuote and CloseQuote then this
+ /// If the aliasName is already enclosed in the OpenQuote and CloseQuote then this
/// method will return the aliasName that was passed in without going through any
- /// Quoting process. So if aliasName is passed in already Quoted make sure that
+ /// Quoting process. So if aliasName is passed in already Quoted make sure that
/// you have escaped all of the chars according to your DataBase's specifications.
///
///
@@ -318,9 +323,9 @@ public virtual string QuoteForAliasName(string aliasName)
/// A Quoted name in the format of OpenQuote + columnName + CloseQuote
///
///
- /// If the columnName is already enclosed in the OpenQuote and CloseQuote then this
+ /// If the columnName is already enclosed in the OpenQuote and CloseQuote then this
/// method will return the columnName that was passed in without going through any
- /// Quoting process. So if columnName is passed in already Quoted make sure that
+ /// Quoting process. So if columnName is passed in already Quoted make sure that
/// you have escaped all of the chars according to your DataBase's specifications.
///
///
@@ -336,9 +341,9 @@ public virtual string QuoteForColumnName(string columnName)
/// A Quoted name in the format of OpenQuote + tableName + CloseQuote
///
///
- /// If the tableName is already enclosed in the OpenQuote and CloseQuote then this
+ /// If the tableName is already enclosed in the OpenQuote and CloseQuote then this
/// method will return the tableName that was passed in without going through any
- /// Quoting process. So if tableName is passed in already Quoted make sure that
+ /// Quoting process. So if tableName is passed in already Quoted make sure that
/// you have escaped all of the chars according to your DataBase's specifications.
///
///
@@ -348,7 +353,7 @@ public virtual string QuoteForTableName(string tableName)
}
///
- /// Casts an SQL expression to db's DbType equivalent.
+ /// Casts an SQL expression to db's DbType equivalent.
///
///
///
@@ -481,7 +486,7 @@ public virtual string FormatCreateIndex(AdoNetTable table, AdoNetIndex index)
{
columns = columns.Select(x => QuoteForColumnName(x)).ToArray();
}
-
+
return string.Format("CREATE INDEX {0} ON {1} {2} ({3});",
!string.IsNullOrEmpty(index.Name) ? QuoteForTableName(index.Name) : "",
QuoteForTableName(table.Name),
@@ -512,7 +517,7 @@ public virtual bool IsDuplicateKeyException(DbException ex)
throw new NotImplementedException("IsDuplicateKeyException not implemented for this dialect!");
}
#endregion
-
+
#region AdvisoryLockFunctions
public virtual bool SupportsTryAdvisoryLockFunction => false;
public virtual bool SupportsTryAdvisoryXactLockFunction => false;
@@ -548,7 +553,7 @@ public virtual string FormatArrayAny(string arg1, string arg2)
throw new NotSupportedException("ArrayAny function not supported by this dialect.");
}
#endregion
-
+
#region GIN Indexing
public virtual bool SupportsGinIndexes => false;
diff --git a/Rebus.AdoNet/Dialects/TypeNames.cs b/Rebus.AdoNet/Dialects/TypeNames.cs
index aa32c0d..5cca450 100755
--- a/Rebus.AdoNet/Dialects/TypeNames.cs
+++ b/Rebus.AdoNet/Dialects/TypeNames.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Data;
using System.Text;
@@ -33,7 +34,7 @@ namespace Rebus.AdoNet.Dialects
/// would result in
///
/// Names.Get(DbType) // --> "VARCHAR($l)" (will cause trouble)
- /// Names.Get(DbType,100) // --> "VARCHAR(100)"
+ /// Names.Get(DbType,100) // --> "VARCHAR(100)"
/// Names.Get(DbType,1000) // --> "VARCHAR(1000)"
/// Names.Get(DbType,10000) // --> "VARCHAR(10000)"
///
@@ -48,7 +49,12 @@ public class TypeNames
private readonly Dictionary defaults = new Dictionary();
///
- ///
+ /// Expose defaults as a readonly collection for testing purpose..
+ ///
+ internal IReadOnlyDictionary Defaults => new ReadOnlyDictionary(defaults);
+
+ ///
+ ///
///
///
///
@@ -164,7 +170,7 @@ public void Put(DbType typecode, uint capacity, string value)
}
///
- ///
+ ///
///
///
///
From 43ebfd154d0434217f5ee8039092067fea6f2762 Mon Sep 17 00:00:00 2001
From: Juanje
Date: Tue, 19 Jul 2022 13:55:19 +0200
Subject: [PATCH 7/8] Requested changes..
---
.gitattributes | 6 +-
Rebus.AdoNet.Tests/App.config | 22 +-
Rebus.AdoNet.Tests/DatabaseFixtureBase.cs | 30 +-
Rebus.AdoNet.Tests/Properties/AssemblyInfo.cs | 74 +-
Rebus.AdoNet.Tests/SagaPersisterTests.cs | 379 ++--
Rebus.AdoNet.Tests/packages.config | 22 +-
Rebus.AdoNet.sln | 54 +-
Rebus.AdoNet/AdoNetSagaPersister.cs | 15 +-
Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs | 805 ++++----
Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs | 1705 +++++++++--------
Rebus.AdoNet/Dialects/PostgreSql95Dialect.cs | 2 +-
Rebus.AdoNet/Dialects/PostgreSqlDialect.cs | 14 +-
Rebus.AdoNet/Dialects/SqlDialect.cs | 15 +-
Rebus.AdoNet/Dialects/SqliteDialect.cs | 15 +
Rebus.AdoNet/Dialects/TypeNames.cs | 356 ++--
Rebus.AdoNet/Dialects/YugabyteDbDialect.cs | 11 +
.../Facilities/IDbConnectionExtensions.cs | 63 +
Rebus.AdoNet/Properties/AssemblyInfo.cs | 78 +-
Rebus.AdoNet/Rebus.AdoNet.csproj | 1 +
Rebus.AdoNet/Schema/AdoNetColumn.cs | 44 +-
Rebus.AdoNet/Schema/AdoNetIndex.cs | 60 +-
Rebus.AdoNet/Schema/AdoNetTable.cs | 38 +-
Rebus.AdoNet/netfx/System/Guard.cs | 200 +-
Rebus.AdoNet/packages.config | 14 +-
24 files changed, 2069 insertions(+), 1954 deletions(-)
create mode 100644 Rebus.AdoNet/Facilities/IDbConnectionExtensions.cs
diff --git a/.gitattributes b/.gitattributes
index 8b37475..028e9b4 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,8 +1,8 @@
# These files are text and should be normalized (convert crlf => lf)
-*.cs text diff=csharp
+#*.cs text diff=csharp
*.xaml text
*.csproj text
-*.sln text
+#*.sln text
*.tt text
*.ps1 text
*.cmd text
@@ -11,7 +11,7 @@
*.cshtml text
*.html text
*.js text
-*.config text
+#*.config text
# Images should be treated as binary
# (binary is a macro for -text -diff)
diff --git a/Rebus.AdoNet.Tests/App.config b/Rebus.AdoNet.Tests/App.config
index 6ec16dc..df3e459 100644
--- a/Rebus.AdoNet.Tests/App.config
+++ b/Rebus.AdoNet.Tests/App.config
@@ -1,11 +1,11 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Rebus.AdoNet.Tests/DatabaseFixtureBase.cs b/Rebus.AdoNet.Tests/DatabaseFixtureBase.cs
index a13c9b9..4e0f419 100644
--- a/Rebus.AdoNet.Tests/DatabaseFixtureBase.cs
+++ b/Rebus.AdoNet.Tests/DatabaseFixtureBase.cs
@@ -119,22 +119,7 @@ protected IEnumerable GetTableNames()
connection.ConnectionString = ConnectionString;
connection.Open();
- // XXX: In order, to retrieve the schema information we can specify
- // the catalog (0), schema (1), table name (2) and column name (3).
- var restrictions = new string[4];
- restrictions[2] = tableName;
-
- var data = new List>();
- var schemas = connection.GetSchema("Columns", restrictions);
-
- foreach (DataRow row in schemas.Rows)
- {
- var name = row["COLUMN_NAME"] as string;
- var type = row["DATA_TYPE"] as string;
- data.Add(Tuple.Create(name, type));
- }
-
- return data.ToArray();
+ return connection.GetColumnSchemaFor(tableName);
}
}
@@ -148,18 +133,7 @@ protected IEnumerable GetIndexesFor(string tableName)
connection.ConnectionString = ConnectionString;
connection.Open();
- // XXX: In order, to retrieve the schema information we can specify
- // the catalog (0), schema (1), table name (2) and column name (3).
- var restrictions = new string[4];
- restrictions[2] = tableName;
-
- var data = new List();
- var schemas = connection.GetSchema("Indexes", restrictions);
-
- foreach (DataRow row in schemas.Rows)
- data.Add(row["INDEX_NAME"] as string);
-
- return data.ToArray();
+ return connection.GetIndexesFor(tableName);
}
}
diff --git a/Rebus.AdoNet.Tests/Properties/AssemblyInfo.cs b/Rebus.AdoNet.Tests/Properties/AssemblyInfo.cs
index 0422338..722b091 100755
--- a/Rebus.AdoNet.Tests/Properties/AssemblyInfo.cs
+++ b/Rebus.AdoNet.Tests/Properties/AssemblyInfo.cs
@@ -1,37 +1,37 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Rebus.AdoNet.Tests")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Rebus.AdoNet.Tests")]
-[assembly: AssemblyCopyright("Copyright © Evidencias Certificadas S.L. (2015~2016)")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("803f12ac-8946-4718-8167-cd27077fde46")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("0.0.0.0")]
-[assembly: AssemblyFileVersion("0.0.0.0")]
-[assembly: AssemblyInformationalVersion("VERSION_STRING")]
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Rebus.AdoNet.Tests")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Rebus.AdoNet.Tests")]
+[assembly: AssemblyCopyright("Copyright © Evidencias Certificadas S.L. (2015~2016)")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("803f12ac-8946-4718-8167-cd27077fde46")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("0.0.0.0")]
+[assembly: AssemblyFileVersion("0.0.0.0")]
+[assembly: AssemblyInformationalVersion("VERSION_STRING")]
diff --git a/Rebus.AdoNet.Tests/SagaPersisterTests.cs b/Rebus.AdoNet.Tests/SagaPersisterTests.cs
index 6e2d7a2..ac64736 100644
--- a/Rebus.AdoNet.Tests/SagaPersisterTests.cs
+++ b/Rebus.AdoNet.Tests/SagaPersisterTests.cs
@@ -54,6 +54,7 @@ class SagaDataWithNestedElement : ISagaData
public Guid Id { get; set; }
public int Revision { get; set; }
public ThisOneIsNested ThisOneIsNested { get; set; }
+ public ThisOneIsNested ThisOneIsNestedToo { get; set; }
}
class ThisOneIsNested
@@ -135,8 +136,8 @@ public enum Flags
public Guid Id { get; set; }
public int Revision { get; set; }
- public Tuple Tuple { get; set; }
public Guid Uuid { get; set; }
+ public Guid? NullableGuid { get; set; }
public char Char { get; set; }
public string Text { get; set; }
public bool Bool { get; set; }
@@ -158,8 +159,6 @@ public enum Flags
public object Object { get; set; }
public IEnumerable Strings { get; set; }
public IEnumerable Decimals { get; set; }
- public IEnumerable Objects { get; set; }
- public IDictionary Bag { get; set; }
#endregion
}
@@ -168,10 +167,10 @@ public enum Flags
public enum Feature
{
None = 0,
- Arrays = (1<<0),
- Json = (1<<1),
- Locking = (1<<2),
- NoWait = (1<<3)
+ Arrays = (1 << 0),
+ Json = (1 << 1),
+ Locking = (1 << 2),
+ NoWait = (1 << 3)
}
#endregion
@@ -239,13 +238,13 @@ public static IEnumerable GetConnectionSources()
var @base = sources.Select(x => new[] { x.Provider, x.ConnectionString, Feature.None });
var extra1 = sources.Where(x => x.Features.HasFlag(Feature.Locking)).Select(x => new[] { x.Provider, x.ConnectionString, (Feature.Locking) });
- var extra2 = sources.Where(x => x.Features.HasFlag(Feature.Locking|Feature.NoWait)).Select(x => new[] { x.Provider, x.ConnectionString, (Feature.Locking|Feature.NoWait) });
+ var extra2 = sources.Where(x => x.Features.HasFlag(Feature.Locking | Feature.NoWait)).Select(x => new[] { x.Provider, x.ConnectionString, (Feature.Locking | Feature.NoWait) });
var extra3 = sources.Where(x => x.Features.HasFlag(Feature.Arrays)).Select(x => new[] { x.Provider, x.ConnectionString, (Feature.Arrays) });
- var extra4 = sources.Where(x => x.Features.HasFlag(Feature.Arrays|Feature.Locking)).Select(x => new[] { x.Provider, x.ConnectionString, (Feature.Arrays|Feature.Locking) });
- var extra5 = sources.Where(x => x.Features.HasFlag(Feature.Arrays|Feature.Locking|Feature.NoWait)).Select(x => new[] { x.Provider, x.ConnectionString, (Feature.Arrays|Feature.Locking|Feature.NoWait) });
+ var extra4 = sources.Where(x => x.Features.HasFlag(Feature.Arrays | Feature.Locking)).Select(x => new[] { x.Provider, x.ConnectionString, (Feature.Arrays | Feature.Locking) });
+ var extra5 = sources.Where(x => x.Features.HasFlag(Feature.Arrays | Feature.Locking | Feature.NoWait)).Select(x => new[] { x.Provider, x.ConnectionString, (Feature.Arrays | Feature.Locking | Feature.NoWait) });
var extra6 = sources.Where(x => x.Features.HasFlag(Feature.Json)).Select(x => new[] { x.Provider, x.ConnectionString, (Feature.Json) });
- var extra7 = sources.Where(x => x.Features.HasFlag(Feature.Json|Feature.Locking)).Select(x => new[] { x.Provider, x.ConnectionString, (Feature.Json|Feature.Locking) });
- var extra8 = sources.Where(x => x.Features.HasFlag(Feature.Json|Feature.Locking|Feature.NoWait)).Select(x => new[] { x.Provider, x.ConnectionString, (Feature.Json|Feature.Locking|Feature.NoWait) });
+ var extra7 = sources.Where(x => x.Features.HasFlag(Feature.Json | Feature.Locking)).Select(x => new[] { x.Provider, x.ConnectionString, (Feature.Json | Feature.Locking) });
+ var extra8 = sources.Where(x => x.Features.HasFlag(Feature.Json | Feature.Locking | Feature.NoWait)).Select(x => new[] { x.Provider, x.ConnectionString, (Feature.Json | Feature.Locking | Feature.NoWait) });
return @base.Union(extra1).Union(extra2).Union(extra3).Union(extra4)
.Union(extra5).Union(extra6).Union(extra7).Union(extra8);
@@ -329,9 +328,7 @@ public void InsertDoesPersistSagaData()
[Test]
public void WhenIgnoringNullProperties_DoesNotSaveNullPropertiesOnUpdate()
{
- var persister = _persister;
-
- persister.DoNotIndexNullProperties();
+ _persister.DoNotIndexNullProperties();
const string correlationProperty1 = "correlation property 1";
const string correlationProperty2 = "correlation property 2";
@@ -344,24 +341,22 @@ public void WhenIgnoringNullProperties_DoesNotSaveNullPropertiesOnUpdate()
var firstPieceOfSagaDataWithNullValueOnProperty = new PieceOfSagaData { SomeProperty = correlationProperty1, AnotherProperty = "random12423" };
var nextPieceOfSagaDataWithNullValueOnProperty = new PieceOfSagaData { SomeProperty = correlationProperty2, AnotherProperty = "random38791387" };
- persister.Insert(firstPieceOfSagaDataWithNullValueOnProperty, correlationPropertyPaths);
- persister.Insert(nextPieceOfSagaDataWithNullValueOnProperty, correlationPropertyPaths);
+ _persister.Insert(firstPieceOfSagaDataWithNullValueOnProperty, correlationPropertyPaths);
+ _persister.Insert(nextPieceOfSagaDataWithNullValueOnProperty, correlationPropertyPaths);
- var firstPiece = persister.Find(Reflect.Path(s => s.SomeProperty), correlationProperty1);
+ var firstPiece = _persister.Find(Reflect.Path(s => s.SomeProperty), correlationProperty1);
firstPiece.AnotherProperty = null;
- persister.Update(firstPiece, correlationPropertyPaths);
+ _persister.Update(firstPiece, correlationPropertyPaths);
- var nextPiece = persister.Find(Reflect.Path(s => s.SomeProperty), correlationProperty2);
+ var nextPiece = _persister.Find(Reflect.Path(s => s.SomeProperty), correlationProperty2);
nextPiece.AnotherProperty = null;
- persister.Update(nextPiece, correlationPropertyPaths);
+ _persister.Update(nextPiece, correlationPropertyPaths);
}
[Test]
public void WhenIgnoringNullProperties_DoesNotSaveNullPropertiesOnInsert()
{
- var persister = _persister;
-
- persister.DoNotIndexNullProperties();
+ _persister.DoNotIndexNullProperties();
const string correlationProperty1 = "correlation property 1";
const string correlationProperty2 = "correlation property 2";
@@ -371,79 +366,72 @@ public void WhenIgnoringNullProperties_DoesNotSaveNullPropertiesOnInsert()
Reflect.Path(s => s.AnotherProperty)
};
- var firstPieceOfSagaDataWithNullValueOnProperty = new PieceOfSagaData
- {
+ var firstPieceOfSagaDataWithNullValueOnProperty = new PieceOfSagaData {
SomeProperty = correlationProperty1
};
- var nextPieceOfSagaDataWithNullValueOnProperty = new PieceOfSagaData
- {
+ var nextPieceOfSagaDataWithNullValueOnProperty = new PieceOfSagaData {
SomeProperty = correlationProperty2
};
var firstId = firstPieceOfSagaDataWithNullValueOnProperty.Id;
var nextId = nextPieceOfSagaDataWithNullValueOnProperty.Id;
- persister.Insert(firstPieceOfSagaDataWithNullValueOnProperty, correlationPropertyPaths);
+ _persister.Insert(firstPieceOfSagaDataWithNullValueOnProperty, correlationPropertyPaths);
// must not throw:
- persister.Insert(nextPieceOfSagaDataWithNullValueOnProperty, correlationPropertyPaths);
+ _persister.Insert(nextPieceOfSagaDataWithNullValueOnProperty, correlationPropertyPaths);
- var firstPiece = persister.Find(Reflect.Path(s => s.SomeProperty), correlationProperty1);
- var nextPiece = persister.Find(Reflect.Path(s => s.SomeProperty), correlationProperty2);
+ var firstPiece = _persister.Find(Reflect.Path(s => s.SomeProperty), correlationProperty1);
+ var nextPiece = _persister.Find(Reflect.Path(s => s.SomeProperty), correlationProperty2);
Assert.That(firstPiece.Id, Is.EqualTo(firstId));
Assert.That(nextPiece.Id, Is.EqualTo(nextId));
}
[Test]
- public void CanCreateSagaTablesAutomatically()
+ public void SchemaIsCreatedAsExpected()
{
var tableNames = GetTableNames();
Assert.That(tableNames, Contains.Item(SagaTableName), "Missing main saga table?!");
var schema = GetColumnSchemaFor(SagaTableName); //< (item1: COLUMN_NAME, item2: DATA_TYPE, [...?])
var indexes = GetIndexesFor(SagaTableName);
- Assert.Multiple(() =>
- {
- Assert.That(Dialect.GetDbTypeFor(schema.First(x => x.Item1 == "id").Item2), Is.EqualTo(DbType.Guid), "#0.0");
- Assert.That(Dialect.GetDbTypeFor(schema.First(x => x.Item1 == "revision").Item2), Is.EqualTo(DbType.Int32), "#0.1");
- Assert.That(schema.First(x => x.Item1 == "saga_type").Item2, Is.EqualTo("varchar").Or.EqualTo("text"), "#0.2");
- Assert.That(schema.First(x => x.Item1 == "data").Item2, Is.EqualTo("varchar").Or.EqualTo("text"), "#0.3");
- Assert.That(indexes, Contains.Item($"ix_{SagaTableName}_id_saga_type"), "#0.4");
- });
+
+ // NOTE: Some 'hacks' for postgres/yuga.. returned column type for strings are varchar(xxx)..
+ // remove the length.
+ var stringDbType = Dialect.GetColumnType(DbType.String).ToLower();
+ if (stringDbType.EndsWith(")"))
+ stringDbType = stringDbType.Substring(0, stringDbType.IndexOf("("));
+
+ Assert.That(schema.First(x => x.Item1 == "id").Item2, Is.EqualTo(Dialect.GetColumnType(DbType.Guid).ToLower()), "#0.0");
+ Assert.That(schema.First(x => x.Item1 == "revision").Item2, Is.EqualTo(Dialect.GetColumnType(DbType.Int32).ToLower()), "#0.1");
+ Assert.That(schema.First(x => x.Item1 == "saga_type").Item2, Is.EqualTo(stringDbType), "#0.2");
+ Assert.That(schema.First(x => x.Item1 == "data").Item2, Is.EqualTo("text"), "#0.3");
+ Assert.That(indexes, Contains.Item($"ix_{SagaTableName}_id_saga_type"), "#0.4");
if (_persister is AdoNetSagaPersisterLegacy) //< NOTE: DATA_TYPE for PostgresSQL arrays are _text instead of text[].
{
- Assert.Multiple(() =>
- {
- var sagaIndexSchema = GetColumnSchemaFor(SagaIndexTableName);
- var sagaIndexIndexes = GetIndexesFor(SagaIndexTableName);
- var isArray = _features.HasFlag(Feature.Arrays);
- Assert.That(tableNames, Contains.Item(SagaIndexTableName), "Missing saga indexes table?!");
- Assert.That(Dialect.GetDbTypeFor(sagaIndexSchema.First(x => x.Item1 == "saga_id").Item2), Is.EqualTo(DbType.Guid), "#1.0");
- Assert.That(sagaIndexSchema.First(x => x.Item1 == "key").Item2, Is.EqualTo("varchar").Or.EqualTo("text"), "#1.1");
- Assert.That(sagaIndexSchema.First(x => x.Item1 == "value").Item2, Is.EqualTo("varchar").Or.EqualTo("text"), "#1.2");
- Assert.That(sagaIndexSchema.First(x => x.Item1 == "values").Item2, Is.EqualTo(isArray ? "varchar[]" : "varchar").Or.EqualTo(isArray ? "_text" : "text"), "#1.3");
- Assert.That(sagaIndexIndexes, Contains.Item($"ix_{SagaIndexTableName}_key_value"), "#1.4");
- Assert.That(sagaIndexIndexes, Contains.Item($"ix_{SagaIndexTableName}_key_values"), "#1.5");
- });
+ var sagaIndexSchema = GetColumnSchemaFor(SagaIndexTableName);
+ var sagaIndexIndexes = GetIndexesFor(SagaIndexTableName);
+ var isArray = _features.HasFlag(Feature.Arrays);
+
+ Assert.That(tableNames, Contains.Item(SagaIndexTableName), "Missing saga indexes table?!");
+ Assert.That(sagaIndexSchema.First(x => x.Item1 == "saga_id").Item2, Is.EqualTo(Dialect.GetColumnType(DbType.Guid).ToLower()), "#1.0");
+ Assert.That(sagaIndexSchema.First(x => x.Item1 == "key").Item2, Is.EqualTo(stringDbType), "#1.1");
+ Assert.That(sagaIndexSchema.First(x => x.Item1 == "value").Item2, Is.EqualTo("text"), "#1.2");
+ Assert.That(sagaIndexSchema.First(x => x.Item1 == "values").Item2, Is.EqualTo(isArray ? "_text" : "text"), "#1.3");
+ Assert.That(sagaIndexIndexes, Contains.Item($"ix_{SagaIndexTableName}_key_value"), "#1.4");
+ Assert.That(sagaIndexIndexes, Contains.Item($"ix_{SagaIndexTableName}_key_values"), "#1.5");
}
else if (_persister is AdoNetSagaPersisterAdvanced)
{
- Assert.Multiple(() =>
- {
- var correlationsType = schema.First(x => x.Item1 == "correlations").Item2;
- Assert.That(tableNames, Does.Not.Contains(SagaIndexTableName), "Have we created saga indexes table?!");
- Assert.That(Dialect.GetDbTypeFor(correlationsType), Is.EqualTo(DbType.Object), "#2.0");
- Assert.That(correlationsType, Is.EqualTo("jsonb"), "#2.1");
+ var correlationsType = schema.First(x => x.Item1 == "correlations").Item2;
+ Assert.That(tableNames, Does.Not.Contains(SagaIndexTableName), "Have we created saga indexes table?!");
+ Assert.That(correlationsType, Is.EqualTo("jsonb"), "#2.0");
- if (Dialect.SupportsGinIndexes)
- {
- var ix = Dialect.SupportsMultiColumnGinIndexes ? $"ix_{SagaTableName}_saga_type_correlations" : $"ix_{SagaTableName}_correlations";
- Assert.That(indexes, Contains.Item(ix), "#2.2");
- }
- });
+ var ix = Dialect.SupportsMultiColumnGinIndexes ? $"ix_{SagaTableName}_saga_type_correlations" : $"ix_{SagaTableName}_correlations";
+ Assert.That(indexes, Contains.Item(ix), "#2.1");
}
else //< If someday we add another persister.. we should add coverage for it.
{
@@ -468,8 +456,8 @@ public void SagaDataCanBeRecoveredWithDifferentKindOfValuesAsCorrelations()
var data = new ComplexSaga()
{
Id = Guid.NewGuid(),
- Tuple = Tuple.Create("z", (object)129310.23D),
Uuid = Guid.NewGuid(),
+ NullableGuid = Guid.NewGuid(),
Char = '\n',
Text = "this is a string w/ spanish characters like EÑE.",
Bool = true,
@@ -488,11 +476,9 @@ public void SagaDataCanBeRecoveredWithDifferentKindOfValuesAsCorrelations()
Time = TimeSpan.MaxValue,
Enum = ComplexSaga.Values.Valid,
EnumFlags = ComplexSaga.Flags.One | ComplexSaga.Flags.Two,
- Object = new { one = 1, two = 2.7878798745M },
+ Object = 2.7878798745M,
Strings = new[] { "x", "y" },
- Decimals = new[] { 0.646584564M, 6.98984564544212M },
- Objects = new object[] { 'x', 0x1, float.Epsilon },
- Bag = new Dictionary() { { "a", 1 }, { "b", float.NaN } }
+ Decimals = new[] { 0.646584564M, 6.98984564544212M }
};
_persister.Insert(data, _correlations.Keys.ToArray());
@@ -566,18 +552,17 @@ private IEnumerableSagaData EnumerableSagaData(int someNumber, IEnumerable(d => d.PropertyThatCanBeNull);
var dataWithIndexedNullProperty = new SomePieceOfSagaData { Id = Guid.NewGuid(), SomeValueWeCanRecognize = "hello" };
- persister.Insert(dataWithIndexedNullProperty, new[] { propertyName });
- var sagaDataFoundViaNullProperty = persister.Find(propertyName, null);
+ _persister.Insert(dataWithIndexedNullProperty, new[] { propertyName });
+ var sagaDataFoundViaNullProperty = _persister.Find(propertyName, null);
Assert.That(sagaDataFoundViaNullProperty, Is.Not.Null, "Could not find saga data with (null) on the correlation property {0}", propertyName);
Assert.That(sagaDataFoundViaNullProperty.SomeValueWeCanRecognize, Is.EqualTo("hello"));
sagaDataFoundViaNullProperty.SomeValueWeCanRecognize = "hwello there!!1";
- persister.Update(sagaDataFoundViaNullProperty, new[] { propertyName });
- var sagaDataFoundAgainViaNullProperty = persister.Find(propertyName, null);
+ _persister.Update(sagaDataFoundViaNullProperty, new[] { propertyName });
+ var sagaDataFoundAgainViaNullProperty = _persister.Find(propertyName, null);
Assert.That(sagaDataFoundAgainViaNullProperty, Is.Not.Null, "Could not find saga data with (null) on the correlation property {0} after having updated it", propertyName);
Assert.That(sagaDataFoundAgainViaNullProperty.SomeValueWeCanRecognize, Is.EqualTo("hwello there!!1"));
}
@@ -585,22 +570,20 @@ public void CanFindAndUpdateSagaDataByCorrelationPropertyWithNull()
[Test]
public void PersisterCanFindSagaByPropertiesWithDifferentDataTypes()
{
- var persister = _persister;
- TestFindSagaByPropertyWithType(persister, "Hello worlds!!");
- TestFindSagaByPropertyWithType(persister, 23);
- TestFindSagaByPropertyWithType(persister, Guid.NewGuid());
+ TestFindSagaByPropertyWithType(_persister, "Hello worlds!!");
+ TestFindSagaByPropertyWithType(_persister, 23);
+ TestFindSagaByPropertyWithType(_persister, Guid.NewGuid());
}
[Test]
public void PersisterCanFindSagaById()
{
- var persister = _persister;
var savedSagaData = new MySagaData();
var savedSagaDataId = Guid.NewGuid();
savedSagaData.Id = savedSagaDataId;
- persister.Insert(savedSagaData, new string[0]);
+ _persister.Insert(savedSagaData, new string[0]);
- var foundSagaData = persister.Find("Id", savedSagaDataId);
+ var foundSagaData = _persister.Find("Id", savedSagaDataId);
Assert.That(foundSagaData.Id, Is.EqualTo(savedSagaDataId));
}
@@ -608,7 +591,6 @@ public void PersisterCanFindSagaById()
[Test]
public void PersistsComplexSagaLikeExpected()
{
- var persister = _persister;
var sagaDataId = Guid.NewGuid();
var complexPieceOfSagaData =
@@ -630,9 +612,9 @@ public void PersistsComplexSagaLikeExpected()
}
};
- persister.Insert(complexPieceOfSagaData, new[] { "SomeField" });
+ _persister.Insert(complexPieceOfSagaData, new[] { "SomeField" });
- var sagaData = persister.Find("Id", sagaDataId);
+ var sagaData = _persister.Find("Id", sagaDataId);
Assert.That(sagaData.SomeField, Is.EqualTo("hello"));
Assert.That(sagaData.AnotherField, Is.EqualTo("world!"));
}
@@ -642,7 +624,6 @@ public void CanDeleteSaga()
{
const string someStringValue = "whoolala";
- var persister = _persister;
var mySagaDataId = Guid.NewGuid();
var mySagaData = new SimpleSagaData
{
@@ -650,27 +631,25 @@ public void CanDeleteSaga()
SomeString = someStringValue
};
- persister.Insert(mySagaData, new[] { "SomeString" });
- var sagaDataToDelete = persister.Find("Id", mySagaDataId);
+ _persister.Insert(mySagaData, new[] { "SomeString" });
+ var sagaDataToDelete = _persister.Find("Id", mySagaDataId);
- persister.Delete(sagaDataToDelete);
+ _persister.Delete(sagaDataToDelete);
- var sagaData = persister.Find("Id", mySagaDataId);
+ var sagaData = _persister.Find("Id", mySagaDataId);
Assert.That(sagaData, Is.Null);
}
[Test]
public void CanFindSagaByPropertyValues()
{
- var persister = _persister;
+ _persister.Insert(SagaData(1, "some field 1"), new[] { "AnotherField" });
+ _persister.Insert(SagaData(2, "some field 2"), new[] { "AnotherField" });
+ _persister.Insert(SagaData(3, "some field 3"), new[] { "AnotherField" });
- persister.Insert(SagaData(1, "some field 1"), new[] { "AnotherField" });
- persister.Insert(SagaData(2, "some field 2"), new[] { "AnotherField" });
- persister.Insert(SagaData(3, "some field 3"), new[] { "AnotherField" });
-
- var dataViaNonexistentValue = persister.Find("AnotherField", "non-existent value");
- var dataViaNonexistentField = persister.Find("SomeFieldThatDoesNotExist", "doesn't matter");
- var mySagaData = persister.Find("AnotherField", "some field 2");
+ var dataViaNonexistentValue = _persister.Find("AnotherField", "non-existent value");
+ var dataViaNonexistentField = _persister.Find("SomeFieldThatDoesNotExist", "doesn't matter");
+ var mySagaData = _persister.Find("AnotherField", "some field 2");
Assert.That(dataViaNonexistentField, Is.Null);
Assert.That(dataViaNonexistentValue, Is.Null);
@@ -681,13 +660,12 @@ public void CanFindSagaByPropertyValues()
[Test]
public void CanFindSagaWithIEnumerableAsCorrelatorId()
{
- var persister = _persister;
- persister.Insert(EnumerableSagaData(3, new string[] { "Field 1", "Field 2", "Field 3"}), new[] { "AnotherFields" });
+ _persister.Insert(EnumerableSagaData(3, new string[] { "Field 1", "Field 2", "Field 3" }), new[] { "AnotherFields" });
- var dataViaNonexistentValue = persister.Find("AnotherFields", "non-existent value");
- var dataViaNonexistentField = persister.Find("SomeFieldThatDoesNotExist", "doesn't matter");
- var mySagaData = persister.Find("AnotherFields", "Field 3");
+ var dataViaNonexistentValue = _persister.Find("AnotherFields", "non-existent value");
+ var dataViaNonexistentField = _persister.Find("SomeFieldThatDoesNotExist", "doesn't matter");
+ var mySagaData = _persister.Find("AnotherFields", "Field 3");
Assert.That(dataViaNonexistentField, Is.Null);
Assert.That(dataViaNonexistentValue, Is.Null);
@@ -698,14 +676,13 @@ public void CanFindSagaWithIEnumerableAsCorrelatorId()
[Test]
public void SamePersisterCanSaveMultipleTypesOfSagaDatas()
{
- var persister = _persister;
var sagaId1 = Guid.NewGuid();
var sagaId2 = Guid.NewGuid();
- persister.Insert(new SimpleSagaData { Id = sagaId1, SomeString = "Ol�" }, new[] { "Id" });
- persister.Insert(new MySagaData { Id = sagaId2, AnotherField = "Yipiie" }, new[] { "Id" });
+ _persister.Insert(new SimpleSagaData { Id = sagaId1, SomeString = "Ol�" }, new[] { "Id" });
+ _persister.Insert(new MySagaData { Id = sagaId2, AnotherField = "Yipiie" }, new[] { "Id" });
- var saga1 = persister.Find("Id", sagaId1);
- var saga2 = persister.Find("Id", sagaId2);
+ var saga1 = _persister.Find("Id", sagaId1);
+ var saga2 = _persister.Find("Id", sagaId2);
Assert.That(saga1.SomeString, Is.EqualTo("Ol�"));
Assert.That(saga2.AnotherField, Is.EqualTo("Yipiie"));
@@ -715,25 +692,43 @@ public void SamePersisterCanSaveMultipleTypesOfSagaDatas()
[Test]
public void PersisterCanFindSagaDataWithNestedElements()
{
- const string stringValue = "I expect to find something with this string!";
+ var one = new SagaDataWithNestedElement()
+ {
+ Id = Guid.NewGuid(),
+ Revision = 1,
+ ThisOneIsNested = new ThisOneIsNested() { SomeString = "1" },
+ ThisOneIsNestedToo = new ThisOneIsNested() { SomeString = "999" }
+ };
- var persister = _persister;
- var path = Reflect.Path(d => d.ThisOneIsNested.SomeString);
+ var two = new SagaDataWithNestedElement()
+ {
+ Id = Guid.NewGuid(),
+ Revision = 2,
+ ThisOneIsNested = new ThisOneIsNested() { SomeString = "999" },
+ ThisOneIsNestedToo = new ThisOneIsNested() { SomeString = "2" }
+ };
- persister.Insert(new SagaDataWithNestedElement
+ var notFound = new SagaDataWithNestedElement()
{
Id = Guid.NewGuid(),
- Revision = 12,
- ThisOneIsNested = new ThisOneIsNested
- {
- SomeString = stringValue
- }
- }, new[] { path });
+ Revision = 404,
+ ThisOneIsNested = new ThisOneIsNested() { SomeString = "2" },
+ ThisOneIsNestedToo = new ThisOneIsNested() { SomeString = "1" }
+ };
+
+ var pathOne = Reflect.Path(x => x.ThisOneIsNested.SomeString);
+ var pathTwo = Reflect.Path(x => x.ThisOneIsNestedToo.SomeString);
- var loadedSagaData = persister.Find(path, stringValue);
+ _persister.Insert(one, new[] { pathOne });
+ _persister.Insert(two, new[] { pathTwo });
+ _persister.Insert(notFound, new[] { pathOne, pathTwo });
- Assert.That(loadedSagaData?.ThisOneIsNested, Is.Not.Null);
- Assert.That(loadedSagaData?.ThisOneIsNested.SomeString, Is.EqualTo(stringValue));
+ var recoveredOne = _persister.Find(pathOne, "1");
+ var recoveredTwo = _persister.Find(pathTwo, "2");
+ Assert.That(recoveredOne?.Id, Is.EqualTo(one.Id), "Expected to recovered one.");
+ Assert.That(recoveredTwo?.Id, Is.EqualTo(two.Id), "Expected to recovered one.");
+ Assert.That(_persister.Find(pathOne, "3"), Is.Null);
+ Assert.That(_persister.Find(pathTwo, "3"), Is.Null);
}
#endregion
@@ -754,16 +749,15 @@ public void CanUpdateSaga()
// arrange
const string theValue = "this is just some value";
- var persister = _persister;
var firstSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = theValue };
var propertyPath = Reflect.Path(s => s.SomeCorrelationId);
var pathsToIndex = new[] { propertyPath };
- persister.Insert(firstSaga, pathsToIndex);
+ _persister.Insert(firstSaga, pathsToIndex);
- var sagaToUpdate = persister.Find(propertyPath, theValue);
+ var sagaToUpdate = _persister.Find(propertyPath, theValue);
- Assert.DoesNotThrow(() => persister.Update(sagaToUpdate, pathsToIndex));
+ Assert.DoesNotThrow(() => _persister.Update(sagaToUpdate, pathsToIndex));
}
[Test, Description("We don't allow two sagas to have the same value of a property that is used to correlate with incoming messages, " +
@@ -772,48 +766,46 @@ public void CanUpdateSaga()
public void CannotInsertAnotherSagaWithDuplicateCorrelationId()
{
// arrange
- var persister = _persister;
var theValue = "this just happens to be the same in two sagas";
var firstSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = theValue };
var secondSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = theValue };
- if (persister is ICanUpdateMultipleSagaDatasAtomically)
+ if (_persister is ICanUpdateMultipleSagaDatasAtomically)
{
Assert.Ignore("Ignore test as persister does actually support multiple saga to be updated automically.");
return;
}
var pathsToIndex = new[] { Reflect.Path(s => s.SomeCorrelationId) };
- persister.Insert(firstSaga, pathsToIndex);
+ _persister.Insert(firstSaga, pathsToIndex);
// act
// assert
- Assert.Throws(() => persister.Insert(secondSaga, pathsToIndex));
+ Assert.Throws(() => _persister.Insert(secondSaga, pathsToIndex));
}
[Test]
public void CannotUpdateAnotherSagaWithDuplicateCorrelationId()
{
- // arrange
- var persister = _persister;
+ // arrange
var theValue = "this just happens to be the same in two sagas";
var firstSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = theValue };
var secondSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = "other value" };
- if (persister is ICanUpdateMultipleSagaDatasAtomically)
- {
+ if (_persister is ICanUpdateMultipleSagaDatasAtomically)
+ {
Assert.Ignore("Ignore test as persister does actually support multiple saga to be updated automically.");
return;
}
var pathsToIndex = new[] { Reflect.Path(s => s.SomeCorrelationId) };
- persister.Insert(firstSaga, pathsToIndex);
- persister.Insert(secondSaga, pathsToIndex);
+ _persister.Insert(firstSaga, pathsToIndex);
+ _persister.Insert(secondSaga, pathsToIndex);
// act
// assert
secondSaga.SomeCorrelationId = theValue;
- Assert.Throws(() => persister.Update(secondSaga, pathsToIndex));
+ Assert.Throws(() => _persister.Update(secondSaga, pathsToIndex));
}
[Test]
@@ -821,12 +813,11 @@ public void CannotUpdateAnotherSagaWithDuplicateCorrelationId()
public void CanInsertAnotherSagaWithDuplicateCorrelationId()
{
// arrange
- var persister = _persister;
var theValue = "this just happens to be the same in two sagas";
var firstSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = theValue };
var secondSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = theValue };
- if (!(persister is ICanUpdateMultipleSagaDatasAtomically))
+ if (!(_persister is ICanUpdateMultipleSagaDatasAtomically))
{
Assert.Ignore("Ignore test as persister does not support multiple saga to be updated automically.");
return;
@@ -835,8 +826,8 @@ public void CanInsertAnotherSagaWithDuplicateCorrelationId()
var pathsToIndex = new[] { Reflect.Path(s => s.SomeCorrelationId) };
// act
- persister.Insert(firstSaga, pathsToIndex);
- persister.Insert(secondSaga, pathsToIndex);
+ _persister.Insert(firstSaga, pathsToIndex);
+ _persister.Insert(secondSaga, pathsToIndex);
// assert
}
@@ -844,13 +835,12 @@ public void CanInsertAnotherSagaWithDuplicateCorrelationId()
[Test]
public void CanUpdateAnotherSagaWithDuplicateCorrelationId()
{
- // arrange
- var persister = _persister;
+ // arrange
var theValue = "this just happens to be the same in two sagas";
var firstSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = theValue };
var secondSaga = new SomeSaga { Id = Guid.NewGuid(), SomeCorrelationId = "other value" };
- if (!(persister is ICanUpdateMultipleSagaDatasAtomically))
+ if (!(_persister is ICanUpdateMultipleSagaDatasAtomically))
{
Assert.Ignore("Ignore test as persister does not support multiple saga to be updated automically.");
return;
@@ -859,11 +849,11 @@ public void CanUpdateAnotherSagaWithDuplicateCorrelationId()
var pathsToIndex = new[] { Reflect.Path(s => s.SomeCorrelationId) };
// act
- persister.Insert(firstSaga, pathsToIndex);
- persister.Insert(secondSaga, pathsToIndex);
+ _persister.Insert(firstSaga, pathsToIndex);
+ _persister.Insert(secondSaga, pathsToIndex);
secondSaga.SomeCorrelationId = theValue;
- persister.Update(secondSaga, pathsToIndex);
+ _persister.Update(secondSaga, pathsToIndex);
// assert
}
@@ -871,16 +861,15 @@ public void CanUpdateAnotherSagaWithDuplicateCorrelationId()
[Test]
public void EnsuresUniquenessAlsoOnCorrelationPropertyWithNull()
{
- var persister = _persister;
var propertyName = Reflect.Path(d => d.PropertyThatCanBeNull);
var dataWithIndexedNullProperty = new SomePieceOfSagaData { Id = Guid.NewGuid(), SomeValueWeCanRecognize = "hello" };
var anotherPieceOfDataWithIndexedNullProperty = new SomePieceOfSagaData { Id = Guid.NewGuid(), SomeValueWeCanRecognize = "hello" };
- persister.Insert(dataWithIndexedNullProperty, new[] { propertyName });
+ _persister.Insert(dataWithIndexedNullProperty, new[] { propertyName });
Assert.That(
- () => persister.Insert(anotherPieceOfDataWithIndexedNullProperty, new[] { propertyName }),
- (persister is ICanUpdateMultipleSagaDatasAtomically) ? (IResolveConstraint)Throws.Nothing : Throws.Exception
+ () => _persister.Insert(anotherPieceOfDataWithIndexedNullProperty, new[] { propertyName }),
+ (_persister is ICanUpdateMultipleSagaDatasAtomically) ? (IResolveConstraint)Throws.Nothing : Throws.Exception
);
}
@@ -891,13 +880,12 @@ public void EnsuresUniquenessAlsoOnCorrelationPropertyWithNull()
[Test]
public void UsesOptimisticLockingAndDetectsRaceConditionsWhenUpdatingFindingBySomeProperty()
{
- var persister = _persister;
var indexBySomeString = new[] { "SomeString" };
var id = Guid.NewGuid();
var simpleSagaData = new SimpleSagaData { Id = id, SomeString = "hello world!" };
- persister.Insert(simpleSagaData, indexBySomeString);
+ _persister.Insert(simpleSagaData, indexBySomeString);
- var sagaData1 = persister.Find("SomeString", "hello world!");
+ var sagaData1 = _persister.Find("SomeString", "hello world!");
Assert.That(sagaData1, Is.Not.Null);
@@ -905,93 +893,89 @@ public void UsesOptimisticLockingAndDetectsRaceConditionsWhenUpdatingFindingBySo
using (EnterAFakeMessageContext())
{
- var sagaData2 = persister.Find("SomeString", "hello world!");
+ var sagaData2 = _persister.Find("SomeString", "hello world!");
sagaData2.SomeString = "I changed this on another worker";
- persister.Update(sagaData2, indexBySomeString);
+ _persister.Update(sagaData2, indexBySomeString);
}
- Assert.Throws(() => persister.Insert(sagaData1, indexBySomeString));
+ Assert.Throws(() => _persister.Insert(sagaData1, indexBySomeString));
}
[Test]
public void UsesOptimisticLockingAndDetectsRaceConditionsWhenUpdatingFindingById()
{
- var persister = _persister;
var indexBySomeString = new[] { "Id" };
var id = Guid.NewGuid();
var simpleSagaData = new SimpleSagaData { Id = id, SomeString = "hello world!" };
- persister.Insert(simpleSagaData, indexBySomeString);
+ _persister.Insert(simpleSagaData, indexBySomeString);
- var sagaData1 = persister.Find("Id", id);
+ var sagaData1 = _persister.Find("Id", id);
sagaData1.SomeString = "I changed this on one worker";
using (EnterAFakeMessageContext())
- {
- var sagaData2 = persister.Find("Id", id);
+ {
+ var sagaData2 = _persister.Find("Id", id);
sagaData2.SomeString = "I changed this on another worker";
- persister.Update(sagaData2, indexBySomeString);
+ _persister.Update(sagaData2, indexBySomeString);
}
- Assert.Throws(() => persister.Insert(sagaData1, indexBySomeString));
+ Assert.Throws(() => _persister.Insert(sagaData1, indexBySomeString));
}
[Test]
public void ConcurrentDeleteAndUpdateThrowsOnUpdate()
{
- var persister = _persister;
var indexBySomeString = new[] { "Id" };
var id = Guid.NewGuid();
var simpleSagaData = new SimpleSagaData { Id = id };
- persister.Insert(simpleSagaData, indexBySomeString);
- var sagaData1 = persister.Find("Id", id);
+ _persister.Insert(simpleSagaData, indexBySomeString);
+ var sagaData1 = _persister.Find("Id", id);
sagaData1.SomeString = "Some new value";
using (EnterAFakeMessageContext())
{
- var sagaData2 = persister.Find("Id", id);
- persister.Delete(sagaData2);
+ var sagaData2 = _persister.Find("Id", id);
+ _persister.Delete(sagaData2);
}
- Assert.Throws(() => persister.Update(sagaData1, indexBySomeString));
+ Assert.Throws(() => _persister.Update(sagaData1, indexBySomeString));
}
[Test]
public void ConcurrentDeleteAndUpdateThrowsOnDelete()
{
- var persister = _persister;
var indexBySomeString = new[] { "Id" };
var id = Guid.NewGuid();
var simpleSagaData = new SimpleSagaData { Id = id };
- persister.Insert(simpleSagaData, indexBySomeString);
- var sagaData1 = persister.Find("Id", id);
+ _persister.Insert(simpleSagaData, indexBySomeString);
+ var sagaData1 = _persister.Find("Id", id);
using (EnterAFakeMessageContext())
{
- var sagaData2 = persister.Find("Id", id);
+ var sagaData2 = _persister.Find("Id", id);
sagaData2.SomeString = "Some new value";
- persister.Update(sagaData2, indexBySomeString);
+ _persister.Update(sagaData2, indexBySomeString);
}
- Assert.Throws(() => persister.Delete(sagaData1));
+ Assert.Throws(() => _persister.Delete(sagaData1));
}
[Test]
public void InsertingTheSameSagaDataTwiceGeneratesAnError()
{
// arrange
- var persister = _persister;
var sagaDataPropertyPathsToIndex = new[] { Reflect.Path(d => d.Id) };
var sagaId = Guid.NewGuid();
- persister.Insert(new SimpleSagaData { Id = sagaId, Revision = 0, SomeString = "hello!" },
+ _persister.Insert(new SimpleSagaData { Id = sagaId, Revision = 0, SomeString = "hello!" },
sagaDataPropertyPathsToIndex);
// act
// assert
Assert.Throws(
- () => persister.Insert(new SimpleSagaData { Id = sagaId, Revision = 0, SomeString = "hello!" },
+ () => _persister.Insert(new SimpleSagaData { Id = sagaId, Revision = 0, SomeString = "hello!" },
sagaDataPropertyPathsToIndex));
}
@@ -1002,22 +986,21 @@ public void InsertingTheSameSagaDataTwiceGeneratesAnError()
[Test]
public void CanInsertTwoSagasUnderASingleUoW()
{
- var persister = _persister;
var sagaId1 = Guid.NewGuid();
var sagaId2 = Guid.NewGuid();
using (var uow = _manager.Create())
{
- persister.Insert(new SimpleSagaData { Id = sagaId1, SomeString = "FirstSaga" }, new[] { "Id" });
- persister.Insert(new MySagaData { Id = sagaId2, AnotherField = "SecondSaga" }, new[] { "Id" });
+ _persister.Insert(new SimpleSagaData { Id = sagaId1, SomeString = "FirstSaga" }, new[] { "Id" });
+ _persister.Insert(new MySagaData { Id = sagaId2, AnotherField = "SecondSaga" }, new[] { "Id" });
uow.Commit();
}
using (EnterAFakeMessageContext())
{
- var saga1 = persister.Find("Id", sagaId1);
- var saga2 = persister.Find("Id", sagaId2);
+ var saga1 = _persister.Find("Id", sagaId1);
+ var saga2 = _persister.Find("Id", sagaId2);
Assert.That(saga1.SomeString, Is.EqualTo("FirstSaga"));
Assert.That(saga2.AnotherField, Is.EqualTo("SecondSaga"));
@@ -1027,22 +1010,21 @@ public void CanInsertTwoSagasUnderASingleUoW()
[Test]
public void NoChangesAreMadeWhenUoWIsNotCommitted()
{
- var persister = _persister;
var sagaId1 = Guid.NewGuid();
var sagaId2 = Guid.NewGuid();
using (var uow = _manager.Create())
{
- persister.Insert(new SimpleSagaData { Id = sagaId1, SomeString = "FirstSaga" }, new[] { "Id" });
- persister.Insert(new MySagaData { Id = sagaId2, AnotherField = "SecondSaga" }, new[] { "Id" });
+ _persister.Insert(new SimpleSagaData { Id = sagaId1, SomeString = "FirstSaga" }, new[] { "Id" });
+ _persister.Insert(new MySagaData { Id = sagaId2, AnotherField = "SecondSaga" }, new[] { "Id" });
// XXX: Purposedly not committed.
}
using (EnterAFakeMessageContext())
{
- var saga1 = persister.Find("Id", sagaId1);
- var saga2 = persister.Find("Id", sagaId2);
+ var saga1 = _persister.Find("Id", sagaId1);
+ var saga2 = _persister.Find("Id", sagaId2);
Assert.That(saga1, Is.Null);
Assert.That(saga2, Is.Null);
@@ -1062,23 +1044,32 @@ public void FindSameSagaTwiceThrowsOnNoWait()
return;
}
- var persister = _persister;
var savedSagaData = new MySagaData();
var savedSagaDataId = Guid.NewGuid();
savedSagaData.Id = savedSagaDataId;
- persister.Insert(savedSagaData, new string[0]);
+ _persister.Insert(savedSagaData, new string[0]);
using (_manager.Create())
- using (_manager.GetScope(autocomplete: true))
+ //using (_manager.GetScope(autocomplete: true)) //< XXX: May required for something but we don't know when.
{
- persister.Find("Id", savedSagaDataId);
+ _persister.Find("Id", savedSagaDataId);
+
Assert.Throws(() =>
{
+ //using (var thread = new CrossThreadRunner(() =>
+ //{
using (EnterAFakeMessageContext())
+ //using (_manager.Create())
+ //using (_manager.GetScope(autocomplete: true))
{
- persister.Find("Id", savedSagaDataId);
+ //_manager.GetScope().Connection.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ COMMITTED;");
+ _persister.Find("Id", savedSagaDataId);
}
+ //}))
+ //{
+ // thread.Run();
+ //}
});
}
}
diff --git a/Rebus.AdoNet.Tests/packages.config b/Rebus.AdoNet.Tests/packages.config
index 4be238e..0ffcbb5 100644
--- a/Rebus.AdoNet.Tests/packages.config
+++ b/Rebus.AdoNet.Tests/packages.config
@@ -1,12 +1,12 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Rebus.AdoNet.sln b/Rebus.AdoNet.sln
index 182307e..6cbc977 100644
--- a/Rebus.AdoNet.sln
+++ b/Rebus.AdoNet.sln
@@ -1,27 +1,27 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 14
-VisualStudioVersion = 14.0.24720.0
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rebus.AdoNet", "Rebus.AdoNet\Rebus.AdoNet.csproj", "{03B56847-7469-4DB3-B146-2D29CE61663E}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rebus.AdoNet.Tests", "Rebus.AdoNet.Tests\Rebus.AdoNet.Tests.csproj", "{803F12AC-8946-4718-8167-CD27077FDE46}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {03B56847-7469-4DB3-B146-2D29CE61663E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {03B56847-7469-4DB3-B146-2D29CE61663E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {03B56847-7469-4DB3-B146-2D29CE61663E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {03B56847-7469-4DB3-B146-2D29CE61663E}.Release|Any CPU.Build.0 = Release|Any CPU
- {803F12AC-8946-4718-8167-CD27077FDE46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {803F12AC-8946-4718-8167-CD27077FDE46}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {803F12AC-8946-4718-8167-CD27077FDE46}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {803F12AC-8946-4718-8167-CD27077FDE46}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
-EndGlobal
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.24720.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rebus.AdoNet", "Rebus.AdoNet\Rebus.AdoNet.csproj", "{03B56847-7469-4DB3-B146-2D29CE61663E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rebus.AdoNet.Tests", "Rebus.AdoNet.Tests\Rebus.AdoNet.Tests.csproj", "{803F12AC-8946-4718-8167-CD27077FDE46}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {03B56847-7469-4DB3-B146-2D29CE61663E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {03B56847-7469-4DB3-B146-2D29CE61663E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {03B56847-7469-4DB3-B146-2D29CE61663E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {03B56847-7469-4DB3-B146-2D29CE61663E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {803F12AC-8946-4718-8167-CD27077FDE46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {803F12AC-8946-4718-8167-CD27077FDE46}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {803F12AC-8946-4718-8167-CD27077FDE46}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {803F12AC-8946-4718-8167-CD27077FDE46}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/Rebus.AdoNet/AdoNetSagaPersister.cs b/Rebus.AdoNet/AdoNetSagaPersister.cs
index a3c2324..94e0958 100644
--- a/Rebus.AdoNet/AdoNetSagaPersister.cs
+++ b/Rebus.AdoNet/AdoNetSagaPersister.cs
@@ -209,20 +209,11 @@ protected string Serialize(ISagaData sagaData)
return JsonConvert.SerializeObject(sagaData, Formatting.Indented, Settings);
}
- public virtual void Insert(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
- {
- throw new NotImplementedException("Insert method implementation missing?1");
- }
+ public abstract void Insert(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex);
- public virtual void Update(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
- {
- throw new NotImplementedException("Update method implementation missing?1");
- }
+ public abstract void Update(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex);
- public virtual void Delete(ISagaData sagaData)
- {
- throw new NotImplementedException("Delete method implementation missing?1");
- }
+ public abstract void Delete(ISagaData sagaData);
protected abstract string Fetch(string sagaDataPropertyPath, object fieldFromMessage)
where TSagaData : class, ISagaData;
diff --git a/Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs b/Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs
index 17e54f0..c7fcc6a 100644
--- a/Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs
+++ b/Rebus.AdoNet/AdoNetSagaPersisterAdvanced.cs
@@ -1,399 +1,436 @@
-#if true
-using System;
-using System.Data;
-using System.Linq;
-using System.Data.Common;
-using System.Collections;
-using System.Collections.Generic;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Globalization;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Converters;
-using Rebus.Logging;
-using Rebus.AdoNet.Schema;
-using Rebus.AdoNet.Dialects;
-
-namespace Rebus.AdoNet
-{
- ///
- /// Implements a saga persister for Rebus that stores sagas using an AdoNet provider.
- /// This is an advanced implementation using single-table scheme for saga & indexes.
- ///
- public class AdoNetSagaPersisterAdvanced : AdoNetSagaPersister, IStoreSagaData, AdoNetSagaPersisterFluentConfigurer, ICanUpdateMultipleSagaDatasAtomically
- {
- private const int MaximumSagaDataTypeNameLength = 80;
- private const string SAGA_ID_COLUMN = "id";
- private const string SAGA_TYPE_COLUMN = "saga_type";
- private const string SAGA_DATA_COLUMN = "data";
- private const string SAGA_REVISION_COLUMN = "revision";
- private const string SAGA_CORRELATIONS_COLUMN = "correlations";
- private static ILog log;
-
- // TODO?: Maybe we should implement our own micro-serialization logic, so we can control actual conversions.
- // I am thinking for example on issues with preccision on decimals, etc. (pruiz)
- private static readonly JsonSerializerSettings IndexSerializerSettings = new JsonSerializerSettings {
- Culture = CultureInfo.InvariantCulture,
- TypeNameHandling = TypeNameHandling.None, // TODO: Make it configurable?
- DateFormatHandling = DateFormatHandling.IsoDateFormat, // TODO: Make it configurable?
- Converters = new List() {
- new StringEnumConverter()
- }
- };
-
- private readonly AdoNetUnitOfWorkManager manager;
- private readonly string sagasTableName;
- private readonly string idPropertyName;
-
- static AdoNetSagaPersisterAdvanced()
- {
- RebusLoggerFactory.Changed += f => log = f.GetCurrentClassLogger();
- }
-
- ///
- /// Constructs the persister with the ability to create connections to database using the specified connection string.
- /// This also means that the persister will manage the connection by itself, closing it when it has stopped using it.
- ///
- public AdoNetSagaPersisterAdvanced(AdoNetUnitOfWorkManager manager, string sagasTableName)
- : base(manager)
- {
- this.manager = manager;
- this.sagasTableName = sagasTableName;
- this.idPropertyName = Reflect.Path(x => x.Id);
- }
-
- #region AdoNetSagaPersisterFluentConfigurer
-
- ///
- /// Creates the necessary saga storage tables if they haven't already been created. If a table already exists
- /// with a name that matches one of the desired table names, no action is performed (i.e. it is assumed that
- /// the tables already exist).
- ///
- public override AdoNetSagaPersisterFluentConfigurer EnsureTablesAreCreated()
- {
- using (var uow = manager.Create(autonomous: true))
- using (var scope = (uow as AdoNetUnitOfWork).GetScope())
- {
- var dialect = scope.Dialect;
- var connection = scope.Connection;
- var tableNames = scope.GetTableNames();
-
- // bail out if there's already a table in the database with one of the names
- var sagaTableIsAlreadyCreated = tableNames.Contains(sagasTableName, StringComparer.InvariantCultureIgnoreCase);
-
- if (sagaTableIsAlreadyCreated)
- {
- log.Debug("Table '{0}' already exists.", sagasTableName);
- return this;
- }
-
- if (UseSqlArrays /* && !dialect.SupportsArrayTypes*/)
- {
- throw new ApplicationException("Enabled UseSqlArraysForCorrelationIndexes AdoNetSagaPersister selecte does not support arrays?!");
- //throw new ApplicationException("Enabled UseSqlArraysForCorrelationIndexes but underlaying database does not support arrays?!");
- }
-
- var indexes = new[] {
- new AdoNetIndex() {
- Name = $"ix_{sagasTableName}_{SAGA_ID_COLUMN}_{SAGA_TYPE_COLUMN}",
- Columns = new[] { SAGA_ID_COLUMN, SAGA_TYPE_COLUMN }
- },
- }.ToList();
-
- if (dialect.SupportsGinIndexes)
- {
- if (dialect.SupportsMultiColumnGinIndexes)
- {
- indexes.Add(new AdoNetIndex() {
- Name = $"ix_{sagasTableName}_{SAGA_TYPE_COLUMN}_{SAGA_CORRELATIONS_COLUMN}",
- Columns = new[] { SAGA_TYPE_COLUMN, SAGA_CORRELATIONS_COLUMN },
- Kind = AdoNetIndex.Kinds.GIN,
- });
- }
- else
- {
- indexes.Add(new AdoNetIndex() {
- Name = $"ix_{sagasTableName}_{SAGA_CORRELATIONS_COLUMN}",
- Columns = new[] { SAGA_CORRELATIONS_COLUMN },
- Kind = AdoNetIndex.Kinds.GIN,
- });
- }
- }
-
- using (var command = connection.CreateCommand())
- {
- command.CommandText = scope.Dialect.FormatCreateTable(
- new AdoNetTable()
- {
- Name = sagasTableName,
- Columns = new []
- {
- new AdoNetColumn() { Name = SAGA_ID_COLUMN, DbType = DbType.Guid },
- new AdoNetColumn() { Name = SAGA_TYPE_COLUMN, DbType = DbType.String, Length = MaximumSagaDataTypeNameLength },
- new AdoNetColumn() { Name = SAGA_REVISION_COLUMN, DbType = DbType.Int32 },
- new AdoNetColumn() { Name = SAGA_DATA_COLUMN, DbType = DbType.String, Length = 1073741823 },
- new AdoNetColumn() { Name = SAGA_CORRELATIONS_COLUMN, DbType = DbType.Object, }
- },
- PrimaryKey = new[] { SAGA_ID_COLUMN },
- Indexes = indexes
- }
- );
-
- log.Info("Table '{0}' do not exists - it will be created now using:\n{1}", sagasTableName, command.CommandText);
-
- command.ExecuteNonQuery();
- }
-
- scope.Complete();
- log.Info("Table '{0}' created", sagasTableName);
- }
-
- return this;
- }
-
- #endregion
-
- protected string SerializeCorrelations(IDictionary sagaData)
- {
- return JsonConvert.SerializeObject(sagaData, Formatting.Indented, IndexSerializerSettings);
- }
- public override void Insert(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
- {
- using (var scope = manager.GetScope())
- using (var command = scope.Connection.CreateCommand())
- {
- var dialect = scope.Dialect;
- var sagaTypeName = GetSagaTypeName(sagaData.GetType());
- var propertiesToIndex = GetCorrelationItems(sagaData, sagaDataPropertyPathsToIndex);
- var correlations = propertiesToIndex.Any() ? SerializeCorrelations(propertiesToIndex) : "{}";
-
- // next insert the saga
- command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
- command.AddParameter(dialect.EscapeParameter(SAGA_TYPE_COLUMN), sagaTypeName);
- command.AddParameter(dialect.EscapeParameter(SAGA_REVISION_COLUMN), ++sagaData.Revision);
- command.AddParameter(dialect.EscapeParameter(SAGA_DATA_COLUMN), Serialize(sagaData));
- command.AddParameter(dialect.EscapeParameter(SAGA_CORRELATIONS_COLUMN), correlations);
-
- command.CommandText = string.Format(
- @"insert into {0} ({1}, {2}, {3}, {4}, {5}) values ({6}, {7}, {8}, {9}, {10});",
- dialect.QuoteForTableName(sagasTableName),
- dialect.QuoteForColumnName(SAGA_ID_COLUMN),
- dialect.QuoteForColumnName(SAGA_TYPE_COLUMN),
- dialect.QuoteForColumnName(SAGA_REVISION_COLUMN),
- dialect.QuoteForColumnName(SAGA_DATA_COLUMN),
- dialect.QuoteForColumnName(SAGA_CORRELATIONS_COLUMN),
- dialect.EscapeParameter(SAGA_ID_COLUMN),
- dialect.EscapeParameter(SAGA_TYPE_COLUMN),
- dialect.EscapeParameter(SAGA_REVISION_COLUMN),
- dialect.EscapeParameter(SAGA_DATA_COLUMN),
- dialect.Cast(dialect.EscapeParameter(SAGA_CORRELATIONS_COLUMN), DbType.Object)
+#if true
+using System;
+using System.Data;
+using System.Linq;
+using System.Data.Common;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Globalization;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Rebus.Logging;
+using Rebus.AdoNet.Schema;
+using Rebus.AdoNet.Dialects;
+
+namespace Rebus.AdoNet
+{
+ ///
+ /// Implements a saga persister for Rebus that stores sagas using an AdoNet provider.
+ /// This is an advanced implementation using single-table scheme for saga & indexes.
+ ///
+ public class AdoNetSagaPersisterAdvanced : AdoNetSagaPersister, IStoreSagaData, AdoNetSagaPersisterFluentConfigurer, ICanUpdateMultipleSagaDatasAtomically
+ {
+ private const int MaximumSagaDataTypeNameLength = 80;
+ private const string SAGA_ID_COLUMN = "id";
+ private const string SAGA_TYPE_COLUMN = "saga_type";
+ private const string SAGA_DATA_COLUMN = "data";
+ private const string SAGA_REVISION_COLUMN = "revision";
+ private const string SAGA_CORRELATIONS_COLUMN = "correlations";
+ private static ILog log;
+
+ // TODO?: Maybe we should implement our own micro-serialization logic, so we can control actual conversions.
+ // I am thinking for example on issues with preccision on decimals, etc. (pruiz)
+ private static readonly JsonSerializerSettings IndexSerializerSettings = new JsonSerializerSettings {
+ Culture = CultureInfo.InvariantCulture,
+ TypeNameHandling = TypeNameHandling.None, // TODO: Make it configurable?
+ DateFormatHandling = DateFormatHandling.IsoDateFormat, // TODO: Make it configurable?
+ Converters = new List() {
+ new StringEnumConverter()
+ }
+ };
+
+ private readonly AdoNetUnitOfWorkManager manager;
+ private readonly string sagasTableName;
+ private readonly string idPropertyName;
+
+ static AdoNetSagaPersisterAdvanced()
+ {
+ RebusLoggerFactory.Changed += f => log = f.GetCurrentClassLogger();
+ }
+
+ ///
+ /// Constructs the persister with the ability to create connections to database using the specified connection string.
+ /// This also means that the persister will manage the connection by itself, closing it when it has stopped using it.
+ ///
+ public AdoNetSagaPersisterAdvanced(AdoNetUnitOfWorkManager manager, string sagasTableName)
+ : base(manager)
+ {
+ this.manager = manager;
+ this.sagasTableName = sagasTableName;
+ this.idPropertyName = Reflect.Path(x => x.Id);
+ }
+
+ #region AdoNetSagaPersisterFluentConfigurer
+
+ ///
+ /// Creates the necessary saga storage tables if they haven't already been created. If a table already exists
+ /// with a name that matches one of the desired table names, no action is performed (i.e. it is assumed that
+ /// the tables already exist).
+ ///
+ public override AdoNetSagaPersisterFluentConfigurer EnsureTablesAreCreated()
+ {
+ using (var uow = manager.Create(autonomous: true))
+ using (var scope = (uow as AdoNetUnitOfWork).GetScope())
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var tableNames = scope.GetTableNames();
+
+ // bail out if there's already a table in the database with one of the names
+ var sagaTableIsAlreadyCreated = tableNames.Contains(sagasTableName, StringComparer.InvariantCultureIgnoreCase);
+
+ if (sagaTableIsAlreadyCreated)
+ {
+ log.Debug("Table '{0}' already exists.", sagasTableName);
+ return this;
+ }
+
+ if (UseSqlArrays /* && !dialect.SupportsArrayTypes*/)
+ {
+ throw new ApplicationException("Enabled UseSqlArraysForCorrelationIndexes AdoNetSagaPersister selecte does not support arrays?!");
+ //throw new ApplicationException("Enabled UseSqlArraysForCorrelationIndexes but underlaying database does not support arrays?!");
+ }
+
+ var indexes = new[] {
+ new AdoNetIndex() {
+ Name = $"ix_{sagasTableName}_{SAGA_ID_COLUMN}_{SAGA_TYPE_COLUMN}",
+ Columns = new[] { SAGA_ID_COLUMN, SAGA_TYPE_COLUMN }
+ },
+ }.ToList();
+
+ if (dialect.SupportsGinIndexes)
+ {
+ if (dialect.SupportsMultiColumnGinIndexes)
+ {
+ indexes.Add(new AdoNetIndex() {
+ Name = $"ix_{sagasTableName}_{SAGA_TYPE_COLUMN}_{SAGA_CORRELATIONS_COLUMN}",
+ Columns = new[] { SAGA_TYPE_COLUMN, SAGA_CORRELATIONS_COLUMN },
+ Kind = AdoNetIndex.Kinds.GIN,
+ });
+ }
+ else
+ {
+ indexes.Add(new AdoNetIndex() {
+ Name = $"ix_{sagasTableName}_{SAGA_CORRELATIONS_COLUMN}",
+ Columns = new[] { SAGA_CORRELATIONS_COLUMN },
+ Kind = AdoNetIndex.Kinds.GIN,
+ });
+ }
+ }
+
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = scope.Dialect.FormatCreateTable(
+ new AdoNetTable()
+ {
+ Name = sagasTableName,
+ Columns = new []
+ {
+ new AdoNetColumn() { Name = SAGA_ID_COLUMN, DbType = DbType.Guid },
+ new AdoNetColumn() { Name = SAGA_TYPE_COLUMN, DbType = DbType.String, Length = MaximumSagaDataTypeNameLength },
+ new AdoNetColumn() { Name = SAGA_REVISION_COLUMN, DbType = DbType.Int32 },
+ new AdoNetColumn() { Name = SAGA_DATA_COLUMN, DbType = DbType.String, Length = 1073741823 },
+ new AdoNetColumn() { Name = SAGA_CORRELATIONS_COLUMN, DbType = DbType.Object, }
+ },
+ PrimaryKey = new[] { SAGA_ID_COLUMN },
+ Indexes = indexes
+ }
+ );
+
+ log.Info("Table '{0}' do not exists - it will be created now using:\n{1}", sagasTableName, command.CommandText);
+
+ command.ExecuteNonQuery();
+ }
+
+ scope.Complete();
+ log.Info("Table '{0}' created", sagasTableName);
+ }
+
+ return this;
+ }
+
+ #endregion
+
+ protected string SerializeCorrelations(IDictionary sagaData)
+ {
+ return JsonConvert.SerializeObject(sagaData, Formatting.Indented, IndexSerializerSettings);
+ }
+ public override void Insert(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
+ {
+ using (var scope = manager.GetScope())
+ using (var command = scope.Connection.CreateCommand())
+ {
+ var dialect = scope.Dialect;
+ var sagaTypeName = GetSagaTypeName(sagaData.GetType());
+ var propertiesToIndex = GetCorrelationItems(sagaData, sagaDataPropertyPathsToIndex);
+ var correlations = propertiesToIndex.Any() ? SerializeCorrelations(propertiesToIndex) : "{}";
+
+ // next insert the saga
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.AddParameter(dialect.EscapeParameter(SAGA_TYPE_COLUMN), sagaTypeName);
+ command.AddParameter(dialect.EscapeParameter(SAGA_REVISION_COLUMN), ++sagaData.Revision);
+ command.AddParameter(dialect.EscapeParameter(SAGA_DATA_COLUMN), Serialize(sagaData));
+ command.AddParameter(dialect.EscapeParameter(SAGA_CORRELATIONS_COLUMN), correlations);
+
+ command.CommandText = string.Format(
+ @"insert into {0} ({1}, {2}, {3}, {4}, {5}) values ({6}, {7}, {8}, {9}, {10});",
+ dialect.QuoteForTableName(sagasTableName),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN),
+ dialect.QuoteForColumnName(SAGA_TYPE_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN),
+ dialect.QuoteForColumnName(SAGA_DATA_COLUMN),
+ dialect.QuoteForColumnName(SAGA_CORRELATIONS_COLUMN),
+ dialect.EscapeParameter(SAGA_ID_COLUMN),
+ dialect.EscapeParameter(SAGA_TYPE_COLUMN),
+ dialect.EscapeParameter(SAGA_REVISION_COLUMN),
+ dialect.EscapeParameter(SAGA_DATA_COLUMN),
+ dialect.Cast(dialect.EscapeParameter(SAGA_CORRELATIONS_COLUMN), DbType.Object)
+ );
+
+ try
+ {
+ command.ExecuteNonQuery();
+ }
+ catch (DbException exception) when (
+ dialect.IsOptimisticLockingException(exception)
+ || dialect.IsDuplicateKeyException(exception))
+ {
+ throw new OptimisticLockingException(sagaData, exception);
+ }
+
+ scope.Complete();
+ }
+ }
+
+ public override void Update(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
+ {
+ using (var scope = manager.GetScope())
+ using (var command = scope.Connection.CreateCommand())
+ {
+ var dialect = scope.Dialect;
+ var items = GetCorrelationItems(sagaData, sagaDataPropertyPathsToIndex);
+ var correlations = items.Any() ? SerializeCorrelations(items) : "{}";
+
+ // next, update or insert the saga
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.AddParameter(dialect.EscapeParameter("current_revision"), sagaData.Revision);
+ command.AddParameter(dialect.EscapeParameter("next_revision"), ++sagaData.Revision);
+ command.AddParameter(dialect.EscapeParameter(SAGA_DATA_COLUMN), Serialize(sagaData));
+ command.AddParameter(dialect.EscapeParameter(SAGA_CORRELATIONS_COLUMN), correlations);
+
+ command.CommandText = string.Format(
+ @"UPDATE {0} SET {1} = {2}, {3} = {4}, {5} = {6} " +
+ @"WHERE {7} = {8} AND {9} = {10};",
+ dialect.QuoteForTableName(sagasTableName),
+ dialect.QuoteForColumnName(SAGA_DATA_COLUMN), dialect.EscapeParameter(SAGA_DATA_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("next_revision"),
+ dialect.QuoteForColumnName(SAGA_CORRELATIONS_COLUMN), dialect.Cast(dialect.EscapeParameter(SAGA_CORRELATIONS_COLUMN), DbType.Object),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("current_revision")
);
try
{
- command.ExecuteNonQuery();
+ var rows = command.ExecuteNonQuery();
+ if (rows == 0)
+ {
+ throw new OptimisticLockingException(sagaData);
+ }
}
- catch (DbException exception)
- {
- throw new OptimisticLockingException(sagaData, exception);
- }
-
- scope.Complete();
- }
- }
-
- public override void Update(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
+ catch (DbException exception) when (
+ dialect.IsOptimisticLockingException(exception)
+ || dialect.IsDuplicateKeyException(exception))
+ {
+ throw new OptimisticLockingException(sagaData, exception);
+ }
+
+ scope.Complete();
+ }
+ }
+
+ public override void Delete(ISagaData sagaData)
+ {
+ using (var scope = manager.GetScope())
+ using (var command = scope.Connection.CreateCommand())
+ {
+ var dialect = scope.Dialect;
+
+ command.CommandText = string.Format(
+ @"DELETE FROM {0} WHERE {1} = {2} AND {3} = {4};",
+ dialect.QuoteForTableName(sagasTableName),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("current_revision")
+ );
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.AddParameter(dialect.EscapeParameter("current_revision"), sagaData.Revision);
+
+ var rows = command.ExecuteNonQuery();
+ if (rows == 0)
+ {
+ throw new OptimisticLockingException(sagaData);
+ }
+
+ scope.Complete();
+ }
+ }
+
+ #region Fetch
+ private string GetSagaLockingClause(SqlDialect dialect)
+ {
+ if (UseSagaLocking)
+ {
+ return UseNoWaitSagaLocking
+ ? $"{dialect.SelectForUpdateClause} {dialect.SelectForNoWaitClause}"
+ : dialect.SelectForUpdateClause;
+ }
+
+ return string.Empty;
+ }
+
+ private bool ShouldIndexValue(object value)
+ {
+ if (IndexNullProperties)
+ return true;
+
+ if (value == null) return false;
+ if (value is string) return true;
+ if ((value is IEnumerable) && !(value as IEnumerable).Cast().Any()) return false;
+
+ return true;
+ }
+
+ private IDictionary GetCorrelationItems(ISagaData sagaData, IEnumerable sagaDataPropertyPathsToIndex)
+ {
+ return sagaDataPropertyPathsToIndex
+ .Select(x => new { Key = x, Value = Reflect.Value(sagaData, x) })
+ .Where(ShouldIndexValue)
+ .ToDictionary(x => x.Key, x => x.Value);
+ }
+
+ private static void Validate(object correlation)
{
- using (var scope = manager.GetScope())
- using (var command = scope.Connection.CreateCommand())
- {
- var dialect = scope.Dialect;
- var items = GetCorrelationItems(sagaData, sagaDataPropertyPathsToIndex);
- var correlations = items.Any() ? SerializeCorrelations(items) : "{}";
-
- // next, update or insert the saga
- command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
- command.AddParameter(dialect.EscapeParameter("current_revision"), sagaData.Revision);
- command.AddParameter(dialect.EscapeParameter("next_revision"), ++sagaData.Revision);
- command.AddParameter(dialect.EscapeParameter(SAGA_DATA_COLUMN), Serialize(sagaData));
- command.AddParameter(dialect.EscapeParameter(SAGA_CORRELATIONS_COLUMN), correlations);
+ var type = correlation?.GetType();
+ if (type == null)
+ return;
- command.CommandText = string.Format(
- @"UPDATE {0} SET {1} = {2}, {3} = {4}, {5} = {6} " +
- @"WHERE {7} = {8} AND {9} = {10};",
- dialect.QuoteForTableName(sagasTableName),
- dialect.QuoteForColumnName(SAGA_DATA_COLUMN), dialect.EscapeParameter(SAGA_DATA_COLUMN),
- dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("next_revision"),
- dialect.QuoteForColumnName(SAGA_CORRELATIONS_COLUMN), dialect.Cast(dialect.EscapeParameter(SAGA_CORRELATIONS_COLUMN), DbType.Object),
- dialect.QuoteForColumnName(SAGA_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN),
- dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("current_revision")
- );
- var rows = command.ExecuteNonQuery();
- if (rows == 0)
- {
- throw new OptimisticLockingException(sagaData);
- }
-
- scope.Complete();
- }
- }
-
- public override void Delete(ISagaData sagaData)
- {
- using (var scope = manager.GetScope())
- using (var command = scope.Connection.CreateCommand())
+ if (type.IsArray)
{
- var dialect = scope.Dialect;
-
- command.CommandText = string.Format(
- @"DELETE FROM {0} WHERE {1} = {2} AND {3} = {4};",
- dialect.QuoteForTableName(sagasTableName),
- dialect.QuoteForColumnName(SAGA_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN),
- dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("current_revision")
- );
- command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
- command.AddParameter(dialect.EscapeParameter("current_revision"), sagaData.Revision);
-
- var rows = command.ExecuteNonQuery();
- if (rows == 0)
- {
- throw new OptimisticLockingException(sagaData);
- }
-
- scope.Complete();
+ if (type.GetArrayRank() > 1)
+ throw new ArgumentOutOfRangeException("Multidimensional arrays are not supported.");
+
+ type = type.GetElementType();
}
- }
- #region Fetch
- private string GetSagaLockingClause(SqlDialect dialect)
- {
- if (UseSagaLocking)
+ type = Nullable.GetUnderlyingType(type) ?? type;
+ if (type.IsPrimitive || type.IsEnum)
+ return;
+
+ if (type == typeof(string) || type == typeof(Guid) || type == typeof(DateTime)
+ || type == typeof(decimal) || type == typeof(TimeSpan))
{
- return UseNoWaitSagaLocking
- ? $"{dialect.SelectForUpdateClause} {dialect.SelectForNoWaitClause}"
- : dialect.SelectForUpdateClause;
+ return;
}
- return string.Empty;
- }
-
- private bool ShouldIndexValue(object value)
- {
- if (IndexNullProperties)
- return true;
-
- if (value == null) return false;
- if (value is string) return true;
- if ((value is IEnumerable) && !(value as IEnumerable).Cast().Any()) return false;
-
- return true;
- }
-
- private IDictionary GetCorrelationItems(ISagaData sagaData, IEnumerable sagaDataPropertyPathsToIndex)
- {
- return sagaDataPropertyPathsToIndex
- .Select(x => new { Key = x, Value = Reflect.Value(sagaData, x) })
- .Where(ShouldIndexValue)
- .ToDictionary(x => x.Key, x => x.Value);
+ throw new NotSupportedException($"Type {type.Name} is not supported as a correlation value.");
}
-
- protected override string Fetch(string sagaDataPropertyPath, object fieldFromMessage)
- {
- using (var scope = manager.GetScope(autocomplete: true))
- using (var command = scope.Connection.CreateCommand())
- {
- var dialect = scope.Dialect;
- var sagaType = GetSagaTypeName(typeof(TSagaData));
-
- if (UseSagaLocking)
- {
- if (!dialect.SupportsSelectForUpdate)
- throw new InvalidOperationException(
- $"You can't use saga locking for a Dialect {dialect.GetType()} that does not supports Select For Update.");
-
- if (UseNoWaitSagaLocking && !dialect.SupportsSelectForWithNoWait)
- throw new InvalidOperationException(
- $"You can't use saga locking with no-wait for a Dialect {dialect.GetType()} that does not supports no-wait clause.");
- }
-
- if (sagaDataPropertyPath == idPropertyName)
+
+ protected override string Fetch(string sagaDataPropertyPath, object fieldFromMessage)
+ {
+ using (var scope = manager.GetScope(autocomplete: true))
+ using (var command = scope.Connection.CreateCommand())
+ {
+ var dialect = scope.Dialect;
+ var sagaType = GetSagaTypeName(typeof(TSagaData));
+
+ if (UseSagaLocking)
+ {
+ if (!dialect.SupportsSelectForUpdate)
+ throw new InvalidOperationException(
+ $"You can't use saga locking for a Dialect {dialect.GetType()} that does not supports Select For Update.");
+
+ if (UseNoWaitSagaLocking && !dialect.SupportsSelectForWithNoWait)
+ throw new InvalidOperationException(
+ $"You can't use saga locking with no-wait for a Dialect {dialect.GetType()} that does not supports no-wait clause.");
+ }
+
+ if (sagaDataPropertyPath == idPropertyName)
+ {
+ var id = (fieldFromMessage is Guid)
+ ? (Guid)fieldFromMessage
+ : Guid.Parse(fieldFromMessage.ToString());
+ var idParam = dialect.EscapeParameter("id");
+ var sagaTypeParam = dialect.EscapeParameter(SAGA_TYPE_COLUMN);
+
+ command.CommandText = string.Format(
+ @"SELECT s.{0} FROM {1} s WHERE s.{2} = {3} AND s.{4} = {5} {6}",
+ dialect.QuoteForColumnName(SAGA_DATA_COLUMN),
+ dialect.QuoteForTableName(sagasTableName),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN),
+ idParam,
+ dialect.QuoteForColumnName(SAGA_TYPE_COLUMN),
+ sagaTypeParam,
+ GetSagaLockingClause(dialect)
+ );
+ command.AddParameter(sagaTypeParam, sagaType);
+ command.AddParameter(idParam, id);
+ }
+ else
{
- var id = (fieldFromMessage is Guid)
- ? (Guid)fieldFromMessage
- : Guid.Parse(fieldFromMessage.ToString());
- var idParam = dialect.EscapeParameter("id");
- var sagaTypeParam = dialect.EscapeParameter(SAGA_TYPE_COLUMN);
-
- command.CommandText = string.Format(
- @"SELECT s.{0} FROM {1} s WHERE s.{2} = {3} AND s.{4} = {5} {6}",
- dialect.QuoteForColumnName(SAGA_DATA_COLUMN),
- dialect.QuoteForTableName(sagasTableName),
- dialect.QuoteForColumnName(SAGA_ID_COLUMN),
- idParam,
- dialect.QuoteForColumnName(SAGA_TYPE_COLUMN),
- sagaTypeParam,
- GetSagaLockingClause(dialect)
- );
- command.AddParameter(sagaTypeParam, sagaType);
- command.AddParameter(idParam, id);
- }
- else
- {
- var dataCol = dialect.QuoteForColumnName(SAGA_DATA_COLUMN);
- var sagaTblName = dialect.QuoteForTableName(sagasTableName);
- var sagaTypeCol = dialect.QuoteForColumnName(SAGA_TYPE_COLUMN);
- var sagaTypeParam = dialect.EscapeParameter(SAGA_TYPE_COLUMN);
- var sagaCorrelationsCol = dialect.QuoteForColumnName(SAGA_CORRELATIONS_COLUMN);
- var sagaCorrelationsValueParam = dialect.EscapeParameter("value");
- var sagaCorrelationsValuesParam = dialect.EscapeParameter("values");
- var forUpdate = GetSagaLockingClause(dialect);
-
- command.CommandText = $@"
- SELECT s.{dataCol}
- FROM {sagaTblName} s
- WHERE s.{sagaTypeCol} = {sagaTypeParam}
- AND (
- s.{sagaCorrelationsCol} @> {dialect.Cast(sagaCorrelationsValueParam, DbType.Object)}
- OR
- s.{sagaCorrelationsCol} @> {dialect.Cast(sagaCorrelationsValuesParam, DbType.Object)}
- )
- {forUpdate};".Replace("\t", "");
-
- var type = fieldFromMessage?.GetType();
- if (type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
- throw new NotSupportedException("Using a KeyValuePair<,> as correlation is not supported.");
-
- var value = SerializeCorrelations(new Dictionary() { { sagaDataPropertyPath, fieldFromMessage } });
- var values = SerializeCorrelations(new Dictionary() { { sagaDataPropertyPath, new[] { fieldFromMessage } } });
-
- command.AddParameter(sagaTypeParam, sagaType);
- command.AddParameter(sagaCorrelationsValueParam, DbType.String, value);
- command.AddParameter(sagaCorrelationsValuesParam, DbType.String, values);
- }
-
- try
- {
- log.Debug("Finding saga of type {0} with {1} = {2}\n{3}", sagaType, sagaDataPropertyPath,
- fieldFromMessage, command.CommandText);
- return (string)command.ExecuteScalar();
- }
- catch (DbException ex)
- {
- // When in no-wait saga-locking mode, inspect
- // exception and rethrow ex as SagaLockedException.
- if (UseSagaLocking && UseNoWaitSagaLocking)
- {
- if (dialect.IsSelectForNoWaitLockingException(ex))
- throw new AdoNetSagaLockedException(ex);
- }
-
- throw;
- }
- }
- }
-
- #endregion
- }
-}
-#endif
+ Validate(correlation: fieldFromMessage);
+
+ var dataCol = dialect.QuoteForColumnName(SAGA_DATA_COLUMN);
+ var sagaTblName = dialect.QuoteForTableName(sagasTableName);
+ var sagaTypeCol = dialect.QuoteForColumnName(SAGA_TYPE_COLUMN);
+ var sagaTypeParam = dialect.EscapeParameter(SAGA_TYPE_COLUMN);
+ var sagaCorrelationsCol = dialect.QuoteForColumnName(SAGA_CORRELATIONS_COLUMN);
+ var sagaCorrelationsValueParam = dialect.EscapeParameter("value");
+ var sagaCorrelationsValuesParam = dialect.EscapeParameter("values");
+ var forUpdate = GetSagaLockingClause(dialect);
+
+ command.CommandText = $@"
+ SELECT s.{dataCol}
+ FROM {sagaTblName} s
+ WHERE s.{sagaTypeCol} = {sagaTypeParam}
+ AND (
+ s.{sagaCorrelationsCol} @> {dialect.Cast(sagaCorrelationsValueParam, DbType.Object)}
+ OR
+ s.{sagaCorrelationsCol} @> {dialect.Cast(sagaCorrelationsValuesParam, DbType.Object)}
+ )
+ {forUpdate};".Replace("\t", "");
+
+ var value = SerializeCorrelations(new Dictionary() { { sagaDataPropertyPath, fieldFromMessage } });
+ var values = SerializeCorrelations(new Dictionary() { { sagaDataPropertyPath, new[] { fieldFromMessage } } });
+
+ command.AddParameter(sagaTypeParam, sagaType);
+ command.AddParameter(sagaCorrelationsValueParam, DbType.String, value);
+ command.AddParameter(sagaCorrelationsValuesParam, DbType.String, values);
+ }
+
+ try
+ {
+ log.Debug("Finding saga of type {0} with {1} = {2}\n{3}", sagaType, sagaDataPropertyPath,
+ fieldFromMessage, command.CommandText);
+ return (string)command.ExecuteScalar();
+ }
+ catch (DbException ex)
+ {
+ // When in no-wait saga-locking mode, inspect
+ // exception and rethrow ex as SagaLockedException.
+ if (UseSagaLocking && UseNoWaitSagaLocking)
+ {
+ if (dialect.IsSelectForNoWaitLockingException(ex))
+ throw new AdoNetSagaLockedException(ex);
+ }
+
+ throw;
+ }
+ }
+ }
+
+ #endregion
+ }
+}
+#endif
diff --git a/Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs b/Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs
index 7ec6def..e5f86ab 100644
--- a/Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs
+++ b/Rebus.AdoNet/AdoNetSagaPersisterLegacy.cs
@@ -1,856 +1,881 @@
-using System;
-using System.Data;
-using System.Linq;
-using System.Data.Common;
-using System.Collections;
-using System.Collections.Generic;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Text.RegularExpressions;
-using Newtonsoft.Json;
-
-using Rebus.Logging;
-using Rebus.Serialization;
-using Rebus.Serialization.Json;
-using Rebus.AdoNet.Schema;
-using Rebus.AdoNet.Dialects;
-
-namespace Rebus.AdoNet
-{
- ///
- /// Implements a saga persister for Rebus that stores sagas using an AdoNet provider.
- ///
- public class AdoNetSagaPersisterLegacy : AdoNetSagaPersister, IStoreSagaData, AdoNetSagaPersisterFluentConfigurer, ICanUpdateMultipleSagaDatasAtomically
- {
- private const int MaximumSagaDataTypeNameLength = 80;
- private const string SAGA_ID_COLUMN = "id";
- private const string SAGA_TYPE_COLUMN = "saga_type";
- private const string SAGA_DATA_COLUMN = "data";
- private const string SAGA_REVISION_COLUMN = "revision";
- private const string SAGAINDEX_ID_COLUMN = "saga_id";
- private const string SAGAINDEX_KEY_COLUMN = "key";
- private const string SAGAINDEX_VALUE_COLUMN = "value";
- private const string SAGAINDEX_VALUES_COLUMN = "values";
- private static ILog log;
-
- private readonly string sagasIndexTableName;
- private readonly string sagasTableName;
- private readonly string idPropertyName;
-
- static AdoNetSagaPersisterLegacy()
- {
- RebusLoggerFactory.Changed += f => log = f.GetCurrentClassLogger();
- }
-
- ///
- /// Constructs the persister with the ability to create connections to database using the specified connection string.
- /// This also means that the persister will manage the connection by itself, closing it when it has stopped using it.
- ///
- public AdoNetSagaPersisterLegacy(AdoNetUnitOfWorkManager manager, string sagasTableName, string sagasIndexTableName)
- : base(manager)
- {
- this.sagasTableName = sagasTableName;
- this.sagasIndexTableName = sagasIndexTableName;
- this.idPropertyName = Reflect.Path(x => x.Id);
- }
-
- #region AdoNetSagaPersisterFluentConfigurer
-
- ///
- /// Creates the necessary saga storage tables if they haven't already been created. If a table already exists
- /// with a name that matches one of the desired table names, no action is performed (i.e. it is assumed that
- /// the tables already exist).
- ///
- public override AdoNetSagaPersisterFluentConfigurer EnsureTablesAreCreated()
- {
- using (var uow = Manager.Create(autonomous: true))
- using (var scope = (uow as AdoNetUnitOfWork).GetScope())
- {
- var dialect = scope.Dialect;
- var connection = scope.Connection;
- var tableNames = scope.GetTableNames();
-
- // bail out if there's already a table in the database with one of the names
- var sagaTableIsAlreadyCreated = tableNames.Contains(sagasTableName, StringComparer.InvariantCultureIgnoreCase);
- var sagaIndexTableIsAlreadyCreated = tableNames.Contains(sagasIndexTableName, StringComparer.OrdinalIgnoreCase);
-
- if (sagaTableIsAlreadyCreated && sagaIndexTableIsAlreadyCreated)
- {
- log.Debug("Tables '{0}' and '{1}' already exists.", sagasTableName, sagasIndexTableName);
- return this;
- }
-
- if (sagaTableIsAlreadyCreated || sagaIndexTableIsAlreadyCreated)
- {
- // if saga index is created, then saga table is not created and vice versa
- throw new ApplicationException(string.Format("Table '{0}' do not exist - you have to create it manually",
- sagaIndexTableIsAlreadyCreated ? sagasTableName : sagasIndexTableName));
- }
-
- if (UseSqlArrays && !dialect.SupportsArrayTypes)
- {
- throw new ApplicationException("Enabled UseSqlArraysForCorrelationIndexes but underlaying database does not support arrays?!");
- }
-
- log.Info("Tables '{0}' and '{1}' do not exist - they will be created now", sagasTableName, sagasIndexTableName);
-
- using (var command = connection.CreateCommand())
- {
- command.CommandText = scope.Dialect.FormatCreateTable(
- new AdoNetTable()
- {
- Name = sagasTableName,
- Columns = new []
- {
- new AdoNetColumn() { Name = SAGA_ID_COLUMN, DbType = DbType.Guid },
- new AdoNetColumn() { Name = SAGA_TYPE_COLUMN, DbType = DbType.String, Length = MaximumSagaDataTypeNameLength },
- new AdoNetColumn() { Name = SAGA_REVISION_COLUMN, DbType = DbType.Int32 },
- new AdoNetColumn() { Name = SAGA_DATA_COLUMN, DbType = DbType.String, Length = 1073741823 }
- },
- PrimaryKey = new[] { SAGA_ID_COLUMN },
- Indexes = new []
- {
- new AdoNetIndex() { Name = $"ix_{sagasTableName}_{SAGA_ID_COLUMN}_{SAGA_TYPE_COLUMN}", Columns = new [] { SAGA_ID_COLUMN, SAGA_TYPE_COLUMN } },
- }
- }
- );
-
- command.ExecuteNonQuery();
- }
-
- using (var command = connection.CreateCommand())
- {
- command.CommandText = scope.Dialect.FormatCreateTable(
- new AdoNetTable()
- {
- Name = sagasIndexTableName,
- Columns = new []
- {
- new AdoNetColumn() { Name = SAGAINDEX_ID_COLUMN, DbType = DbType.Guid },
- new AdoNetColumn() { Name = SAGAINDEX_KEY_COLUMN, DbType = DbType.String, Length = 200 },
- new AdoNetColumn() { Name = SAGAINDEX_VALUE_COLUMN, DbType = DbType.String, Length = 1073741823, Nullable = true },
- new AdoNetColumn() { Name = SAGAINDEX_VALUES_COLUMN, DbType = DbType.String, Length = 1073741823, Nullable = true, Array = UseSqlArrays }
- },
- PrimaryKey = new[] { SAGAINDEX_ID_COLUMN, SAGAINDEX_KEY_COLUMN },
- Indexes = new []
- {
- new AdoNetIndex()
- {
- Name = $"ix_{sagasIndexTableName}_{SAGAINDEX_KEY_COLUMN}_{SAGAINDEX_VALUE_COLUMN}",
- Columns = new[] { SAGAINDEX_KEY_COLUMN, SAGAINDEX_VALUE_COLUMN },
- },
- new AdoNetIndex()
- {
- Name = $"ix_{sagasIndexTableName}_{SAGAINDEX_KEY_COLUMN}_{SAGAINDEX_VALUES_COLUMN}",
- Columns = new[] { SAGAINDEX_KEY_COLUMN, SAGAINDEX_VALUES_COLUMN },
- }
- }
- }
- );
-
- command.ExecuteNonQuery();
- }
-
- scope.Complete();
- log.Info("Tables '{0}' and '{1}' created", sagasTableName, sagasIndexTableName);
- }
-
- return this;
- }
-
- #endregion
-
- public override void Insert(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
- {
- using (var scope = Manager.GetScope())
- {
- var dialect = scope.Dialect;
- var connection = scope.Connection;
- var tableNames = scope.GetTableNames();
- var sagaTypeName = GetSagaTypeName(sagaData.GetType());
-
- // next insert the saga
- using (var command = connection.CreateCommand())
- {
- command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
- command.AddParameter(dialect.EscapeParameter(SAGA_TYPE_COLUMN), sagaTypeName);
- command.AddParameter(dialect.EscapeParameter(SAGA_REVISION_COLUMN), ++sagaData.Revision);
- command.AddParameter(dialect.EscapeParameter(SAGA_DATA_COLUMN), Serialize(sagaData));
-
- command.CommandText = string.Format(
- @"insert into {0} ({1}, {2}, {3}, {4}) values ({5}, {6}, {7}, {8});",
- dialect.QuoteForTableName(sagasTableName),
- dialect.QuoteForColumnName(SAGA_ID_COLUMN),
- dialect.QuoteForColumnName(SAGA_TYPE_COLUMN),
- dialect.QuoteForColumnName(SAGA_REVISION_COLUMN),
- dialect.QuoteForColumnName(SAGA_DATA_COLUMN),
- dialect.EscapeParameter(SAGA_ID_COLUMN),
- dialect.EscapeParameter(SAGA_TYPE_COLUMN),
- dialect.EscapeParameter(SAGA_REVISION_COLUMN),
- dialect.EscapeParameter(SAGA_DATA_COLUMN)
+using System;
+using System.Data;
+using System.Linq;
+using System.Data.Common;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Text.RegularExpressions;
+using Newtonsoft.Json;
+
+using Rebus.Logging;
+using Rebus.Serialization;
+using Rebus.Serialization.Json;
+using Rebus.AdoNet.Schema;
+using Rebus.AdoNet.Dialects;
+
+namespace Rebus.AdoNet
+{
+ ///
+ /// Implements a saga persister for Rebus that stores sagas using an AdoNet provider.
+ ///
+ public class AdoNetSagaPersisterLegacy : AdoNetSagaPersister, IStoreSagaData, AdoNetSagaPersisterFluentConfigurer, ICanUpdateMultipleSagaDatasAtomically
+ {
+ private const int MaximumSagaDataTypeNameLength = 80;
+ private const string SAGA_ID_COLUMN = "id";
+ private const string SAGA_TYPE_COLUMN = "saga_type";
+ private const string SAGA_DATA_COLUMN = "data";
+ private const string SAGA_REVISION_COLUMN = "revision";
+ private const string SAGAINDEX_ID_COLUMN = "saga_id";
+ private const string SAGAINDEX_KEY_COLUMN = "key";
+ private const string SAGAINDEX_VALUE_COLUMN = "value";
+ private const string SAGAINDEX_VALUES_COLUMN = "values";
+ private static ILog log;
+
+ private readonly string sagasIndexTableName;
+ private readonly string sagasTableName;
+ private readonly string idPropertyName;
+
+ static AdoNetSagaPersisterLegacy()
+ {
+ RebusLoggerFactory.Changed += f => log = f.GetCurrentClassLogger();
+ }
+
+ ///
+ /// Constructs the persister with the ability to create connections to database using the specified connection string.
+ /// This also means that the persister will manage the connection by itself, closing it when it has stopped using it.
+ ///
+ public AdoNetSagaPersisterLegacy(AdoNetUnitOfWorkManager manager, string sagasTableName, string sagasIndexTableName)
+ : base(manager)
+ {
+ this.sagasTableName = sagasTableName;
+ this.sagasIndexTableName = sagasIndexTableName;
+ this.idPropertyName = Reflect.Path(x => x.Id);
+ }
+
+ #region AdoNetSagaPersisterFluentConfigurer
+
+ ///
+ /// Creates the necessary saga storage tables if they haven't already been created. If a table already exists
+ /// with a name that matches one of the desired table names, no action is performed (i.e. it is assumed that
+ /// the tables already exist).
+ ///
+ public override AdoNetSagaPersisterFluentConfigurer EnsureTablesAreCreated()
+ {
+ using (var uow = Manager.Create(autonomous: true))
+ using (var scope = (uow as AdoNetUnitOfWork).GetScope())
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var tableNames = scope.GetTableNames();
+
+ // bail out if there's already a table in the database with one of the names
+ var sagaTableIsAlreadyCreated = tableNames.Contains(sagasTableName, StringComparer.InvariantCultureIgnoreCase);
+ var sagaIndexTableIsAlreadyCreated = tableNames.Contains(sagasIndexTableName, StringComparer.OrdinalIgnoreCase);
+
+ if (sagaTableIsAlreadyCreated && sagaIndexTableIsAlreadyCreated)
+ {
+ log.Debug("Tables '{0}' and '{1}' already exists.", sagasTableName, sagasIndexTableName);
+ return this;
+ }
+
+ if (sagaTableIsAlreadyCreated || sagaIndexTableIsAlreadyCreated)
+ {
+ // if saga index is created, then saga table is not created and vice versa
+ throw new ApplicationException(string.Format("Table '{0}' do not exist - you have to create it manually",
+ sagaIndexTableIsAlreadyCreated ? sagasTableName : sagasIndexTableName));
+ }
+
+ if (UseSqlArrays && !dialect.SupportsArrayTypes)
+ {
+ throw new ApplicationException("Enabled UseSqlArraysForCorrelationIndexes but underlaying database does not support arrays?!");
+ }
+
+ log.Info("Tables '{0}' and '{1}' do not exist - they will be created now", sagasTableName, sagasIndexTableName);
+
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = scope.Dialect.FormatCreateTable(
+ new AdoNetTable()
+ {
+ Name = sagasTableName,
+ Columns = new []
+ {
+ new AdoNetColumn() { Name = SAGA_ID_COLUMN, DbType = DbType.Guid },
+ new AdoNetColumn() { Name = SAGA_TYPE_COLUMN, DbType = DbType.String, Length = MaximumSagaDataTypeNameLength },
+ new AdoNetColumn() { Name = SAGA_REVISION_COLUMN, DbType = DbType.Int32 },
+ new AdoNetColumn() { Name = SAGA_DATA_COLUMN, DbType = DbType.String, Length = 1073741823 }
+ },
+ PrimaryKey = new[] { SAGA_ID_COLUMN },
+ Indexes = new []
+ {
+ new AdoNetIndex() { Name = $"ix_{sagasTableName}_{SAGA_ID_COLUMN}_{SAGA_TYPE_COLUMN}", Columns = new [] { SAGA_ID_COLUMN, SAGA_TYPE_COLUMN } },
+ }
+ }
+ );
+
+ command.ExecuteNonQuery();
+ }
+
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = scope.Dialect.FormatCreateTable(
+ new AdoNetTable()
+ {
+ Name = sagasIndexTableName,
+ Columns = new []
+ {
+ new AdoNetColumn() { Name = SAGAINDEX_ID_COLUMN, DbType = DbType.Guid },
+ new AdoNetColumn() { Name = SAGAINDEX_KEY_COLUMN, DbType = DbType.String, Length = 200 },
+ new AdoNetColumn() { Name = SAGAINDEX_VALUE_COLUMN, DbType = DbType.String, Length = 1073741823, Nullable = true },
+ new AdoNetColumn() { Name = SAGAINDEX_VALUES_COLUMN, DbType = DbType.String, Length = 1073741823, Nullable = true, Array = UseSqlArrays }
+ },
+ PrimaryKey = new[] { SAGAINDEX_ID_COLUMN, SAGAINDEX_KEY_COLUMN },
+ Indexes = new []
+ {
+ new AdoNetIndex()
+ {
+ Name = $"ix_{sagasIndexTableName}_{SAGAINDEX_KEY_COLUMN}_{SAGAINDEX_VALUE_COLUMN}",
+ Columns = new[] { SAGAINDEX_KEY_COLUMN, SAGAINDEX_VALUE_COLUMN },
+ },
+ new AdoNetIndex()
+ {
+ Name = $"ix_{sagasIndexTableName}_{SAGAINDEX_KEY_COLUMN}_{SAGAINDEX_VALUES_COLUMN}",
+ Columns = new[] { SAGAINDEX_KEY_COLUMN, SAGAINDEX_VALUES_COLUMN },
+ }
+ }
+ }
+ );
+
+ command.ExecuteNonQuery();
+ }
+
+ scope.Complete();
+ log.Info("Tables '{0}' and '{1}' created", sagasTableName, sagasIndexTableName);
+ }
+
+ return this;
+ }
+
+ #endregion
+
+ public override void Insert(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
+ {
+ using (var scope = Manager.GetScope())
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var tableNames = scope.GetTableNames();
+ var sagaTypeName = GetSagaTypeName(sagaData.GetType());
+
+ // next insert the saga
+ using (var command = connection.CreateCommand())
+ {
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.AddParameter(dialect.EscapeParameter(SAGA_TYPE_COLUMN), sagaTypeName);
+ command.AddParameter(dialect.EscapeParameter(SAGA_REVISION_COLUMN), ++sagaData.Revision);
+ command.AddParameter(dialect.EscapeParameter(SAGA_DATA_COLUMN), Serialize(sagaData));
+
+ command.CommandText = string.Format(
+ @"insert into {0} ({1}, {2}, {3}, {4}) values ({5}, {6}, {7}, {8});",
+ dialect.QuoteForTableName(sagasTableName),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN),
+ dialect.QuoteForColumnName(SAGA_TYPE_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN),
+ dialect.QuoteForColumnName(SAGA_DATA_COLUMN),
+ dialect.EscapeParameter(SAGA_ID_COLUMN),
+ dialect.EscapeParameter(SAGA_TYPE_COLUMN),
+ dialect.EscapeParameter(SAGA_REVISION_COLUMN),
+ dialect.EscapeParameter(SAGA_DATA_COLUMN)
+ );
+
+ try
+ {
+ command.ExecuteNonQuery();
+ }
+ catch (DbException exception) when (
+ dialect.IsOptimisticLockingException(exception)
+ || dialect.IsDuplicateKeyException(exception))
+ {
+ throw new OptimisticLockingException(sagaData, exception);
+ }
+ }
+
+ var propertiesToIndex = GetPropertiesToIndex(sagaData, sagaDataPropertyPathsToIndex);
+
+ if (propertiesToIndex.Any())
+ {
+ DeclareIndex(sagaData, scope, propertiesToIndex);
+ }
+
+ scope.Complete();
+ }
+ }
+
+ public override void Update(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
+ {
+ using (var scope = Manager.GetScope())
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var tableNames = scope.GetTableNames();
+
+ // next, update or insert the saga
+ using (var command = connection.CreateCommand())
+ {
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.AddParameter(dialect.EscapeParameter("current_revision"), sagaData.Revision);
+ command.AddParameter(dialect.EscapeParameter("next_revision"), ++sagaData.Revision);
+ command.AddParameter(dialect.EscapeParameter(SAGA_DATA_COLUMN), Serialize(sagaData));
+
+ command.CommandText = string.Format(
+ @"UPDATE {0} SET {1} = {2}, {3} = {4} " +
+ @"WHERE {5} = {6} AND {7} = {8};",
+ dialect.QuoteForTableName(sagasTableName),
+ dialect.QuoteForColumnName(SAGA_DATA_COLUMN), dialect.EscapeParameter(SAGA_DATA_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("next_revision"),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("current_revision")
);
try
{
- command.ExecuteNonQuery();
+ var rows = command.ExecuteNonQuery();
+ if (rows == 0)
+ {
+ throw new OptimisticLockingException(sagaData);
+ }
}
- catch (DbException exception)
+ catch (DbException exception) when (
+ dialect.IsOptimisticLockingException(exception)
+ || dialect.IsDuplicateKeyException(exception))
{
throw new OptimisticLockingException(sagaData, exception);
+ }
+ }
+
+ var propertiesToIndex = GetPropertiesToIndex(sagaData, sagaDataPropertyPathsToIndex);
+
+ if (propertiesToIndex.Any())
+ {
+ DeclareIndex(sagaData, scope, propertiesToIndex);
+ }
+
+ scope.Complete();
+ }
+ }
+
+ private void DeclareIndexUsingTableExpressions(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary 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(sagasIndexTableName), //< 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) when (
+ dialect.IsOptimisticLockingException(exception)
+ || dialect.IsDuplicateKeyException(exception))
+ {
+ throw new OptimisticLockingException(sagaData, exception);
+ }
+ }
+ }
+
+ private void DeclareIndexUsingReturningClause(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary propertiesToIndex)
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var existingKeys = Enumerable.Empty();
+
+ 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(sagasIndexTableName), //< 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(SAGAINDEX_KEY_COLUMN).ToArray();
+ }
+ }
+ catch (DbException exception) when (
+ dialect.IsOptimisticLockingException(exception)
+ || dialect.IsDuplicateKeyException(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(sagasIndexTableName), //< 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) when (
+ dialect.IsOptimisticLockingException(exception)
+ || dialect.IsDuplicateKeyException(exception))
+ {
+ throw new OptimisticLockingException(sagaData, exception);
+ }
+ }
+ }
+
+ private void DeclareIndexUnoptimized(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary propertiesToIndex)
+ {
+ var connection = scope.Connection;
+ var dialect = scope.Dialect;
+ var sagaTypeName = GetSagaTypeName(sagaData.GetType());
+
+ var idxTbl = dialect.QuoteForTableName(sagasIndexTableName);
+ 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();
+
+ // Let's fetch existing keys..
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = string.Format(
+ "SELECT {1} FROM {0} WHERE {2} = {3};",
+ dialect.QuoteForTableName(sagasIndexTableName), //< 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(SAGAINDEX_KEY_COLUMN).ToArray();
+ }
+ }
+ catch (DbException exception) when (
+ dialect.IsOptimisticLockingException(exception)
+ || dialect.IsDuplicateKeyException(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(sagasIndexTableName), //< 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();
}
- }
-
- var propertiesToIndex = GetPropertiesToIndex(sagaData, sagaDataPropertyPathsToIndex);
-
- if (propertiesToIndex.Any())
- {
- DeclareIndex(sagaData, scope, propertiesToIndex);
- }
-
- scope.Complete();
- }
- }
-
- public override void Update(ISagaData sagaData, string[] sagaDataPropertyPathsToIndex)
- {
- using (var scope = Manager.GetScope())
- {
- var dialect = scope.Dialect;
- var connection = scope.Connection;
- var tableNames = scope.GetTableNames();
-
- // next, update or insert the saga
- using (var command = connection.CreateCommand())
- {
- command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
- command.AddParameter(dialect.EscapeParameter("current_revision"), sagaData.Revision);
- command.AddParameter(dialect.EscapeParameter("next_revision"), ++sagaData.Revision);
- command.AddParameter(dialect.EscapeParameter(SAGA_DATA_COLUMN), Serialize(sagaData));
-
- command.CommandText = string.Format(
- @"UPDATE {0} SET {1} = {2}, {3} = {4} " +
- @"WHERE {5} = {6} AND {7} = {8};",
- dialect.QuoteForTableName(sagasTableName),
- dialect.QuoteForColumnName(SAGA_DATA_COLUMN), dialect.EscapeParameter(SAGA_DATA_COLUMN),
- dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("next_revision"),
- dialect.QuoteForColumnName(SAGA_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN),
- dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("current_revision")
- );
- var rows = command.ExecuteNonQuery();
- if (rows == 0)
- {
- throw new OptimisticLockingException(sagaData);
- }
- }
-
- var propertiesToIndex = GetPropertiesToIndex(sagaData, sagaDataPropertyPathsToIndex);
-
- if (propertiesToIndex.Any())
- {
- DeclareIndex(sagaData, scope, propertiesToIndex);
- }
-
- scope.Complete();
- }
- }
-
- private void DeclareIndexUsingTableExpressions(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary 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(sagasIndexTableName), //< 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);
- }
- }
- }
-
- private void DeclareIndexUsingReturningClause(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary propertiesToIndex)
- {
- var dialect = scope.Dialect;
- var connection = scope.Connection;
- var existingKeys = Enumerable.Empty();
-
- 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(sagasIndexTableName), //< 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(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(sagasIndexTableName), //< 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);
- }
- }
- }
-
- private void DeclareIndexUnoptimized(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary propertiesToIndex)
- {
- var connection = scope.Connection;
- var dialect = scope.Dialect;
- var sagaTypeName = GetSagaTypeName(sagaData.GetType());
-
- var idxTbl = dialect.QuoteForTableName(sagasIndexTableName);
- 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();
-
- // Let's fetch existing keys..
- using (var command = connection.CreateCommand())
- {
- command.CommandText = string.Format(
- "SELECT {1} FROM {0} WHERE {2} = {3};",
- dialect.QuoteForTableName(sagasIndexTableName), //< 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(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(sagasIndexTableName), //< 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)
+ catch (DbException exception) when (
+ dialect.IsOptimisticLockingException(exception)
+ || dialect.IsDuplicateKeyException(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(sagasIndexTableName), //< 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();
}
- }
- }
-
- 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(sagasIndexTableName), //< 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)
+ catch (DbException exception) when (
+ dialect.IsOptimisticLockingException(exception)
+ || dialect.IsDuplicateKeyException(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(sagasIndexTableName), //< 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();
}
- }
- }
-
- 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(sagasIndexTableName), //< 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)
+ catch (DbException exception) when (
+ dialect.IsOptimisticLockingException(exception)
+ || dialect.IsDuplicateKeyException(exception))
{
throw new OptimisticLockingException(sagaData, exception);
- }
-
- }
- }
- }
-
- private void DeclareIndex(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary propertiesToIndex)
- {
- var dialect = scope.Dialect;
-
- if (dialect.SupportsOnConflictClause && dialect.SupportsReturningClause && dialect.SupportsTableExpressions)
- {
- DeclareIndexUsingTableExpressions(sagaData, scope, propertiesToIndex);
- }
- else if (dialect.SupportsOnConflictClause && dialect.SupportsReturningClause)
- {
- DeclareIndexUsingReturningClause(sagaData, scope, propertiesToIndex);
- }
- else
- {
- DeclareIndexUnoptimized(sagaData, scope, propertiesToIndex);
- }
- }
-
- public override void Delete(ISagaData sagaData)
- {
- using (var scope = Manager.GetScope())
- {
- var dialect = scope.Dialect;
- var connection = scope.Connection;
-
- using (var command = connection.CreateCommand())
- {
- command.CommandText = string.Format(
- @"DELETE FROM {0} WHERE {1} = {2} AND {3} = {4};",
- dialect.QuoteForTableName(sagasTableName),
- dialect.QuoteForColumnName(SAGA_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN),
- dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("current_revision")
- );
- command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
- command.AddParameter(dialect.EscapeParameter("current_revision"), sagaData.Revision);
-
- var rows = command.ExecuteNonQuery();
-
- if (rows == 0)
- {
- throw new OptimisticLockingException(sagaData);
- }
- }
-
- using (var command = connection.CreateCommand())
- {
- command.CommandText = string.Format(
- @"DELETE FROM {0} WHERE {1} = {2};",
- dialect.QuoteForTableName(sagasIndexTableName),
- dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN)
- );
- command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
- command.ExecuteNonQuery();
- }
-
- scope.Complete();
- }
- }
-
- private string GetSagaLockingClause(SqlDialect dialect)
- {
- if (UseSagaLocking)
- {
- return UseNoWaitSagaLocking
- ? $"{dialect.SelectForUpdateClause} {dialect.SelectForNoWaitClause}"
- : dialect.SelectForUpdateClause;
- }
-
- return string.Empty;
- }
-
- private bool ArraysEnabledFor(SqlDialect dialect)
- {
- return UseSqlArrays && dialect.SupportsArrayTypes;
- }
-
- private bool ShouldIndexValue(object value)
- {
- if (IndexNullProperties)
- return true;
-
- if (value == null) return false;
- if (value is string) return true;
- if ((value is IEnumerable) && !(value as IEnumerable).Cast().Any()) return false;
-
- return true;
- }
-
- private IDictionary GetPropertiesToIndex(ISagaData sagaData, IEnumerable sagaDataPropertyPathsToIndex)
- {
- return sagaDataPropertyPathsToIndex
- .Select(x => new { Key = x, Value = Reflect.Value(sagaData, x) })
- .Where(ShouldIndexValue)
- .ToDictionary(x => x.Key, x => x.Value);
- }
-
- private static string GetIndexValue(object value)
- {
- if (value is string)
- {
- return value as string;
- }
- else if (value == null || value is IEnumerable)
- {
- return null;
- }
-
- return Convert.ToString(value);
- }
-
- private static IEnumerable GetIndexValues(object value)
- {
- if (!(value is IEnumerable) || value is string)
- {
- return null;
- }
-
- return (value as IEnumerable).Cast().Select(x => Convert.ToString(x)).ToArray();
- }
-
- private static string GetConcatenatedIndexValues(IEnumerable values)
- {
- if (values == null || !values.Any())
- {
- return null;
- }
-
- var sb = new StringBuilder(values.Sum(x => x.Length + 1) + 1);
- sb.Append('|');
-
- foreach (var value in values)
- {
- sb.Append(value);
- sb.Append('|');
- }
-
- return sb.ToString();
- }
-
- protected override string Fetch(string sagaDataPropertyPath, object fieldFromMessage)
- {
- using (var scope = Manager.GetScope(autocomplete: true))
- {
- var dialect = scope.Dialect;
- var connection = scope.Connection;
- var sagaType = GetSagaTypeName(typeof(TSagaData));
-
- if (UseSagaLocking)
- {
- if (!dialect.SupportsSelectForUpdate)
- throw new InvalidOperationException($"You can't use saga locking for a Dialect {dialect.GetType()} that does not supports Select For Update.");
-
- if (UseNoWaitSagaLocking && !dialect.SupportsSelectForWithNoWait)
- throw new InvalidOperationException($"You can't use saga locking with no-wait for a Dialect {dialect.GetType()} that does not supports no-wait clause.");
- }
-
- using (var command = connection.CreateCommand())
- {
- if (sagaDataPropertyPath == idPropertyName)
- {
- var id = (fieldFromMessage is Guid) ? (Guid)fieldFromMessage : Guid.Parse(fieldFromMessage.ToString());
- var idParam = dialect.EscapeParameter("id");
- var sagaTypeParam = dialect.EscapeParameter(SAGA_TYPE_COLUMN);
-
- command.CommandText = string.Format(
- @"SELECT s.{0} FROM {1} s WHERE s.{2} = {3} AND s.{4} = {5} {6}",
- dialect.QuoteForColumnName(SAGA_DATA_COLUMN),
- dialect.QuoteForTableName(sagasTableName),
- dialect.QuoteForColumnName(SAGA_ID_COLUMN),
- idParam,
- dialect.QuoteForColumnName(SAGA_TYPE_COLUMN),
- sagaTypeParam,
- GetSagaLockingClause(dialect)
- );
- command.AddParameter(sagaTypeParam, sagaType);
- command.AddParameter(idParam, id);
- }
- else
- {
- var dataCol = dialect.QuoteForColumnName(SAGA_DATA_COLUMN);
- var sagaTblName = dialect.QuoteForTableName(sagasTableName);
- var sagaTypeCol = dialect.QuoteForColumnName(SAGA_TYPE_COLUMN);
- var sagaTypeParam = dialect.EscapeParameter(SAGA_TYPE_COLUMN);
- var indexTblName = dialect.QuoteForTableName(sagasIndexTableName);
- var sagaIdCol = dialect.QuoteForColumnName(SAGA_ID_COLUMN);
- var indexIdCol = dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN);
- var indexKeyCol = dialect.QuoteForColumnName(SAGAINDEX_KEY_COLUMN);
- var indexKeyParam = dialect.EscapeParameter(SAGAINDEX_KEY_COLUMN);
- var indexValueCol = dialect.QuoteForColumnName(SAGAINDEX_VALUE_COLUMN);
- var indexValueParm = dialect.EscapeParameter(SAGAINDEX_VALUE_COLUMN);
- var indexValuesCol = dialect.QuoteForColumnName(SAGAINDEX_VALUES_COLUMN);
- var indexValuesParm = dialect.EscapeParameter(SAGAINDEX_VALUES_COLUMN);
- var forUpdate = GetSagaLockingClause(dialect);
- var valuesPredicate = ArraysEnabledFor(dialect)
- ? dialect.FormatArrayAny($"i.{indexValuesCol}", indexValuesParm)
- : $"(i.{indexValuesCol} LIKE ('%' || {indexValuesParm} || '%'))";
-
- command.CommandText = $@"
- SELECT s.{dataCol}
- FROM {sagaTblName} s
- JOIN {indexTblName} i on s.{sagaIdCol} = i.{indexIdCol}
- WHERE s.{sagaTypeCol} = {sagaTypeParam}
- AND i.{indexKeyCol} = {indexKeyParam}
- AND (
- CASE WHEN {indexValueParm} IS NULL THEN i.{indexValueCol} IS NULL
- ELSE
- (
- i.{indexValueCol} = {indexValueParm}
- OR
- (i.{indexValuesCol} is NOT NULL AND {valuesPredicate})
- )
- END
- )
- {forUpdate};".Replace("\t", "");
-
- var value = GetIndexValue(fieldFromMessage);
- var values = value == null ? DBNull.Value : ArraysEnabledFor(dialect)
- ? (object)(new[] { value })
- : GetConcatenatedIndexValues(new[] { value });
- var valuesDbType = ArraysEnabledFor(dialect) ? DbType.Object : DbType.String;
-
- command.AddParameter(indexKeyParam, sagaDataPropertyPath);
- command.AddParameter(sagaTypeParam, sagaType);
- command.AddParameter(indexValueParm, DbType.String, value);
- command.AddParameter(indexValuesParm, valuesDbType, values);
- }
-
-
- try
- {
- log.Debug("Finding saga of type {0} with {1} = {2}", sagaType, sagaDataPropertyPath, fieldFromMessage);
- return (string)command.ExecuteScalar();
- }
- catch (DbException ex)
- {
- // When in no-wait saga-locking mode, inspect
- // exception and rethrow ex as SagaLockedException.
- if (UseSagaLocking && UseNoWaitSagaLocking)
- {
- if (dialect.IsSelectForNoWaitLockingException(ex))
- throw new AdoNetSagaLockedException(ex);
- }
-
- throw;
- }
- }
- }
- }
- }
-}
+ }
+ }
+ }
+ }
+
+ private void DeclareIndex(ISagaData sagaData, AdoNetUnitOfWorkScope scope, IDictionary propertiesToIndex)
+ {
+ var dialect = scope.Dialect;
+
+ if (dialect.SupportsOnConflictClause && dialect.SupportsReturningClause && dialect.SupportsTableExpressions)
+ {
+ DeclareIndexUsingTableExpressions(sagaData, scope, propertiesToIndex);
+ }
+ else if (dialect.SupportsOnConflictClause && dialect.SupportsReturningClause)
+ {
+ DeclareIndexUsingReturningClause(sagaData, scope, propertiesToIndex);
+ }
+ else
+ {
+ DeclareIndexUnoptimized(sagaData, scope, propertiesToIndex);
+ }
+ }
+
+ public override void Delete(ISagaData sagaData)
+ {
+ using (var scope = Manager.GetScope())
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = string.Format(
+ @"DELETE FROM {0} WHERE {1} = {2} AND {3} = {4};",
+ dialect.QuoteForTableName(sagasTableName),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN),
+ dialect.QuoteForColumnName(SAGA_REVISION_COLUMN), dialect.EscapeParameter("current_revision")
+ );
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.AddParameter(dialect.EscapeParameter("current_revision"), sagaData.Revision);
+
+ var rows = command.ExecuteNonQuery();
+
+ if (rows == 0)
+ {
+ throw new OptimisticLockingException(sagaData);
+ }
+ }
+
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = string.Format(
+ @"DELETE FROM {0} WHERE {1} = {2};",
+ dialect.QuoteForTableName(sagasIndexTableName),
+ dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN), dialect.EscapeParameter(SAGA_ID_COLUMN)
+ );
+ command.AddParameter(dialect.EscapeParameter(SAGA_ID_COLUMN), sagaData.Id);
+ command.ExecuteNonQuery();
+ }
+
+ scope.Complete();
+ }
+ }
+
+ private string GetSagaLockingClause(SqlDialect dialect)
+ {
+ if (UseSagaLocking)
+ {
+ return UseNoWaitSagaLocking
+ ? $"{dialect.SelectForUpdateClause} {dialect.SelectForNoWaitClause}"
+ : dialect.SelectForUpdateClause;
+ }
+
+ return string.Empty;
+ }
+
+ private bool ArraysEnabledFor(SqlDialect dialect)
+ {
+ return UseSqlArrays && dialect.SupportsArrayTypes;
+ }
+
+ private bool ShouldIndexValue(object value)
+ {
+ if (IndexNullProperties)
+ return true;
+
+ if (value == null) return false;
+ if (value is string) return true;
+ if ((value is IEnumerable) && !(value as IEnumerable).Cast().Any()) return false;
+
+ return true;
+ }
+
+ private IDictionary GetPropertiesToIndex(ISagaData sagaData, IEnumerable sagaDataPropertyPathsToIndex)
+ {
+ return sagaDataPropertyPathsToIndex
+ .Select(x => new { Key = x, Value = Reflect.Value(sagaData, x) })
+ .Where(ShouldIndexValue)
+ .ToDictionary(x => x.Key, x => x.Value);
+ }
+
+ private static string GetIndexValue(object value)
+ {
+ if (value is string)
+ {
+ return value as string;
+ }
+ else if (value == null || value is IEnumerable)
+ {
+ return null;
+ }
+
+ return Convert.ToString(value);
+ }
+
+ private static IEnumerable GetIndexValues(object value)
+ {
+ if (!(value is IEnumerable) || value is string)
+ {
+ return null;
+ }
+
+ return (value as IEnumerable).Cast().Select(x => Convert.ToString(x)).ToArray();
+ }
+
+ private static string GetConcatenatedIndexValues(IEnumerable values)
+ {
+ if (values == null || !values.Any())
+ {
+ return null;
+ }
+
+ var sb = new StringBuilder(values.Sum(x => x.Length + 1) + 1);
+ sb.Append('|');
+
+ foreach (var value in values)
+ {
+ sb.Append(value);
+ sb.Append('|');
+ }
+
+ return sb.ToString();
+ }
+
+ protected override string Fetch(string sagaDataPropertyPath, object fieldFromMessage)
+ {
+ using (var scope = Manager.GetScope(autocomplete: true))
+ {
+ var dialect = scope.Dialect;
+ var connection = scope.Connection;
+ var sagaType = GetSagaTypeName(typeof(TSagaData));
+
+ if (UseSagaLocking)
+ {
+ if (!dialect.SupportsSelectForUpdate)
+ throw new InvalidOperationException($"You can't use saga locking for a Dialect {dialect.GetType()} that does not supports Select For Update.");
+
+ if (UseNoWaitSagaLocking && !dialect.SupportsSelectForWithNoWait)
+ throw new InvalidOperationException($"You can't use saga locking with no-wait for a Dialect {dialect.GetType()} that does not supports no-wait clause.");
+ }
+
+ using (var command = connection.CreateCommand())
+ {
+ if (sagaDataPropertyPath == idPropertyName)
+ {
+ var id = (fieldFromMessage is Guid) ? (Guid)fieldFromMessage : Guid.Parse(fieldFromMessage.ToString());
+ var idParam = dialect.EscapeParameter("id");
+ var sagaTypeParam = dialect.EscapeParameter(SAGA_TYPE_COLUMN);
+
+ command.CommandText = string.Format(
+ @"SELECT s.{0} FROM {1} s WHERE s.{2} = {3} AND s.{4} = {5} {6}",
+ dialect.QuoteForColumnName(SAGA_DATA_COLUMN),
+ dialect.QuoteForTableName(sagasTableName),
+ dialect.QuoteForColumnName(SAGA_ID_COLUMN),
+ idParam,
+ dialect.QuoteForColumnName(SAGA_TYPE_COLUMN),
+ sagaTypeParam,
+ GetSagaLockingClause(dialect)
+ );
+ command.AddParameter(sagaTypeParam, sagaType);
+ command.AddParameter(idParam, id);
+ }
+ else
+ {
+ var dataCol = dialect.QuoteForColumnName(SAGA_DATA_COLUMN);
+ var sagaTblName = dialect.QuoteForTableName(sagasTableName);
+ var sagaTypeCol = dialect.QuoteForColumnName(SAGA_TYPE_COLUMN);
+ var sagaTypeParam = dialect.EscapeParameter(SAGA_TYPE_COLUMN);
+ var indexTblName = dialect.QuoteForTableName(sagasIndexTableName);
+ var sagaIdCol = dialect.QuoteForColumnName(SAGA_ID_COLUMN);
+ var indexIdCol = dialect.QuoteForColumnName(SAGAINDEX_ID_COLUMN);
+ var indexKeyCol = dialect.QuoteForColumnName(SAGAINDEX_KEY_COLUMN);
+ var indexKeyParam = dialect.EscapeParameter(SAGAINDEX_KEY_COLUMN);
+ var indexValueCol = dialect.QuoteForColumnName(SAGAINDEX_VALUE_COLUMN);
+ var indexValueParm = dialect.EscapeParameter(SAGAINDEX_VALUE_COLUMN);
+ var indexValuesCol = dialect.QuoteForColumnName(SAGAINDEX_VALUES_COLUMN);
+ var indexValuesParm = dialect.EscapeParameter(SAGAINDEX_VALUES_COLUMN);
+ var forUpdate = GetSagaLockingClause(dialect);
+ var valuesPredicate = ArraysEnabledFor(dialect)
+ ? dialect.FormatArrayAny($"i.{indexValuesCol}", indexValuesParm)
+ : $"(i.{indexValuesCol} LIKE ('%' || {indexValuesParm} || '%'))";
+
+ command.CommandText = $@"
+ SELECT s.{dataCol}
+ FROM {sagaTblName} s
+ JOIN {indexTblName} i on s.{sagaIdCol} = i.{indexIdCol}
+ WHERE s.{sagaTypeCol} = {sagaTypeParam}
+ AND i.{indexKeyCol} = {indexKeyParam}
+ AND (
+ CASE WHEN {indexValueParm} IS NULL THEN i.{indexValueCol} IS NULL
+ ELSE
+ (
+ i.{indexValueCol} = {indexValueParm}
+ OR
+ (i.{indexValuesCol} is NOT NULL AND {valuesPredicate})
+ )
+ END
+ )
+ {forUpdate};".Replace("\t", "");
+
+ var value = GetIndexValue(fieldFromMessage);
+ var values = value == null ? DBNull.Value : ArraysEnabledFor(dialect)
+ ? (object)(new[] { value })
+ : GetConcatenatedIndexValues(new[] { value });
+ var valuesDbType = ArraysEnabledFor(dialect) ? DbType.Object : DbType.String;
+
+ command.AddParameter(indexKeyParam, sagaDataPropertyPath);
+ command.AddParameter(sagaTypeParam, sagaType);
+ command.AddParameter(indexValueParm, DbType.String, value);
+ command.AddParameter(indexValuesParm, valuesDbType, values);
+ }
+
+
+ try
+ {
+ log.Debug("Finding saga of type {0} with {1} = {2}", sagaType, sagaDataPropertyPath, fieldFromMessage);
+ return (string)command.ExecuteScalar();
+ }
+ catch (DbException ex)
+ {
+ // When in no-wait saga-locking mode, inspect
+ // exception and rethrow ex as SagaLockedException.
+ if (UseSagaLocking && UseNoWaitSagaLocking)
+ {
+ if (dialect.IsSelectForNoWaitLockingException(ex))
+ throw new AdoNetSagaLockedException(ex);
+ }
+
+ throw;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Rebus.AdoNet/Dialects/PostgreSql95Dialect.cs b/Rebus.AdoNet/Dialects/PostgreSql95Dialect.cs
index 7e1d829..03705c0 100644
--- a/Rebus.AdoNet/Dialects/PostgreSql95Dialect.cs
+++ b/Rebus.AdoNet/Dialects/PostgreSql95Dialect.cs
@@ -5,7 +5,7 @@ namespace Rebus.AdoNet.Dialects
{
public class PostgreSql95Dialect : PostgreSql94Dialect
{
- protected override Version MinimumDatabaseVersion => new Version("9.5");
+ protected override Version MinimumDatabaseVersion => new Version("9.5");
public override ushort Priority => 95;
public override bool SupportsOnConflictClause => true;
diff --git a/Rebus.AdoNet/Dialects/PostgreSqlDialect.cs b/Rebus.AdoNet/Dialects/PostgreSqlDialect.cs
index ba91ea5..7a2f4e8 100644
--- a/Rebus.AdoNet/Dialects/PostgreSqlDialect.cs
+++ b/Rebus.AdoNet/Dialects/PostgreSqlDialect.cs
@@ -74,7 +74,7 @@ public override bool SupportsThisDialect(IDbConnection connection)
return false;
}
}
-
+
public override bool IsSelectForNoWaitLockingException(DbException ex)
{
if (ex != null && _postgresExceptionNames.Contains(ex.GetType().Name))
@@ -97,6 +97,17 @@ public override bool IsDuplicateKeyException(DbException ex)
return false;
}
+ public override bool IsOptimisticLockingException(DbException ex)
+ {
+ if (ex != null && _postgresExceptionNames.Contains(ex.GetType().Name))
+ {
+ var psqlex = new PostgreSqlExceptionAdapter(ex);
+ return psqlex.Code == "40001";
+ }
+
+ return false;
+ }
+
#endregion
#region GetColumnType
@@ -145,3 +156,4 @@ public override string FormatArrayAny(string arg1, string arg2)
#endregion
}
}
+
diff --git a/Rebus.AdoNet/Dialects/SqlDialect.cs b/Rebus.AdoNet/Dialects/SqlDialect.cs
index 16dbca9..fba9f94 100644
--- a/Rebus.AdoNet/Dialects/SqlDialect.cs
+++ b/Rebus.AdoNet/Dialects/SqlDialect.cs
@@ -139,11 +139,6 @@ public virtual string GetLongestTypeName(DbType dbType)
return _typeNames.GetLongest(dbType);
}
- ///
- /// Reverse search for *testing purpose* in order to get (for each dialect) the related DBType..
- ///
- internal virtual DbType GetDbTypeFor(string name) => _typeNames.Defaults.First(x => x.Value.ToLowerInvariant() == name.ToLowerInvariant()).Key;
-
#endregion
#region Identifier quoting support
@@ -429,8 +424,8 @@ public virtual string FormatCreateTable(AdoNetTable table)
{
var primaryKey = (!table.HasCompositePrimaryKey && (bool)table.PrimaryKey?.Any(x => x == column.Name));
var @default = column.Default == null ? "" : column.Default is string
- ? Quote(column.Default as string)
- : column.Default.ToString();
+ ? Quote(column.Default as string)
+ : column.Default.ToString();
sb.AppendFormat(" {0} {1} {2} {3} {4}",
QuoteForColumnName(column.Name),
@@ -570,6 +565,12 @@ public virtual string FormatArrayAny(string arg1, string arg2)
public virtual string JsonColumnGinPathIndexOpclass => "";
#endregion
+ #region IsOptimisticException
+
+ public abstract bool IsOptimisticLockingException(DbException ex);
+
+ #endregion
+
#region SqlDialects Registry
private static readonly IList _dialects =
diff --git a/Rebus.AdoNet/Dialects/SqliteDialect.cs b/Rebus.AdoNet/Dialects/SqliteDialect.cs
index 65cefde..bea46f4 100644
--- a/Rebus.AdoNet/Dialects/SqliteDialect.cs
+++ b/Rebus.AdoNet/Dialects/SqliteDialect.cs
@@ -66,5 +66,20 @@ public override string GetColumnType(DbType type, uint length, uint precision, u
"INTEGER PRIMARY KEY AUTOINCREMENT" //< This is all you can get from sqlite's identity/auto-increment support.
: base.GetColumnType(type, length, precision, scale, identity, array, primary);
}
+
+ public override bool IsDuplicateKeyException(DbException ex)
+ {
+ // See: https://www.sqlite.org/rescode.html
+ return ex.ErrorCode == 19 //< CONSTRAINT
+ || ex.ErrorCode == 2067; //< UNIQUE
+ }
+
+ public override bool IsOptimisticLockingException(DbException ex)
+ {
+ // See: https://www.sqlite.org/rescode.html
+ return ex.ErrorCode == 5 //< BUSY
+ || ex.ErrorCode == 6; //< LOCKED
+ }
}
}
+
diff --git a/Rebus.AdoNet/Dialects/TypeNames.cs b/Rebus.AdoNet/Dialects/TypeNames.cs
index 5cca450..4959586 100755
--- a/Rebus.AdoNet/Dialects/TypeNames.cs
+++ b/Rebus.AdoNet/Dialects/TypeNames.cs
@@ -1,182 +1,176 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Data;
-using System.Text;
-
-namespace Rebus.AdoNet.Dialects
-{
- ///
- /// This class maps a DbType to names.
- ///
- ///
- /// Associations may be marked with a capacity. Calling the Get()
- /// method with a type and actual size n will return the associated
- /// name with smallest capacity >= n, if available and an unmarked
- /// default type otherwise.
- /// Eg, setting
- ///
- /// Names.Put(DbType, "TEXT" );
- /// Names.Put(DbType, 255, "VARCHAR($l)" );
- /// Names.Put(DbType, 65534, "LONGVARCHAR($l)" );
- ///
- /// will give you back the following:
- ///
- /// Names.Get(DbType) // --> "TEXT" (default)
- /// Names.Get(DbType,100) // --> "VARCHAR(100)" (100 is in [0:255])
- /// Names.Get(DbType,1000) // --> "LONGVARCHAR(1000)" (100 is in [256:65534])
- /// Names.Get(DbType,100000) // --> "TEXT" (default)
- ///
- /// On the other hand, simply putting
- ///
- /// Names.Put(DbType, "VARCHAR($l)" );
- ///
- /// would result in
- ///
- /// Names.Get(DbType) // --> "VARCHAR($l)" (will cause trouble)
- /// Names.Get(DbType,100) // --> "VARCHAR(100)"
- /// Names.Get(DbType,1000) // --> "VARCHAR(1000)"
- /// Names.Get(DbType,10000) // --> "VARCHAR(10000)"
- ///
- ///
- public class TypeNames
- {
- public const string LengthPlaceHolder = "$l";
- public const string PrecisionPlaceHolder = "$p";
- public const string ScalePlaceHolder = "$s";
-
- private readonly Dictionary> weighted = new Dictionary>();
- private readonly Dictionary defaults = new Dictionary();
-
- ///
- /// Expose defaults as a readonly collection for testing purpose..
- ///
- internal IReadOnlyDictionary Defaults => new ReadOnlyDictionary(defaults);
-
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- private static string ReplaceOnce(string template, string placeholder, string replacement)
- {
- int loc = template.IndexOf(placeholder, StringComparison.Ordinal);
- if (loc < 0)
- {
- return template;
- }
- else
- {
- return new StringBuilder(template.Substring(0, loc))
- .Append(replacement)
- .Append(template.Substring(loc + placeholder.Length))
- .ToString();
- }
- }
-
- ///
- /// Replaces the specified type.
- ///
- /// The type.
- /// The size.
- /// The precision.
- /// The scale.
- ///
- private static string Replace(string type, uint size, uint precision, uint scale)
- {
- type = ReplaceOnce(type, LengthPlaceHolder, size.ToString());
- type = ReplaceOnce(type, ScalePlaceHolder, scale.ToString());
- return ReplaceOnce(type, PrecisionPlaceHolder, precision.ToString());
- }
-
- ///
- /// Get default type name for specified type
- ///
- /// the type key
- /// the default type name associated with the specified key
- public string Get(DbType typecode)
- {
- string result;
- if (!defaults.TryGetValue(typecode, out result))
- {
- throw new ArgumentException("Dialect does not support DbType." + typecode, "typecode");
- }
- return result;
- }
-
- ///
- /// Get the type name specified type and size
- ///
- /// the type key
- /// the SQL length
- /// the SQL scale
- /// the SQL precision
- ///
- /// The associated name with smallest capacity >= size if available and the
- /// default type name otherwise
- ///
- public string Get(DbType typecode, uint size, uint precision, uint scale)
- {
- SortedList map;
- weighted.TryGetValue(typecode, out map);
- if (map != null && map.Count > 0)
- {
- foreach (KeyValuePair entry in map)
- {
- if (size <= entry.Key)
- {
- return Replace(entry.Value, size, precision, scale);
- }
- }
- }
- //Could not find a specific type for the size, using the default
- return Replace(Get(typecode), size, precision, scale);
- }
-
- ///
- /// For types with a simple length, this method returns the definition
- /// for the longest registered type.
- ///
- ///
- ///
- public string GetLongest(DbType typecode)
- {
- SortedList map;
- weighted.TryGetValue(typecode, out map);
-
- if (map != null && map.Count > 0)
- return Replace(map.Values[map.Count - 1], map.Keys[map.Count - 1], 0, 0);
-
- return Get(typecode);
- }
-
- ///
- /// Set a type name for specified type key and capacity
- ///
- /// the type key
- /// the (maximum) type size/length
- /// The associated name
- public void Put(DbType typecode, uint capacity, string value)
- {
- SortedList map;
- if (!weighted.TryGetValue(typecode, out map))
- {
- // add new ordered map
- weighted[typecode] = map = new SortedList();
- }
- map[capacity] = value;
- }
-
- ///
- ///
- ///
- ///
- ///
- public void Put(DbType typecode, string value)
- {
- defaults[typecode] = value;
- }
- }
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Text;
+
+namespace Rebus.AdoNet.Dialects
+{
+ ///
+ /// This class maps a DbType to names.
+ ///
+ ///
+ /// Associations may be marked with a capacity. Calling the Get()
+ /// method with a type and actual size n will return the associated
+ /// name with smallest capacity >= n, if available and an unmarked
+ /// default type otherwise.
+ /// Eg, setting
+ ///
+ /// Names.Put(DbType, "TEXT" );
+ /// Names.Put(DbType, 255, "VARCHAR($l)" );
+ /// Names.Put(DbType, 65534, "LONGVARCHAR($l)" );
+ ///
+ /// will give you back the following:
+ ///
+ /// Names.Get(DbType) // --> "TEXT" (default)
+ /// Names.Get(DbType,100) // --> "VARCHAR(100)" (100 is in [0:255])
+ /// Names.Get(DbType,1000) // --> "LONGVARCHAR(1000)" (100 is in [256:65534])
+ /// Names.Get(DbType,100000) // --> "TEXT" (default)
+ ///
+ /// On the other hand, simply putting
+ ///
+ /// Names.Put(DbType, "VARCHAR($l)" );
+ ///
+ /// would result in
+ ///
+ /// Names.Get(DbType) // --> "VARCHAR($l)" (will cause trouble)
+ /// Names.Get(DbType,100) // --> "VARCHAR(100)"
+ /// Names.Get(DbType,1000) // --> "VARCHAR(1000)"
+ /// Names.Get(DbType,10000) // --> "VARCHAR(10000)"
+ ///
+ ///
+ public class TypeNames
+ {
+ public const string LengthPlaceHolder = "$l";
+ public const string PrecisionPlaceHolder = "$p";
+ public const string ScalePlaceHolder = "$s";
+
+ private readonly Dictionary> weighted = new Dictionary>();
+ private readonly Dictionary defaults = new Dictionary();
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static string ReplaceOnce(string template, string placeholder, string replacement)
+ {
+ int loc = template.IndexOf(placeholder, StringComparison.Ordinal);
+ if (loc < 0)
+ {
+ return template;
+ }
+ else
+ {
+ return new StringBuilder(template.Substring(0, loc))
+ .Append(replacement)
+ .Append(template.Substring(loc + placeholder.Length))
+ .ToString();
+ }
+ }
+
+ ///
+ /// Replaces the specified type.
+ ///
+ /// The type.
+ /// The size.
+ /// The precision.
+ /// The scale.
+ ///
+ private static string Replace(string type, uint size, uint precision, uint scale)
+ {
+ type = ReplaceOnce(type, LengthPlaceHolder, size.ToString());
+ type = ReplaceOnce(type, ScalePlaceHolder, scale.ToString());
+ return ReplaceOnce(type, PrecisionPlaceHolder, precision.ToString());
+ }
+
+ ///
+ /// Get default type name for specified type
+ ///
+ /// the type key
+ /// the default type name associated with the specified key
+ public string Get(DbType typecode)
+ {
+ string result;
+ if (!defaults.TryGetValue(typecode, out result))
+ {
+ throw new ArgumentException("Dialect does not support DbType." + typecode, "typecode");
+ }
+ return result;
+ }
+
+ ///
+ /// Get the type name specified type and size
+ ///
+ /// the type key
+ /// the SQL length
+ /// the SQL scale
+ /// the SQL precision
+ ///
+ /// The associated name with smallest capacity >= size if available and the
+ /// default type name otherwise
+ ///
+ public string Get(DbType typecode, uint size, uint precision, uint scale)
+ {
+ SortedList map;
+ weighted.TryGetValue(typecode, out map);
+ if (map != null && map.Count > 0)
+ {
+ foreach (KeyValuePair entry in map)
+ {
+ if (size <= entry.Key)
+ {
+ return Replace(entry.Value, size, precision, scale);
+ }
+ }
+ }
+ //Could not find a specific type for the size, using the default
+ return Replace(Get(typecode), size, precision, scale);
+ }
+
+ ///
+ /// For types with a simple length, this method returns the definition
+ /// for the longest registered type.
+ ///
+ ///
+ ///
+ public string GetLongest(DbType typecode)
+ {
+ SortedList map;
+ weighted.TryGetValue(typecode, out map);
+
+ if (map != null && map.Count > 0)
+ return Replace(map.Values[map.Count - 1], map.Keys[map.Count - 1], 0, 0);
+
+ return Get(typecode);
+ }
+
+ ///
+ /// Set a type name for specified type key and capacity
+ ///
+ /// the type key
+ /// the (maximum) type size/length
+ /// The associated name
+ public void Put(DbType typecode, uint capacity, string value)
+ {
+ SortedList map;
+ if (!weighted.TryGetValue(typecode, out map))
+ {
+ // add new ordered map
+ weighted[typecode] = map = new SortedList();
+ }
+ map[capacity] = value;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Put(DbType typecode, string value)
+ {
+ defaults[typecode] = value;
+ }
+ }
}
\ No newline at end of file
diff --git a/Rebus.AdoNet/Dialects/YugabyteDbDialect.cs b/Rebus.AdoNet/Dialects/YugabyteDbDialect.cs
index 1f137ee..9ede794 100644
--- a/Rebus.AdoNet/Dialects/YugabyteDbDialect.cs
+++ b/Rebus.AdoNet/Dialects/YugabyteDbDialect.cs
@@ -61,5 +61,16 @@ public override bool IsDuplicateKeyException(DbException ex)
return false;
}
+
+ public override bool IsOptimisticLockingException(DbException ex)
+ {
+ if (ex != null && _postgresExceptionNames.Contains(ex.GetType().Name))
+ {
+ var psqlex = new PostgreSqlExceptionAdapter(ex);
+ return psqlex.Code == "40001";
+ }
+
+ return false;
+ }
}
}
\ No newline at end of file
diff --git a/Rebus.AdoNet/Facilities/IDbConnectionExtensions.cs b/Rebus.AdoNet/Facilities/IDbConnectionExtensions.cs
new file mode 100644
index 0000000..cc39d17
--- /dev/null
+++ b/Rebus.AdoNet/Facilities/IDbConnectionExtensions.cs
@@ -0,0 +1,63 @@
+using System.Data.Common;
+using System.Collections.Generic;
+
+namespace System.Data
+{
+ internal static class IDbConnectionExtensions
+ {
+ ///
+ /// Fetch the column names with their data types..
+ /// where the Tuple.Item1 is the column name and the Tuple.Item2 is the data type.
+ ///
+ public static IEnumerable> GetColumnSchemaFor(this IDbConnection @this, string tableName)
+ {
+ if (@this == null)
+ throw new ArgumentNullException(nameof(@this));
+
+ if (string.IsNullOrWhiteSpace(tableName))
+ throw new ArgumentNullException(nameof(tableName));
+
+ // XXX: In order, to retrieve the schema information we can specify
+ // the catalog (0), schema (1), table name (2) and column name (3).
+ var restrictions = new string[4];
+ restrictions[2] = tableName;
+
+ var data = new List>();
+ var schemas = (@this as DbConnection).GetSchema("Columns", restrictions);
+
+ foreach (DataRow row in schemas.Rows)
+ {
+ var name = row["COLUMN_NAME"] as string;
+ var type = row["DATA_TYPE"] as string;
+ data.Add(Tuple.Create(name, type));
+ }
+
+ return data.ToArray();
+ }
+
+ ///
+ /// Retrieve table's indexes for a specific table.
+ ///
+ public static IEnumerable GetIndexesFor(this IDbConnection @this, string tableName)
+ {
+ if (@this == null)
+ throw new ArgumentNullException(nameof(@this));
+
+ if (string.IsNullOrWhiteSpace(tableName))
+ throw new ArgumentNullException(nameof(tableName));
+
+ // XXX: In order, to retrieve the schema information we can specify
+ // the catalog (0), schema (1), table name (2) and column name (3).
+ var restrictions = new string[4];
+ restrictions[2] = tableName;
+
+ var data = new List();
+ var schemas = (@this as DbConnection).GetSchema("Indexes", restrictions);
+
+ foreach (DataRow row in schemas.Rows)
+ data.Add(row["INDEX_NAME"] as string);
+
+ return data.ToArray();
+ }
+ }
+}
diff --git a/Rebus.AdoNet/Properties/AssemblyInfo.cs b/Rebus.AdoNet/Properties/AssemblyInfo.cs
index 7adf52b..139f099 100644
--- a/Rebus.AdoNet/Properties/AssemblyInfo.cs
+++ b/Rebus.AdoNet/Properties/AssemblyInfo.cs
@@ -1,39 +1,39 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Rebus.AdoNet")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Rebus.AdoNet")]
-[assembly: AssemblyCopyright("Copyright © Evidencias Certificadas S.L. (2015~2016)")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("03b56847-7469-4db3-b146-2d29ce61663e")]
-
-[assembly: InternalsVisibleTo("Rebus.AdoNet.Tests")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("0.0.0.0")]
-[assembly: AssemblyFileVersion("0.0.0.0")]
-[assembly: AssemblyInformationalVersion("VERSION_STRING")]
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Rebus.AdoNet")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Rebus.AdoNet")]
+[assembly: AssemblyCopyright("Copyright © Evidencias Certificadas S.L. (2015~2016)")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("03b56847-7469-4db3-b146-2d29ce61663e")]
+
+[assembly: InternalsVisibleTo("Rebus.AdoNet.Tests")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("0.0.0.0")]
+[assembly: AssemblyFileVersion("0.0.0.0")]
+[assembly: AssemblyInformationalVersion("VERSION_STRING")]
diff --git a/Rebus.AdoNet/Rebus.AdoNet.csproj b/Rebus.AdoNet/Rebus.AdoNet.csproj
index 457e88f..7f1360c 100644
--- a/Rebus.AdoNet/Rebus.AdoNet.csproj
+++ b/Rebus.AdoNet/Rebus.AdoNet.csproj
@@ -69,6 +69,7 @@
+
diff --git a/Rebus.AdoNet/Schema/AdoNetColumn.cs b/Rebus.AdoNet/Schema/AdoNetColumn.cs
index ff23ea1..9d0a066 100644
--- a/Rebus.AdoNet/Schema/AdoNetColumn.cs
+++ b/Rebus.AdoNet/Schema/AdoNetColumn.cs
@@ -1,22 +1,22 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Data;
-using System.Data.Common;
-using System.Text;
-
-namespace Rebus.AdoNet.Schema
-{
- public class AdoNetColumn
- {
- public string Name { get; set; }
- public DbType DbType { get; set; }
- public uint Length { get; set; }
- public uint Precision { get; set; }
- public uint Scale { get; set; }
- public bool Nullable { get; set; }
- public bool Identity { get; set; }
- public bool Array { get; set; }
- public object Default { get; set; }
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Data;
+using System.Data.Common;
+using System.Text;
+
+namespace Rebus.AdoNet.Schema
+{
+ public class AdoNetColumn
+ {
+ public string Name { get; set; }
+ public DbType DbType { get; set; }
+ public uint Length { get; set; }
+ public uint Precision { get; set; }
+ public uint Scale { get; set; }
+ public bool Nullable { get; set; }
+ public bool Identity { get; set; }
+ public bool Array { get; set; }
+ public object Default { get; set; }
+ }
+}
diff --git a/Rebus.AdoNet/Schema/AdoNetIndex.cs b/Rebus.AdoNet/Schema/AdoNetIndex.cs
index f86335e..959a8eb 100755
--- a/Rebus.AdoNet/Schema/AdoNetIndex.cs
+++ b/Rebus.AdoNet/Schema/AdoNetIndex.cs
@@ -1,30 +1,30 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Rebus.AdoNet.Schema
-{
- public class AdoNetIndex
- {
- public enum SortOrder
- {
- Unspecified = -1,
- Ascending = 0,
- Descending = 1
- }
-
- public enum Kinds
- {
- Default = 0,
- BTree = 1,
- GIN = 2
- }
-
- public string Name { get; set; }
- public string[] Columns { get; set; }
- public SortOrder Order { get; set; }
- public Kinds Kind { get; set; }
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Rebus.AdoNet.Schema
+{
+ public class AdoNetIndex
+ {
+ public enum SortOrder
+ {
+ Unspecified = -1,
+ Ascending = 0,
+ Descending = 1
+ }
+
+ public enum Kinds
+ {
+ Default = 0,
+ BTree = 1,
+ GIN = 2
+ }
+
+ public string Name { get; set; }
+ public string[] Columns { get; set; }
+ public SortOrder Order { get; set; }
+ public Kinds Kind { get; set; }
+ }
+}
diff --git a/Rebus.AdoNet/Schema/AdoNetTable.cs b/Rebus.AdoNet/Schema/AdoNetTable.cs
index c903d49..1495f99 100755
--- a/Rebus.AdoNet/Schema/AdoNetTable.cs
+++ b/Rebus.AdoNet/Schema/AdoNetTable.cs
@@ -1,19 +1,19 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Data;
-using System.Data.Common;
-using System.Text;
-
-namespace Rebus.AdoNet.Schema
-{
- public class AdoNetTable
- {
- public string Name { get; set; }
- public IEnumerable Columns { get; set; }
- public string[] PrimaryKey { get; set; }
- public IEnumerable Indexes { get; set; }
-
- public bool HasCompositePrimaryKey => PrimaryKey?.Count() > 1;
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Data;
+using System.Data.Common;
+using System.Text;
+
+namespace Rebus.AdoNet.Schema
+{
+ public class AdoNetTable
+ {
+ public string Name { get; set; }
+ public IEnumerable Columns { get; set; }
+ public string[] PrimaryKey { get; set; }
+ public IEnumerable Indexes { get; set; }
+
+ public bool HasCompositePrimaryKey => PrimaryKey?.Count() > 1;
+ }
+}
diff --git a/Rebus.AdoNet/netfx/System/Guard.cs b/Rebus.AdoNet/netfx/System/Guard.cs
index ab217bd..ff733b4 100755
--- a/Rebus.AdoNet/netfx/System/Guard.cs
+++ b/Rebus.AdoNet/netfx/System/Guard.cs
@@ -1,101 +1,101 @@
-#region BSD License
-/*
-Copyright (c) 2011, NETFx
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without modification,
-are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this list
- of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice, this
- list of conditions and the following disclaimer in the documentation and/or other
- materials provided with the distribution.
-
-* Neither the name of Clarius Consulting nor the names of its contributors may be
- used to endorse or promote products derived from this software without specific
- prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
-EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
-OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
-SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
-INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
-TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
-BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
-ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
-DAMAGE.
-*/
-#endregion
-
-using System;
-using System.Diagnostics;
-using System.Linq.Expressions;
-
-///
-/// Common guard class for argument validation.
-///
-///
-[DebuggerStepThrough]
-static class Guard
-{
- ///
- /// Ensures the given is not null.
- /// Throws otherwise.
- ///
- /// The is null.
- public static void NotNull(Expression> reference, T value)
- {
- if (value == null)
- throw new ArgumentNullException(GetParameterName(reference), "Parameter cannot be null.");
- }
-
- ///
- /// Ensures the given string is not null or empty.
- /// Throws in the first case, or
- /// in the latter.
- ///
- /// The is null or an empty string.
- public static void NotNullOrEmpty(Expression> reference, string value)
- {
- NotNull(reference, value);
- if (value.Length == 0)
- throw new ArgumentException("Parameter cannot be empty.", GetParameterName(reference));
- }
-
- ///
- /// Ensures the given string is valid according
- /// to the function. Throws
- /// otherwise.
- ///
- /// The is not valid according
- /// to the function.
- public static void IsValid(Expression> reference, T value, Func validate, string message)
- {
- if (!validate(value))
- throw new ArgumentException(message, GetParameterName(reference));
- }
-
- ///
- /// Ensures the given string is valid according
- /// to the function. Throws
- /// otherwise.
- ///
- /// The is not valid according
- /// to the function.
- public static void IsValid(Expression> reference, T value, Func validate, string format, params object[] args)
- {
- if (!validate(value))
- throw new ArgumentException(string.Format(format, args), GetParameterName(reference));
- }
-
- private static string GetParameterName(Expression reference)
- {
- var lambda = reference as LambdaExpression;
- var member = lambda.Body as MemberExpression;
-
- return member.Member.Name;
- }
+#region BSD License
+/*
+Copyright (c) 2011, NETFx
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this list
+ of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or other
+ materials provided with the distribution.
+
+* Neither the name of Clarius Consulting nor the names of its contributors may be
+ used to endorse or promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+*/
+#endregion
+
+using System;
+using System.Diagnostics;
+using System.Linq.Expressions;
+
+///
+/// Common guard class for argument validation.
+///
+///
+[DebuggerStepThrough]
+static class Guard
+{
+ ///
+ /// Ensures the given is not null.
+ /// Throws otherwise.
+ ///
+ /// The is null.
+ public static void NotNull(Expression> reference, T value)
+ {
+ if (value == null)
+ throw new ArgumentNullException(GetParameterName(reference), "Parameter cannot be null.");
+ }
+
+ ///
+ /// Ensures the given string is not null or empty.
+ /// Throws in the first case, or
+ /// in the latter.
+ ///
+ /// The is null or an empty string.
+ public static void NotNullOrEmpty(Expression> reference, string value)
+ {
+ NotNull(reference, value);
+ if (value.Length == 0)
+ throw new ArgumentException("Parameter cannot be empty.", GetParameterName(reference));
+ }
+
+ ///
+ /// Ensures the given string is valid according
+ /// to the function. Throws
+ /// otherwise.
+ ///
+ /// The is not valid according
+ /// to the function.
+ public static void IsValid(Expression> reference, T value, Func validate, string message)
+ {
+ if (!validate(value))
+ throw new ArgumentException(message, GetParameterName(reference));
+ }
+
+ ///
+ /// Ensures the given string is valid according
+ /// to the function. Throws
+ /// otherwise.
+ ///
+ /// The is not valid according
+ /// to the function.
+ public static void IsValid(Expression> reference, T value, Func validate, string format, params object[] args)
+ {
+ if (!validate(value))
+ throw new ArgumentException(string.Format(format, args), GetParameterName(reference));
+ }
+
+ private static string GetParameterName(Expression reference)
+ {
+ var lambda = reference as LambdaExpression;
+ var member = lambda.Body as MemberExpression;
+
+ return member.Member.Name;
+ }
}
\ No newline at end of file
diff --git a/Rebus.AdoNet/packages.config b/Rebus.AdoNet/packages.config
index d2e5194..e81bee4 100755
--- a/Rebus.AdoNet/packages.config
+++ b/Rebus.AdoNet/packages.config
@@ -1,8 +1,8 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
\ No newline at end of file
From 446b45d76f27a94fd378e27ecf8e632384fa24ec Mon Sep 17 00:00:00 2001
From: Juanje
Date: Tue, 19 Jul 2022 20:28:48 +0200
Subject: [PATCH 8/8] Restore gitattributes
---
.gitattributes | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.gitattributes b/.gitattributes
index 028e9b4..8b37475 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,8 +1,8 @@
# These files are text and should be normalized (convert crlf => lf)
-#*.cs text diff=csharp
+*.cs text diff=csharp
*.xaml text
*.csproj text
-#*.sln text
+*.sln text
*.tt text
*.ps1 text
*.cmd text
@@ -11,7 +11,7 @@
*.cshtml text
*.html text
*.js text
-#*.config text
+*.config text
# Images should be treated as binary
# (binary is a macro for -text -diff)